Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

parsePkcs12 has problems with certificates that contain diacritics in friendlyName #115

Open
pavkoccino opened this issue Oct 17, 2024 · 0 comments

Comments

@pavkoccino
Copy link

I imagine this is rather an edge case given that we are living in Czech republic and our language use specific letters (ÁÉĚÍÝÓÚŮŽŠČŘĎŤŇáéěíýóúůžščřďťň), but for us it was a huge blocker that required fast workaround and I would be glad if you could take a look at it.

We are using your package to extract data from .pfx/.p12 files and turns out that if friendlyName attribute contains any of the mentioned letters, the parsePkcs12 function cannot handle it and throws exception.

Following certificate (posting in bytes so you can reproduce it easily), contains letter "í" in the friendlyName:
cert_in_bytes.txt
Password to decode it is: 123456789

When you pass it to parsePkcs12 function, it is gonna fail eventually in ASN1Sequence.fromBytes() constructor call with exception:
Format exception, 237 character is invalid...according to extended ASCII table, 237 is equivalent of "í". That is how I found the issue. Initially I thought it will be problem if it appears anywhere inside a certificate (like in SUBJECT, ORGANIZATION etc.), but no, it is only a problem if it is in the friendlyName.

As this is represented under following OID

    // OID 1.2.840.113549.1.9.20
    // OID for friendlyName in hexadecimal: 06 09 2A 86 48 86 F7 0D 01 09 14
    List<int> friendlyNameOid = [0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x09, 0x14];

I am able to look for those bytes in the decryptes Uint8List and find and replace diacritics that follows after it.

Functions added to hex_utils.dart

  /// Modifies the given [bytes] to replace Czech diacritics with their ASCII equivalents
  static Uint8List _sanitizeCzechDiacritics(Uint8List bytes) {
    Map<int, int> czechDiacriticsMap = {
      // Uppercase letters
      193: 65, // Á -> A
      201: 69, // É -> E
      204: 69, // Ě -> E
      205: 73, // Í -> I
      221: 89, // Ý -> Y
      211: 79, // Ó -> O
      218: 85, // Ú -> U
      366: 85, // Ů -> U
      381: 90, // Ž -> Z
      352: 83, // Š -> S
      268: 67, // Č -> C
      344: 82, // Ř -> R
      270: 68, // Ď -> D
      356: 84, // Ť -> T
      327: 78, // Ň -> N

      // Lowercase letters
      225: 97, // á -> a
      233: 101, // é -> e
      283: 101, // ě -> e
      237: 105, // í -> i
      253: 121, // ý -> y
      243: 111, // ó -> o
      250: 117, // ú -> u
      367: 117, // ů -> u
      382: 122, // ž -> z
      353: 115, // š -> s
      269: 99, // č -> c
      345: 114, // ř -> r
      271: 100, // ď -> d
      357: 116, // ť -> t
      328: 110, // ň -> n
    };

    var sanitizedContent = Uint8List.fromList(bytes);

    for (int i = 0; i < sanitizedContent.length; i++) {
      int byte = sanitizedContent[i];

      // Check if the byte is in the map and replace it with the ASCII equivalent
      if (czechDiacriticsMap.containsKey(byte)) {
        final replacement = czechDiacriticsMap[byte]!;
        print('Found Czech diacritic byte at index $i: $byte, it will be replaced with $replacement');
        sanitizedContent[i] = replacement;
      }
    }

    return sanitizedContent;
  }

  /// Finds and sanitizes the friendly name in the given [content] byte array
  static Uint8List? findAndSanitizeFriendlyName(Uint8List content) {
    // OID 1.2.840.113549.1.9.20
    // OID for friendlyName in hexadecimal: 06 09 2A 86 48 86 F7 0D 01 09 14
    List<int> friendlyNameOid = [0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x09, 0x14];

    // Scan through the content byte array to locate the OID
    for (int i = 0; i <= content.length - friendlyNameOid.length; i++) {
      bool match = true;
      for (int j = 0; j < friendlyNameOid.length; j++) {
        if (content[i + j] != friendlyNameOid[j]) {
          match = false;
          break;
        }
      }

      // If the OID matches, process the friendlyName
      if (match) {
        print('Found friendlyName OID at index $i');

        // After finding the OID, the next bytes represent the length and content of the BMPString
        int lengthIndex = i + friendlyNameOid.length + 3; // Length comes after OID
        int length = content[lengthIndex]; // Assuming one-byte length for simplicity

        // Extract the BMPString (two-byte character encoding)
        int startIndex = lengthIndex + 1; // Start reading after the length byte
        int endIndex = startIndex + length;
        Uint8List friendlyNameBytes = content.sublist(startIndex, endIndex); // Extract the friendly name bytes

        // Sanitize the friendly name bytes
        final sanitized = _sanitizeCzechDiacritics(friendlyNameBytes);

        // Replace the original friendly name bytes with the sanitized bytes
        for (int j = 0; j < sanitized.length; j++) {
          content[startIndex + j] = sanitized[j];
        }

        // Convert to loggable string, the replace take care of SUB, NUL, ACK chars
        String originalFriendlyNameString =
            String.fromCharCodes(friendlyNameBytes).replaceAll(RegExp(r'[\u0000-\u001F\u007F]'), '');
        String convertedFriendlyNameString =
            String.fromCharCodes(sanitized).replaceAll(RegExp(r'[\u0000-\u001F\u007F]'), '');

        print('Original Friendly Name: $originalFriendlyNameString');
        print('Sanitized Friendly Name: $convertedFriendlyNameString');

        // Return the modified content with the sanitized friendly name
        return content;
      }
    }

    return null;
  }

