Java 1.7 X.509证书主题哈希 OpenSSL 1.0+兼容

3
我已经苦恼了几天。我正在开发一个Java 1.7应用程序,运行在嵌入式Linux环境中。OpenSSL不可用,并且我无法控制设备上的OS映像中存在什么。我需要计算自签名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( );
}

构建了OpenSSL。我的DER编码数据与openssl略有不同。看起来openssl将每个主题名称条目标记为0x0C而不是0x13(可打印字符串)。 0x0C在X.208的允许标记表中没有出现,因此我不确定这意味着什么。我使用的ASN1库将CN标记为0x0C,但将所有其他主题名称条目标记为0x13。这就是为什么当存在CN以外的条目时,我的条目与openssl不匹配的原因。我可以用胶带修补它,但我想知道为什么。规格的新修改? - NefariousB
4个回答

2

好的,暂时使用胶带。这似乎适用于我可用于测试的所有证书。这是getSubjectHash方法的重写版本:

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( );
            term[9] = 0x0c; // change tag from 0x13 (printable string) to 0x0c
            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( "" );

    }
    catch ( Exception ex ) {
        throw new RuntimeException( "uh-oh" );
    }
}

标签0x0C是UTF8String - RFC2459。 - NefariousB

1
这个答案是我找到的最接近好答案,但还远远不够好。
其中有一些误解:
- X509_NAME_hash 返回一个无符号长整型 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)));
}

希望这能帮助到某人。

1
以下有几个回答对我非常有帮助,但我认为它们是不完整的(错误的)。忘记Java创建的规范格式吧,它与OpenSSL创建的格式不兼容,也不能用于重新编码到OpenSSL格式。请注意,"规范"格式没有标准。(如果需要,我可以详细介绍)。
我的代码基于RFC的Name定义 (其他回答中没有涉及),以及OpenSSL代码 (其他回答只涵盖了部分)。
我已经测试了我的代码:

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_canonstatic。请删除并重新编译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要小得多。

这里有一个自签名证书,其中包含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

我在Java代码中从DER格式得到了正确规范化的X.509名称: MIHMMQswCQYDVQQGDAJkZTERMA8GA1UECAwIYsO2ciBsaW4xEjAQBgNVBAcMCWLDlnIgbCBpbjEuMCwGA1UECgwl0JLQuNC60LjQv9C10LTQuNGOINCS0LjQutC40L/QtdC00LjRjjEWMBQGA1UECwwNZXhhbXBsZSBjb3JwLjEoMCYGA1UEAwwfbWljaGFlbC1vIGNlcnRpZmljYXRlIGF1dGhvcml0eTEkMCIGCSqGSIb3DQEJAQwVbWljaGFlbC1vQGV4YW1wbGUuY29t


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));
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接