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

时间:2016-11-21 15:36:14

标签: java x509

我几天都在努力解决这个问题。我正在使用在嵌入式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( );
}

4 个答案:

答案 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_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少。

这里是一个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

我在DER:MIHMMQswCQYDVQQGDAJkZTERMA8GA1UECAwIYsO2ciBsaW4xEjAQBgNVBAcMCWLDlnIgbCBpbjEuMCwGA1UECgwl0JLQuNC60LjQv9C10LTQuNGOINCS0LjQutC40L/QtdC00LjRjjEWMBQGA1UECwwNZXhhbXBsZSBjb3JwLjEoMCYGA1UEAwwfbWljaGFlbC1vIGNlcnRpZmljYXRlIGF1dGhvcml0eTEkMCIGCSqGSIb3DQEJAQwVbWljaGFlbC1vQGV4YW1wbGUuY29t中的Java代码中正确规范了主题X.509名称

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