Usage in parsePkcs12.dart (look for function findAndSanitizeFriendlyName)

  static List<String> parsePkcs12(
    Uint8List pkcs12, {
    String? password,
  }) {
    Uint8List? pwFormatted;
    if (password != null) {
      pwFormatted = formatPkcs12Password(Uint8List.fromList(password.codeUnits));
    }

    var pems = <String>[];
    var parser = ASN1Parser(pkcs12);
    var wrapperSeq = parser.nextObject() as ASN1Sequence;
    var pfx = ASN1Pfx.fromSequence(wrapperSeq);

    var authSafeContent = pfx.authSafe.content as ASN1OctetString;
    parser = ASN1Parser(authSafeContent.valueBytes);
    wrapperSeq = parser.nextObject() as ASN1Sequence;
    if (wrapperSeq.elements == null || wrapperSeq.elements!.isEmpty) {
      // TODO
    }
    for (var e in wrapperSeq.elements!) {
      if (e is ASN1Sequence) {
        if (e.elements == null || e.elements!.isEmpty) {
          // TODO
        }
        var contentInfo = ASN1ContentInfo.fromSequence(e);

        switch (contentInfo.contentType.objectIdentifierAsString) {
          case '1.2.840.113549.1.7.6': // encryptedData
            var encryptedData = ASN1EncryptedData.fromSequence(contentInfo.content as ASN1Sequence);
            var encryptedContentInfo = encryptedData.encryptedContentInfo;

            // GET ALGORITHM
            var contentEncryptionAlgorithm = encryptedContentInfo.contentEncryptionAlgorithm;
            var encryptionAlgorithm = _algorithmFromOi(contentEncryptionAlgorithm.algorithm.objectIdentifierAsString!);
            // GET SALT AND MACITER AND DIGEST ALGORITHM
            Uint8List salt = _getSaltFromAlgorithmParameters(contentEncryptionAlgorithm.parameters);
            int macIter = _getMacIterFromAlgorithmParameters(contentEncryptionAlgorithm.parameters);
            var digestAlgorithm = _getDigestAlgorithmFromEncryptionAlgorithm(encryptionAlgorithm);
            // DECRYPT
            var decryptedContent = _decrypt(
              encryptedContentInfo.encryptedContent!,
              encryptionAlgorithm,
              pwFormatted!,
              salt,
              macIter,
              digestAlgorithm,
            );
            var contentType = encryptedContentInfo.contentType;
            final friendlyNameSanitized = HexUtils.findAndSanitizeFriendlyName(decryptedContent);
            switch (contentType.objectIdentifierAsString) {
              case '1.2.840.113549.1.7.1': // CERTIFICATES
                var safeContents = ASN1SafeContents.fromSequence(
                  ASN1Sequence.fromBytes(friendlyNameSanitized ?? decryptedContent),
                );
                safeContents.safeBags.forEach((element) {
                  var pem = _pemFromSafeBag(element);
                  pems.add(pem);
                });
                break;
            }
            print('');
            break;
          case '1.2.840.113549.1.7.1': // data (PKCS #7)
            final friendlyNameSanitized = HexUtils.findAndSanitizeFriendlyName(contentInfo.content!.valueBytes!);
            var safeContents = ASN1SafeContents.fromSequence(
              ASN1Sequence.fromBytes(friendlyNameSanitized ?? contentInfo.content!.valueBytes!),
            );
            safeContents.safeBags.forEach((element) {
              var bagValueSeq = element.bagValue as ASN1Sequence;
              switch (element.bagId.objectIdentifierAsString!) {
                case "1.2.840.113549.1.12.10.1.3": // pkcs-12-certBag
                  var seq = element.bagValue as ASN1Sequence;
                  var octet = ASN1OctetString.fromBytes(seq.elements!.elementAt(1).valueBytes!);
                  var asn1o = ASN1Sequence.fromBytes(octet.valueBytes!);
                  pems.insert(
                    0,
                    X509Utils.encodeASN1ObjectToPem(
                      asn1o,
                      X509Utils.BEGIN_CERT,
                      X509Utils.END_CERT,
                    ),
                  );
                  break;
                case "1.2.840.113549.1.12.10.1.2": // pkcs-12-pkcs-8ShroudedKeyBag
                  var contentEncryptionAlgorithm =
                      ASN1AlgorithmIdentifier.fromSequence(bagValueSeq.elements!.elementAt(0) as ASN1Sequence);
                  // GET ALGORITHM
                  var encryptionAlgorithm =
                      _algorithmFromOi(contentEncryptionAlgorithm.algorithm.objectIdentifierAsString!);
                  // GET SALT AND MACITER AND DIGEST ALGORITHM
                  Uint8List salt = _getSaltFromAlgorithmParameters(contentEncryptionAlgorithm.parameters);
                  int macIter = _getMacIterFromAlgorithmParameters(contentEncryptionAlgorithm.parameters);
                  var digestAlgorithm = _getDigestAlgorithmFromEncryptionAlgorithm(encryptionAlgorithm);
                  // DECRYPT
                  var decryptedContent = _decrypt(
                    bagValueSeq.elements!.elementAt(1).valueBytes!,
                    encryptionAlgorithm,
                    pwFormatted!,
                    salt,
                    macIter,
                    digestAlgorithm,
                  );
                  var s = ASN1Sequence.fromBytes(decryptedContent);
                  pems.insert(
                    0,
                    X509Utils.encodeASN1ObjectToPem(s, CryptoUtils.BEGIN_PRIVATE_KEY, CryptoUtils.END_PRIVATE_KEY),
                  ); // TODO ECC ?
                  break;
                case "1.2.840.113549.1.12.10.1.1": // pkcs-12-keyBag
                  var seq = bagValueSeq.elements!.elementAt(1) as ASN1Sequence;
                  var identifier = seq.elements!.elementAt(0) as ASN1ObjectIdentifier;
                  switch (identifier.objectIdentifierAsString!) {
                    case "1.2.840.113549.1.1.1": // rsaEncryption
                      pems.insert(
                        0,
                        X509Utils.encodeASN1ObjectToPem(
                            bagValueSeq, CryptoUtils.BEGIN_PRIVATE_KEY, CryptoUtils.END_PRIVATE_KEY),
                      );
                      break;
                  }
                  break;
              }
            });

            break;
        }
      }
    }
    return pems;
  }

Therefore to reproduce it on the current version of basic utils, you can open any project and just call:

final certBytes = [COPY THE ATTACHED LIST];
final extractedPems = Pkcs12Utils.parsePkcs12(certBytes, password: '123456789');

You should get the Format Input exception with 237.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant