我几天都在努力解决这个问题。我正在使用在嵌入式Linux环境中运行的Java 1.7应用程序。 OpenSSL不可用,我无法控制设备上的操作系统映像中的内容。我需要计算自签名X.509证书的主题哈希值,产生与OpenSSL 1.0+相同的结果。这个现有的答案让我开始:
The new subject hash openssl algorithm differs
我的测试应用中的代码如下所示。我的计算适用于主题名称仅包含CN值的证书,但它不适用于指定了任何其他主题组件(OU,O,L,ST或C)的证书。对于那些证书,整个主题的散列(较少的介绍顺序)不匹配。根据上面的答案,我已经提取了每个组件(使用getObjectAt()方法)并将它们中的每一个单独散列(没有快乐),颠倒它们的顺序并将它们全部哈希(没有快乐),以及其他一些变体主题。我一直试图避免我担心下载OpenSSL源并使其运行更耗时的工作,这样我就可以检查中间结果,看看我哪里出错了。也许有人这样做可以提供一些指导。
private static void getSubjectHash( X509Certificate x509Cert )
{
try {
// get the subject principal
X500Principal x500Princ = x509Cert.getSubjectX500Principal( );
// create a new principal using canonical name (order, spacing, etc.) and get it in ANS1 DER format
byte[] newPrincEnc = new X500Principal( x500Princ.getName( X500Principal.CANONICAL ) ).getEncoded( );
// read it in as an ASN1 Sequence to avoid custom parsing
ASN1InputStream aIn = new ASN1InputStream( newPrincEnc );
ASN1Sequence seq = (ASN1Sequence) aIn.readObject( );
List<byte[]> terms = new ArrayList<>( );
int finalLen = 0;
int i = 0;
// hash the encodables for each term individually and accumulate them in a list
for ( ASN1Encodable asn1Set : seq.toArray( ) ) {
byte[] term = ( (ASN1Set) asn1Set ).getEncoded( );
terms.add( term );
finalLen += term.length;
// digest the term
byte[] hashBytes = truncatedHash( getDigest( term ), 4 );
printByteArray( String.format( "hash of object at %d:", i++ ), hashBytes );
System.out.println( "" );
}
// hash all terms together in order of appearance
int j = 0;
byte[] finalEncForw = new byte[finalLen];
for ( byte[] term : terms )
for ( byte b : term )
finalEncForw[j++] = b;
// digest and truncate
byte[] hashBytes = truncatedHash( getDigest( finalEncForw ), 4 );
printByteArray( "hash of all terms in forward order", hashBytes );
System.out.println( "" );
// hash all terms together in reverse order
j = 0;
byte[] finalEncRev = new byte[finalLen];
for ( int k = terms.size( ) - 1; k >= 0; --k )
for ( byte b : terms.get( k ) )
finalEncRev[j++] = b;
// digest and truncate
hashBytes = truncatedHash( getDigest( finalEncRev ), 4 );
printByteArray( "hash of all terms in reverse order", hashBytes );
}
catch ( Exception ex ) {
throw new RuntimeException( "uh-oh" );
}
}
private static byte[] getDigest( byte[] toHash )
{
MessageDigest md;
try {
md = MessageDigest.getInstance( "SHA1" );
}
catch ( NoSuchAlgorithmException nsa ) {
throw new RuntimeException( "no such algorithm" );
}
return md.digest( toHash );
}
private static byte[] truncatedHash( byte[] hash, int truncatedLength )
{
if ( truncatedLength < 1 || hash.length < 1 )
return new byte[0];
byte[] result = new byte[truncatedLength];
for ( int i = 0; i < truncatedLength; ++i )
result[truncatedLength - 1 - i] = hash[i];
return result;
}
private static void printByteArray( String name, byte[] bytes )
{
System.out.println( name + " length=" + String.valueOf( bytes.length ) );
for ( byte b: bytes ) {
System.out.print( String.format( "%02X ", Byte.toUnsignedInt( b ) ) );
}
System.out.println( );
}
答案 0 :(得分:1)
好的,现在是胶带。这似乎适用于我可以测试的所有证书。这是getSubjectHash方法的重写版本:
{{1}}
答案 1 :(得分:1)
这个答案是我找到的最接近的一个答案,但与这个答案相去甚远
其中有一些误解
-X509_NAME_hash返回 long unsigned long X509_NAME_hash(X509_NAME *x)
-不是第10个字符需要为0x0c
,而是值的第1个字符
-将文本转换为小写不能解决问题
为了克服这些问题,我开始使用X500Name代替X500Principal作为输入参数,可以很容易地在两者之间进行转换
原因是X500Name公开了RDN数组,我们可以从该数组中检索值(在这里,我忽略了多值选项,仅使用第一个)
检索名称可以使我进行规范转换(不仅是小写字母),而且知道它从哪里开始,将第一个字节替换为0x0c
更新的代码现在包含完整的解决方案,而无需进行多个字节到字符串的反向转换
public static long calculateX500NameHash(X500Name name) throws IOException, NoSuchAlgorithmException {
byte[] nameEncoded = name.getEncoded();
final ASN1Sequence asn1Sequence = (ASN1Sequence) ASN1Primitive.fromByteArray(nameEncoded);
List<byte[]> rdnList = new ArrayList<>();
int length = 0;
for (ASN1Encodable asn1Set : asn1Sequence.toArray()) {
byte[] bytes = ((ASN1Set) asn1Set).getEncoded();
length += bytes.length;
rdnList.add(bytes);
}
byte[] nameBytes = new byte[length];
int counter = 0;
int addedItems = 0;
for (RDN rdn : name.getRDNs()) {
// Get original encoded RDN value
byte[] encoded = rdn.getFirst().getValue().toASN1Primitive().getEncoded();
// Get the RDN value as string without the prefix
StringBuilder content = new StringBuilder();
for (int j = 2; j < encoded.length; j++) {
content.append((char) encoded[j]);
}
// canonicalize the string
byte[] updateContent = IETFUtils.canonicalize(content.toString()).getBytes(StandardCharsets.UTF_8);
// create new byte[] with the updated prefix and canonicalized string
byte[] updated = new byte[encoded.length];
updated[0] = 0x0c;
updated[1] = encoded[1];
System.arraycopy(updateContent, 0, updated, 2, updateContent.length);
// get full RDN with type prefix
byte[] rdnFromList = rdnList.get(counter);
int fullLength = rdnFromList.length;
int valueLength = encoded.length;
// Additional check, expect to always return true
if (isMatchingTheEnd(rdnFromList, encoded)) {
int prefixLength = (fullLength - valueLength);
// add the beginning of the full RDN to the `nameBytes` array without the value
System.arraycopy(rdnFromList, 0, nameBytes, addedItems, prefixLength);
// add the updated value to the `nameBytes` array
System.arraycopy(updated, 0, nameBytes, addedItems + prefixLength, valueLength);
} else {
// safeguard
System.arraycopy(rdnFromList, 0, nameBytes, addedItems, fullLength);
}
addedItems += fullLength;
++counter;
}
return getHashFromByteArray(nameBytes) & 0xffffffffL;
}
private static boolean isMatchingTheEnd(byte[] fullRdn, byte[] rdnValue) {
int fullRdnLength = fullRdn.length;
int rdnValueLength = rdnValue.length;
if (fullRdnLength > rdnValueLength) {
int prefixLength = fullRdnLength - rdnValueLength;
for (int i = 0; i < rdnValueLength; i++) {
if (fullRdn[prefixLength + i] != rdnValue[i]) {
return false;
}
}
return true;
}
return false;
}
private static long getHashFromByteArray(byte[] nameBytes) throws NoSuchAlgorithmException {
byte[] digest = MessageDigest.getInstance("SHA1").digest(nameBytes);
return (((digest[0] & 0xff))
| (((digest[1] & 0xff) << 8))
| (((digest[2] & 0xff) << 16))
| (((digest[3] & 0xff) << 24)));
}
希望这对某人有帮助
答案 2 :(得分:1)
这里给出了几个答案,它们对我非常有帮助,我声称它们不完整(错误)。忘了Java创建的规范格式,它与OpenSSL创建的规范格式不兼容,并且不能用于重新编码为OpenSSL格式。请注意,“规范”格式没有标准。 (如果需要,我可以详细介绍)。
我的代码基于RFC's Name
definition定义(其他答案未涵盖)和OpenSSL code(其他答案未全面涵盖)。
我已经对以下代码进行了测试:
在C中的验证:
#include <openssl/asn1.h>
#include <stdio.h>
#include <string.h>
int main(void) {
ASN1_STRING * tugra_asn1 = ASN1_STRING_type_new(V_ASN1_UTF8STRING);
/*char *tugra = "E-Tuğra EBG Bilişim Teknolojileri ve Hizmetleri A.Ş.";
*/
char *wikipedia = "Википедия";
ASN1_STRING_set(tugra_asn1, tugra, -1);
printf("ASN1_STRING_length: %d\n", ASN1_STRING_length(tugra_asn1));
ASN1_STRING * tugra_asn1_canon = ASN1_STRING_new();
int ret = asn1_string_canon(tugra_asn1_canon, tugra_asn1);
printf("ret: %d\n", ret);
printf("ASN1_STRING_length: %d\n", ASN1_STRING_length(tugra_asn1_canon));
const unsigned char * data = ASN1_STRING_data(tugra_asn1_canon);
printf("ASN1_STRING_canon: %s\n", data);
printf("ASN1_tag2str: %s\n", ASN1_tag2str(ASN1_STRING_type(tugra_asn1)));
return 0;
}
该代码可用于OpenSSL 1.0.2+,但由于asn1_string_canon
为static
,因此需要进行修改。删除并重新编译OpenSSL。
现在的Java代码:
byte[] encoded = subject.getEncoded();
Asn1Sequence asn1Name = (Asn1Sequence) Asn1.decode(encoded);
ByteBuffer recoded = ByteBuffer.allocate(asn1Name.getContainer().getBodyLength());
// Based on https://github.com/openssl/openssl/blob/852c2ed260860b6b85c84f9fe96fb4d23d49c9f2/crypto/x509/x_name.c#L296-L306
// We only need the sequence elements
for (Asn1Type asn1type0 : asn1Name.getValue()) {
Asn1Set asn1Rdn = (Asn1Set) asn1type0;
for (Asn1Type asn1type1 : asn1Rdn.getValue()) {
Asn1Sequence asn1Ava = (Asn1Sequence) asn1type1;
List<Asn1Type> asn1AvaTV = asn1Ava.getValue();
Asn1ObjectIdentifier asn1AttrType = (Asn1ObjectIdentifier) asn1AvaTV.get(0);
Asn1Type asn1AttrValue = asn1AvaTV.get(1);
UniversalTag valueTag = asn1AttrValue.tag().universalTag();
switch(valueTag) {
case UTF8_STRING:
case BMP_STRING:
case UNIVERSAL_STRING:
case PRINTABLE_STRING:
case T61_STRING:
case IA5_STRING:
case VISIBLE_STRING:
Asn1String asn1AttrValueString = (Asn1String) asn1AttrValue;
String string = asn1AttrValueString.getValue();
string = string.replaceAll("^\\s+|\\s+$", "").replaceAll("\\s+", " ");
char[] chars = string.toCharArray();
for (int i = 0; i < chars.length; i++) {
char c = chars[i];
if (c >= 'A' && c <= 'Z')
chars[i] = Character.toLowerCase(c);
}
String utf8String = new String(chars);
Asn1Utf8String asn1Utf8Sring = new Asn1Utf8String(utf8String);
asn1AttrValue = asn1Utf8Sring;
asn1Ava.clear();
asn1Ava.addItem(asn1AttrType);
asn1Ava.addItem(asn1AttrValue);
break;
default:
// leave as-is
break;
}
}
byte[] asn1RdnDer = asn1Rdn.encode();
// Concat for hash
if (recoded.position() + asn1RdnDer.length > recoded.capacity()) {
ByteBuffer tmp = recoded;
recoded = ByteBuffer.allocate(tmp.position() + asn1RdnDer.length);
tmp.flip();
recoded.put(tmp);
}
recoded.put(asn1RdnDer);
}
recoded.flip();
try {
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
byte[] hash = sha1.digest(Arrays.copyOf(recoded.array(), recoded.remaining()));
int truncHash = (((hash[0] & 0xff)) | (((hash[1] & 0xff) << 8))
| (((hash[2] & 0xff) << 16)) | (((hash[3] & 0xff) << 24)));
System.out.printf("subject hash: %08x%n", truncHash);
} catch (NoSuchAlgorithmException e) {
// Should not happen for SHA-1
}
您将需要:
<dependency>
<groupId>org.apache.kerby</groupId>
<artifactId>kerby-asn1</artifactId>
<version>2.0.1-SNAPSHOT</version>
</dependency>
一个非常轻巧的ASN.1库(60 kB),比BC少。
这里是一个self-signed cert,具有UTF-8字节以及很多空白:
-----BEGIN CERTIFICATE-----
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
n0tEnfE7rodFIjuSFxBSD2k=
-----END CERTIFICATE-----
主题:emailAddress=MICHAEL-O@EXAMPLE.COM,CN=\ \ \ Michael-O Certificate Authority\ \ \ \ ,OU=Example Corp.\ \ \ \ ,O=\ \ Википедию Википедию\ \ \ \ ,L=\ \ BÖR l iN\ \ \ \ ,ST=\ \ Bör lin\ \ \ ,C=DE
指纹(SHA-256):F0:04:0D:38:8A:E5:93:A8:51:1D:06:3E:96:8F:44:29:29:F2:2D:57:A1:5F:7B:CB:F9:F4:EE:98:5B:A8:50:CA
主题哈希:5ba4b7de
答案 3 :(得分:0)
感谢你的代码老兄。我已经改进了它,以支持在主题中包含扩展ASCII字符的证书(例如,éËÁñç)。
public static int X509_NAME_hash(X509Certificate x509Cert) throws IOException, NoSuchAlgorithmException {
// get the subject principal
X500Principal x500Princ = x509Cert.getSubjectX500Principal();
byte[] newPrincEnc = x500Princ.getEncoded();
final ASN1Sequence asn1Sequence = (ASN1Sequence) ASN1Primitive.fromByteArray(newPrincEnc);
Debugger.log(asn1Sequence);
List<byte[]> terms = new ArrayList<>();
int finalLen = 0;
// hash the encodables for each term individually and accumulate them in a list
for (ASN1Encodable asn1Set : asn1Sequence.toArray()) {
byte[] term = ((ASN1Set) asn1Set).getEncoded();
term[9] = 0x0c; // change tag from 0x13 (printable string) to 0x0c
for (int i = 11; i < term.length; i++) {
byte actual = term[i];
//lowercase only if the character is not ASCCI Extended (below 126)
if (actual < 127) {
term[i] = (byte) Character.toLowerCase((char) actual);
}
}
terms.add(term);
finalLen += term.length;
}
// hash all terms together in order of appearance
int j = 0;
byte[] finalEncForw = new byte[finalLen];
for (byte[] term : terms)
for (byte b : term)
finalEncForw[j++] = b;
return peekInt(MessageDigest.getInstance("SHA1").digest(finalEncForw), 0, ByteOrder.LITTLE_ENDIAN);
}
public static X509Certificate readCertificate(File rootFile) throws CertificateException, IOException {
CertificateFactory fact = CertificateFactory.getInstance("X.509");
FileInputStream is = new FileInputStream(rootFile);
return (X509Certificate) fact.generateCertificate(is);
}
public static int peekInt(byte[] src, int offset, ByteOrder order) {
if (order == ByteOrder.BIG_ENDIAN) {
return (((src[offset++] & 0xff) << 24) | ((src[offset++] & 0xff) << 16) | ((src[offset++] & 0xff) << 8)
| ((src[offset] & 0xff) << 0));
} else {
return (((src[offset++] & 0xff) << 0) | ((src[offset++] & 0xff) << 8) | ((src[offset++] & 0xff) << 16)
| ((src[offset] & 0xff) << 24));
}
}