Ephenodrom / Dart-Basic-Utils

A dart package for many helper methods fitting common situations
MIT License
364 stars 77 forks source link

Missing import for EC private keys in PKCS#8 encoding #48

Closed java-crypto closed 3 years ago

java-crypto commented 3 years ago

Thanks for your very helpful library that I could use many times in my Cross platform cryptography project that is supporting a lot of frameworks (Java, PHP, C#, JavaScript, NodeJs, Golang, Python and Dart).

When trying to run a compatible Dart version of an ECDSA signature using the curve PRIME256V1 and SHA-256 hashing I encountered the problem that your import function ("CryptoUtils.ecPrivateKeyFromPem") is only accepting an EC private key in traditional = SEC1 encoding but not in a PKCS#8 encoding that is widely used in Java etc.

Below you find a full running program that is showing the issue - the SEC1 encoded private key signs and verifies the signature against a plaintext, the PKCS#8 encoded key is failing (that's why I'm surrounding the function call with a "try/catch" construct) and in the end I'm presenting a rough coded import of an EC private key in PKCS#8 encoding that signs and verifies successfully.

Just a note regarding the EC keys - they are sample keys I used and published in my project, so don't worry.

BTW: a good place for choosing the SEC1- or PKCS#8-import could be in the PEM-header line - a SEC1 header is "BEGIN EC PRIVATE KEY", the PKCS#8 header line is "BEGIN PRIVATE KEY".

Full source code:

import 'dart:typed_data';
import 'package:pointycastle/asn1.dart';
import 'package:pointycastle/asn1/object_identifiers.dart';
import "package:pointycastle/export.dart";
import 'package:pointycastle/src/utils.dart';
import 'package:basic_utils/basic_utils.dart';

void main() {
/* add in pubspec.yaml:
dependencies:
  pointycastle: ^3.1.1
  basic_utils: ^3.4.0
 */
  // https://pub.dev/packages/pointycastle
  // https://github.com/bcgit/pc-dart/
  // https://pub.dev/packages/basic_utils
  // https://github.com/Ephenodrom/Dart-Basic-Utils

  print('Import EC keys using Basic Utils and Pointycastle');

/* EC key generation using OpenSSL:
ecdsa private key generation in traditional = sec1 encoding:
openssl ecparam -name prime256v1 -genkey -noout -out ecdsa_secp256r1_cpc_privatekey_sec1.pem
ecdsa public key generation:
openssl ec -in ecdsa_secp256r1_cpc_privatekey_sec1.pem -pubout -out ecdsa_secp256r1_cpc_publickey_sec1.pem
convert the ec private key to pkcs#8 encoding
openssl pkcs8 -topk8 -nocrypt -in ecdsa_secp256r1_cpc_privatekey_sec1.pem -out ecdsa_secp256r1_cpc_privatekey_pkcs8.pem
*/

  // import the EC private key in OpenSSL traditional format = SEC1 encoding
  ECPrivateKey ecPrivateKeyImport =
      CryptoUtils.ecPrivateKeyFromPem(loadEcPrivateKeyPemSec1());
  // import the EC public key
  ECPublicKey ecPublicKeyImport =
      CryptoUtils.ecPublicKeyFromPem(loadEcPublicKeyPem());

  // test that EC signature is working
  final dataToSignString = 'The quick brown fox jumps over the lazy dog';
  final dataToSign = createUint8ListFromString(dataToSignString);
  print(
      '\n* * * sign the plaintext with the EC private key in SEC1 encoding * * *');
  ECSignature ecSignature = ecSign(ecPrivateKeyImport, dataToSign);
  print(
      '* * * verify the signature against the plaintext with the EC public key * * *');
  bool verified = ecVerify(ecPublicKeyImport, dataToSign, ecSignature);
  print('signature verified: ' + verified.toString()); // true = working

  // import the EC private key in PKCS#8 encoding will fail
  try {
    print('\ntrying to import the PKCS#8 encoded EC private key');
    ECPrivateKey ecPrivateKeyImport =
        CryptoUtils.ecPrivateKeyFromPem(loadEcPrivateKeyPemPkcs8());
  } on Exception catch (e) {
    // Anything else that is an exception
    print('Unknown exception: $e');
  } catch (e) {
    // No specified type, handles all
    print('Something really unknown: $e');
  }
/* ECPrivateKey ecPrivateKeyImport = CryptoUtils.ecPrivateKeyFromPem(loadEcPrivateKeyPemPkcs8());
Unhandled exception:
type 'ASN1Sequence' is not a subtype of type 'ASN1OctetString' in type cast
#0      CryptoUtils.ecPrivateKeyFromDerBytes (package:basic_utils/src/CryptoUtils.dart:635:44)
#1      CryptoUtils.ecPrivateKeyFromPem (package:basic_utils/src/CryptoUtils.dart:625:12)
#2      main (package:dartprojectspointycastle/CrossPlatformCryptography/EcKeyExportImportBasicUtils.dart:69:50)
#3      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:283:19)
#4      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)
*/

  // try my own import function
  print(
      '\ntrying to import the PKCS#8 encoded EC private key using PKCS8 decode function');
  // get the data from PEM
  Uint8List ecPrivateKeyPkcs8Der =
      CryptoUtils.getBytesFromPEMString(loadEcPrivateKeyPemPkcs8());
  //ECPrivateKey ecPrivateKeyImportPkcs8 = ecPrivateKeyFromDerBytesOwn(ecPrivateKeyPkcs8Der);
  ECPrivateKey ecPrivateKeyImportPkcs8 =
      ecPrivateKeyFromDerBytesPkcs8(ecPrivateKeyPkcs8Der);
  print(
      '* * * sign the plaintext with the EC private key in PKCS#8 encoding * * *');
  ECSignature ecSignaturePkcs8 = ecSign(ecPrivateKeyImportPkcs8, dataToSign);
  print(
      '* * * verify the signature against the plaintext with the EC public key * * *');
  bool verifiedPkcs8 =
      ecVerify(ecPublicKeyImport, dataToSign, ecSignaturePkcs8);
  print('signature verified: ' + verifiedPkcs8.toString());
}

// the new - rough coded - function
///
/// Decode the given [bytes] in PKCS8 encoding into an [ECPrivateKey].
///
ECPrivateKey ecPrivateKeyFromDerBytesPkcs8(Uint8List bytes) {
  ASN1Parser asn1Parser = ASN1Parser(bytes);
  ASN1Sequence topLevelSeq = asn1Parser.nextObject() as ASN1Sequence;
  ASN1Sequence innerSeq = topLevelSeq.elements!.elementAt(1) as ASN1Sequence;
  ASN1ObjectIdentifier b2 =
      innerSeq.elements!.elementAt(1) as ASN1ObjectIdentifier;
  String? b2Data = b2.objectIdentifierAsString;
  Map<String, dynamic>? b2Curvedata =
      ObjectIdentifiers.getIdentifierByIdentifier(b2Data);
  dynamic curveName;
  if (b2Curvedata != null) {
    curveName = b2Curvedata['readableName'];
  }
  // get the octet string data for the private key der data
  ASN1OctetString octetString =
      topLevelSeq.elements!.elementAt(2) as ASN1OctetString;
  asn1Parser = ASN1Parser(octetString.valueBytes);
  ASN1Sequence octetStringSeq = asn1Parser.nextObject() as ASN1Sequence;
  ASN1OctetString octetStringKeyData =
      octetStringSeq.elements!.elementAt(1) as ASN1OctetString;
  // now generate the key
  Uint8List privateKeyDer = octetStringKeyData.valueBytes!;
  return ECPrivateKey(
      decodeBigInt(privateKeyDer), ECDomainParameters(curveName));
}

// the original import function
///
/// Decode the given [bytes] into an [ECPrivateKey].
///
ECPrivateKey ecPrivateKeyFromDerBytesOrg(Uint8List bytes) {
  var asn1Parser = ASN1Parser(bytes);
  var topLevelSeq = asn1Parser.nextObject() as ASN1Sequence;
  var privateKeyAsOctetString =
      topLevelSeq.elements!.elementAt(1) as ASN1OctetString;
  var choice = topLevelSeq.elements!.elementAt(2);
  var s = ASN1Sequence();
  var parser = ASN1Parser(choice.valueBytes);
  while (parser.hasNext()) {
    s.add(parser.nextObject());
  }
  var curveNameOi = s.elements!.elementAt(0) as ASN1ObjectIdentifier;
  var curveName;
  var data = ObjectIdentifiers.getIdentifierByIdentifier(
      curveNameOi.objectIdentifierAsString);
  if (data != null) {
    curveName = data['readableName'];
  }
  var x = privateKeyAsOctetString.valueBytes!;
  return ECPrivateKey(decodeBigInt(x), ECDomainParameters(curveName));
}

ECSignature ecSign(ECPrivateKey privateKey, Uint8List dataToSign) {
  return CryptoUtils.ecSign(privateKey, dataToSign,
      algorithmName: 'SHA-256/ECDSA');
}

bool ecVerify(
    ECPublicKey publicKey, Uint8List dataToSign, ECSignature signature) {
  return CryptoUtils.ecVerify(publicKey, dataToSign, signature,
      algorithm: 'SHA-256/ECDSA');
}

Uint8List createUint8ListFromString(String s) {
  var ret = new Uint8List(s.length);
  for (var i = 0; i < s.length; i++) {
    ret[i] = s.codeUnitAt(i);
  }
  return ret;
}

// don't worry - it's a sample key
String loadEcPublicKeyPem() {
  return ('''-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMZHnt1D1tddLNGlEHXEtEw5K/G5Q
HkgaLi+IV84oiV+THv/DqGxYDX2F5JOkfyv36iYSf5lfIC7q9el4YLnlwA==
-----END PUBLIC KEY-----''');
}

// don't worry - it's a sample key
String loadEcPrivateKeyPemSec1() {
  return ('''-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIHIj7f4Lpc8O/13tdmDrv7y1ICFGfu4BqOVZJlNAfoFFoAoGCCqGSM49
AwEHoUQDQgAEMZHnt1D1tddLNGlEHXEtEw5K/G5QHkgaLi+IV84oiV+THv/DqGxY
DX2F5JOkfyv36iYSf5lfIC7q9el4YLnlwA==
-----END EC PRIVATE KEY-----''');
}

// don't worry - it's a sample key
String loadEcPrivateKeyPemPkcs8() {
  return ('''-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgciPt/gulzw7/Xe12
YOu/vLUgIUZ+7gGo5VkmU0B+gUWhRANCAAQxkee3UPW110s0aUQdcS0TDkr8blAe
SBouL4hXziiJX5Me/8OobFgNfYXkk6R/K/fqJhJ/mV8gLur16XhgueXA
-----END PRIVATE KEY-----''');
}
Ephenodrom commented 3 years ago

Hello and thank you for the issue! The PKCS8 seems to have another structure according to the RFC https://datatracker.ietf.org/doc/html/rfc5915 that describes SEC1.

ECPrivateKey ::= SEQUENCE {
     version        INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
     privateKey     OCTET STRING,
     parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
     publicKey  [1] BIT STRING OPTIONAL
}

Whereas RFC https://datatracker.ietf.org/doc/html/rfc5208 describes the PKCS8 as

PrivateKeyInfo ::= SEQUENCE {
        version                   Version,
        privateKeyAlgorithm       PrivateKeyAlgorithmIdentifier,
        privateKey                PrivateKey,
        attributes           [0]  IMPLICIT Attributes OPTIONAL }

I oriented myself on the structure that is used by openssl. I will update the parsing method to support both structures. I keep you updated.

Ephenodrom commented 3 years ago

This is done now with version 3.5.0. I took over your example and added your code with some improvements.