From 8a488bc731acf4724acc01c04e978e8fc8906a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Strna=CC=81dek?= Date: Wed, 30 Sep 2020 23:21:20 +0200 Subject: [PATCH 01/11] Init --- lib/pkcs7.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/pkcs7.js b/lib/pkcs7.js index 3a5d845c5..383c7648e 100644 --- a/lib/pkcs7.js +++ b/lib/pkcs7.js @@ -245,6 +245,7 @@ p7.createSignedData = function() { * (eg: forge.pki.oids.sha1). * [authenticatedAttributes] an optional array of attributes * to also sign along with the content. + * [unauthenticatedAttributes] an optional array of unauthenticated attributes */ addSigner: function(signer) { var issuer = signer.issuer; @@ -322,7 +323,7 @@ p7.createSignedData = function() { signatureAlgorithm: forge.pki.oids.rsaEncryption, signature: null, authenticatedAttributes: authenticatedAttributes, - unauthenticatedAttributes: [] + unauthenticatedAttributes: signer.unauthenticatedAttributes || [] }); }, @@ -999,7 +1000,7 @@ function _signerToAsn1(obj) { var attrsAsn1 = asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, []); for(var i = 0; i < obj.unauthenticatedAttributes.length; ++i) { var attr = obj.unauthenticatedAttributes[i]; - attrsAsn1.values.push(_attributeToAsn1(attr)); + attrsAsn1.value.push(_attributeToAsn1(attr)); } rval.value.push(attrsAsn1); } From 59a59abd160d22aa6aa51307c9d4c49946f5dce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Strna=CC=81dek?= Date: Wed, 7 Oct 2020 08:42:22 +0200 Subject: [PATCH 02/11] Add timestamptoken OID attribute to list --- lib/oids.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/oids.js b/lib/oids.js index 0ca96e989..0461ac4c7 100644 --- a/lib/oids.js +++ b/lib/oids.js @@ -71,6 +71,7 @@ _IN('1.2.840.113549.1.9.14', 'extensionRequest'); _IN('1.2.840.113549.1.9.20', 'friendlyName'); _IN('1.2.840.113549.1.9.21', 'localKeyId'); _IN('1.2.840.113549.1.9.22.1', 'x509Certificate'); +_IN('1.2.840.113549.1.9.16.2.14', 'timeStampToken'); // pkcs#12 safe bags _IN('1.2.840.113549.1.12.10.1.1', 'keyBag'); From aef5d2f9b860f478846df2fdecc317dbe02b4f16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Strn=C3=A1dek?= Date: Sun, 6 Feb 2022 18:24:48 +0100 Subject: [PATCH 03/11] Style fixes --- lib/oids.js | 2 +- lib/pkcs7.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/oids.js b/lib/oids.js index 0461ac4c7..e625cd7a1 100644 --- a/lib/oids.js +++ b/lib/oids.js @@ -68,10 +68,10 @@ _IN('1.2.840.113549.1.9.7', 'challengePassword'); _IN('1.2.840.113549.1.9.8', 'unstructuredAddress'); _IN('1.2.840.113549.1.9.14', 'extensionRequest'); +_IN('1.2.840.113549.1.9.16.2.14', 'timeStampToken'); _IN('1.2.840.113549.1.9.20', 'friendlyName'); _IN('1.2.840.113549.1.9.21', 'localKeyId'); _IN('1.2.840.113549.1.9.22.1', 'x509Certificate'); -_IN('1.2.840.113549.1.9.16.2.14', 'timeStampToken'); // pkcs#12 safe bags _IN('1.2.840.113549.1.12.10.1.1', 'keyBag'); diff --git a/lib/pkcs7.js b/lib/pkcs7.js index 383c7648e..b59406bbb 100644 --- a/lib/pkcs7.js +++ b/lib/pkcs7.js @@ -245,7 +245,8 @@ p7.createSignedData = function() { * (eg: forge.pki.oids.sha1). * [authenticatedAttributes] an optional array of attributes * to also sign along with the content. - * [unauthenticatedAttributes] an optional array of unauthenticated attributes + * [unauthenticatedAttributes] an optional array of unauthenticated + * attributes */ addSigner: function(signer) { var issuer = signer.issuer; From 5b0b04f9bab4ba54367bea310aa820a11f4c00e1 Mon Sep 17 00:00:00 2001 From: HAMANO Tsukasa Date: Mon, 15 Nov 2021 20:57:07 +0900 Subject: [PATCH 04/11] pkcs7 support mrtd signature data --- lib/oids.js | 1 + lib/pkcs7.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/oids.js b/lib/oids.js index e625cd7a1..6d769e04c 100644 --- a/lib/oids.js +++ b/lib/oids.js @@ -56,6 +56,7 @@ _IN('1.2.840.113549.1.7.3', 'envelopedData'); _IN('1.2.840.113549.1.7.4', 'signedAndEnvelopedData'); _IN('1.2.840.113549.1.7.5', 'digestedData'); _IN('1.2.840.113549.1.7.6', 'encryptedData'); +_IN('2.23.136.1.1.1', 'mrtdSignatureData'); // pkcs#9 oids _IN('1.2.840.113549.1.9.1', 'emailAddress'); diff --git a/lib/pkcs7.js b/lib/pkcs7.js index b59406bbb..4751120f3 100644 --- a/lib/pkcs7.js +++ b/lib/pkcs7.js @@ -1169,7 +1169,7 @@ function _fromAsn1(msg, obj, validator) { // Check contentType, so far we only support (raw) Data. var contentType = asn1.derToOid(capture.contentType); - if(contentType !== forge.pki.oids.data) { + if(![forge.pki.oids.data, forge.pki.oids.mrtdSignatureData].includes(contentType)) { throw new Error('Unsupported PKCS#7 message. ' + 'Only wrapped ContentType Data supported.'); } From 1d497b1b99b17f8564996c3cf4b2abdd3be3716d Mon Sep 17 00:00:00 2001 From: HAMANO Tsukasa Date: Tue, 16 Nov 2021 00:19:37 +0900 Subject: [PATCH 05/11] add ecdsa oid --- lib/oids.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/oids.js b/lib/oids.js index 6d769e04c..44fad6750 100644 --- a/lib/oids.js +++ b/lib/oids.js @@ -39,6 +39,18 @@ _IN('1.3.101.112', 'EdDSA25519'); _IN('1.2.840.10040.4.3', 'dsa-with-sha1'); +_IN('1.2.840.10045.2.1', 'ecPublicKey'); +_IN('1.2.840.10045.4.3.2', 'ecdsa-with-SHA256'); +_IN('1.2.840.10045.4.3.3', 'ecdsa-with-SHA384'); +_IN('1.2.840.10045.4.3.4', 'ecdsa-with-SHA512'); + +_IN('1.2.840.10045.3.1.1', 'p192'); +_IN('1.2.840.10045.3.1.7', 'p256'); +_IN('1.3.132.0.33', 'p224'); +_IN('1.3.132.0.34', 'p384'); +_IN('1.3.132.0.35', 'p521'); +_IN('1.3.132.0.10', 'secp256k1'); + _IN('1.3.14.3.2.7', 'desCBC'); _IN('1.3.14.3.2.26', 'sha1'); From f61866e687e018c348027942004ce8133cf666c3 Mon Sep 17 00:00:00 2001 From: HAMANO Tsukasa Date: Wed, 17 Nov 2021 08:23:43 +0900 Subject: [PATCH 06/11] update SubjectKeyInfo validator for ecdsa --- lib/asn1-validator.js | 38 +++++++++++++++-- lib/rsa.js | 96 ++++++++++++++++++------------------------- 2 files changed, 75 insertions(+), 59 deletions(-) diff --git a/lib/asn1-validator.js b/lib/asn1-validator.js index 2be328501..ecc8cc442 100644 --- a/lib/asn1-validator.js +++ b/lib/asn1-validator.js @@ -17,7 +17,6 @@ exports.privateKeyValidator = { name: 'PrivateKeyInfo.version', tagClass: asn1.Class.UNIVERSAL, type: asn1.Type.INTEGER, - constructed: false, capture: 'privateKeyVersion' }, { // privateKeyAlgorithm @@ -29,7 +28,6 @@ exports.privateKeyValidator = { name: 'AlgorithmIdentifier.algorithm', tagClass: asn1.Class.UNIVERSAL, type: asn1.Type.OID, - constructed: false, capture: 'privateKeyOid' }] }, { @@ -37,11 +35,45 @@ exports.privateKeyValidator = { name: 'PrivateKeyInfo', tagClass: asn1.Class.UNIVERSAL, type: asn1.Type.OCTETSTRING, - constructed: false, capture: 'privateKey' }] }; +exports.publicKeyInfoValidator = { + name: 'SubjectPublicKeyInfo', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.SEQUENCE, + constructed: true, + captureAsn1: 'subjectPublicKeyInfo', + value: [{ + name: 'SubjectPublicKeyInfo.AlgorithmIdentifier', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.SEQUENCE, + constructed: true, + value: [{ + name: 'AlgorithmIdentifier.algorithm', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.OID, + capture: 'publicKeyOid' + }, + { + name: 'AlgorithmIdentifier.parameters', + tagClass: asn1.Class.UNIVERSAL, + optional: true, + captureAsn1: 'parameters', + }] + }, { + // subjectPublicKey + name: 'SubjectPublicKeyInfo.subjectPublicKey', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.BITSTRING, + constructed: false, + captureAsn1: 'subjectPublicKey', + captureBitStringValue: 'subjectPublicKeyRaw' + }] +}; + +// obsolute: use publicKeyInfoValidator exports.publicKeyValidator = { name: 'SubjectPublicKeyInfo', tagClass: asn1.Class.UNIVERSAL, diff --git a/lib/rsa.js b/lib/rsa.js index 7c67917ce..7888fb8e9 100644 --- a/lib/rsa.js +++ b/lib/rsa.js @@ -70,6 +70,10 @@ require('./prime'); require('./random'); require('./util'); +var asn1Validator = require('./asn1-validator'); +var publicKeyInfoValidator = asn1Validator.publicKeyInfoValidator; +var ecdsa = require('./ecdsa'); + if(typeof BigInteger === 'undefined') { var BigInteger = forge.jsbn.BigInteger; } @@ -226,44 +230,6 @@ var rsaPublicKeyValidator = { }] }; -// validator for an SubjectPublicKeyInfo structure -// Note: Currently only works with an RSA public key -var publicKeyValidator = forge.pki.rsa.publicKeyValidator = { - name: 'SubjectPublicKeyInfo', - tagClass: asn1.Class.UNIVERSAL, - type: asn1.Type.SEQUENCE, - constructed: true, - captureAsn1: 'subjectPublicKeyInfo', - value: [{ - name: 'SubjectPublicKeyInfo.AlgorithmIdentifier', - tagClass: asn1.Class.UNIVERSAL, - type: asn1.Type.SEQUENCE, - constructed: true, - value: [{ - name: 'AlgorithmIdentifier.algorithm', - tagClass: asn1.Class.UNIVERSAL, - type: asn1.Type.OID, - constructed: false, - capture: 'publicKeyOid' - }] - }, { - // subjectPublicKey - name: 'SubjectPublicKeyInfo.subjectPublicKey', - tagClass: asn1.Class.UNIVERSAL, - type: asn1.Type.BITSTRING, - constructed: false, - value: [{ - // RSAPublicKey - name: 'SubjectPublicKeyInfo.subjectPublicKey.RSAPublicKey', - tagClass: asn1.Class.UNIVERSAL, - type: asn1.Type.SEQUENCE, - constructed: true, - optional: true, - captureAsn1: 'rsaPublicKey' - }] - }] -}; - /** * Wrap digest in DigestInfo object. * @@ -1366,36 +1332,29 @@ pki.privateKeyToAsn1 = pki.privateKeyToRSAPrivateKey = function(key) { }; /** - * Converts a public key from an ASN.1 SubjectPublicKeyInfo or RSAPublicKey. + * Converts a RSA public key from an ASN.1 SubjectPublicKeyInfo. * - * @param obj the asn1 representation of a SubjectPublicKeyInfo or RSAPublicKey. + * @param obj the asn1 representation of a SubjectPublicKeyInfo. * * @return the public key. */ -pki.publicKeyFromAsn1 = function(obj) { - // get SubjectPublicKeyInfo +pki.rsa.publicKeyFromAsn1 = function(obj) { + //var subjectPublicKey = obj.value[1].value[0]; var capture = {}; var errors = []; - if(asn1.validate(obj, publicKeyValidator, capture, errors)) { - // get oid - var oid = asn1.derToOid(capture.publicKeyOid); - if(oid !== pki.oids.rsaEncryption) { - var error = new Error('Cannot read public key. Unknown OID.'); - error.oid = oid; - throw error; - } - obj = capture.rsaPublicKey; + if(!asn1.validate(obj, publicKeyInfoValidator, capture, errors)) { + var error = new Error('Cannot read public key. ' + + 'ASN.1 object does not contain an PublicKeyInfo.'); + error.errors = errors; + throw error; } - - // get RSA params - errors = []; + obj = capture.subjectPublicKey.value[0]; if(!asn1.validate(obj, rsaPublicKeyValidator, capture, errors)) { var error = new Error('Cannot read public key. ' + - 'ASN.1 object does not contain an RSAPublicKey.'); + 'ASN.1 object does not contain an RSAPublicKey.'); error.errors = errors; throw error; } - // FIXME: inefficient, get a BigInteger that uses byte strings var n = forge.util.createBuffer(capture.publicKeyModulus).toHex(); var e = forge.util.createBuffer(capture.publicKeyExponent).toHex(); @@ -1404,6 +1363,31 @@ pki.publicKeyFromAsn1 = function(obj) { return pki.setRsaPublicKey( new BigInteger(n, 16), new BigInteger(e, 16)); +} + +/** + * Converts a public key from an ASN.1 SubjectPublicKeyInfo or RSAPublicKey. + * + * @param obj the asn1 representation of a SubjectPublicKeyInfo or RSAPublicKey. + * + * @return the public key. + */ +pki.publicKeyFromAsn1 = function(obj) { + // get SubjectPublicKeyInfo + var capture = {}; + var errors = []; + // get oid + var oid = asn1.derToOid(obj.value[0].value[0].value); + switch (oid) { + case pki.oids.rsaEncryption: + return pki.rsa.publicKeyFromAsn1(obj); + case pki.oids.ecPublicKey: + return ecdsa.ECPublicKey.fromAsn1(obj); + default: + var error = new Error('Cannot read public key. Unknown OID.'); + error.oid = oid; + throw error; + } }; /** From 685bc91280396b712048b0a1d57d5b30c1db63dc Mon Sep 17 00:00:00 2001 From: HAMANO Tsukasa Date: Thu, 18 Nov 2021 00:59:24 +0900 Subject: [PATCH 07/11] add ecdsa support --- lib/ecdsa.js | 388 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 388 insertions(+) create mode 100644 lib/ecdsa.js diff --git a/lib/ecdsa.js b/lib/ecdsa.js new file mode 100644 index 000000000..ba0bec1fc --- /dev/null +++ b/lib/ecdsa.js @@ -0,0 +1,388 @@ +/** + * JavaScript implementation of ECDSA. + * + * Copyright (c) 2021 HAMANO Tsukasa + * + * This implementation is based on the elliptic + * + * https://github.com/indutny/elliptic/ + */ +var forge = require('./forge'); +require('./asn1'); +require('./jsbn'); +require('./random'); +require('./sha512'); +var util = require('./util'); +var elliptic = require('elliptic'); +var asn1Validator = require('./asn1-validator'); +var publicKeyInfoValidator = asn1Validator.publicKeyInfoValidator; +var privateKeyValidator = asn1Validator.privateKeyValidator; +var asn1 = forge.asn1; + +if(typeof BigInteger === 'undefined') { + var BigInteger = forge.jsbn.BigInteger; +} + +var ByteBuffer = util.ByteBuffer; +var NativeBuffer = typeof Buffer === 'undefined' ? Uint8Array : Buffer; + +forge.pki = forge.pki || {}; +module.exports = forge.pki.ecdsa = forge.ecdsa = forge.ecdsa || {}; +var ecdsa = forge.ecdsa; + +ecdsa.constants = {}; + +/* + * Supported namedCurve listed here: + * https://github.com/indutny/elliptic/blob/master/lib/elliptic/curves.js + */ +ecdsa.supportedCueves = [ + 'p192', // secp192r1, prime192v1 + 'p256', // secp256r1, prime256v1 + 'p224', // secp224r1, + 'p384', // secp384r1 + 'p521', // secp521r1 + 'secp256k1',// secp256k1 +]; + +/* + * RCF5915: Elliptic Curve Private Key Format + * https://datatracker.ietf.org/doc/html/rfc5915 + * + * ECPrivateKey ::= SEQUENCE { + * version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1), + * privateKey OCTET STRING, + * parameters [0] ECParameters {{ NamedCurve }} OPTIONAL, + * publicKey [1] BIT STRING OPTIONAL + * } + */ +var ecPrivateKeyValidator = { + name: 'ECPrivateKey', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.SEQUENCE, + constructed: true, + value: [{ + name: 'ECPrivateKey.version', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.INTEGER, + capture: 'version', + }, { + name: 'ECPrivateKey.privateKey', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.OCTETSTRING, + capture: 'privateKey', + }, { + tagClass: asn1.Class.CONTEXT_SPECIFIC, + type: 0x0, + optional: true, + value: [{ + name: 'ECPrivateKey.parameters', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.OID, + captureAsn1: 'parameters', + }], + }, { + tagClass: asn1.Class.CONTEXT_SPECIFIC, + type: 0x1, + optional: true, + value: [{ + name: 'ECPrivateKey.publicKey', + type: asn1.Type.BITSTRING, + captureAsn1: 'publicKey', + }], + }] +}; + +var ecSpecifiedCurveValidator = { + name: 'SpecifiedCurve', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.SEQUENCE, + constructed: true, + value: [{ + name: 'SpecifiedCurveVersion', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.INTEGER, + capture: 'version', + }, { + name: 'SpecifiedCurve.FieldID', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.SEQUENCE, + constructed: true, + value: [{ + name: 'SpecifiedCurve.FieldID.fieldType', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.OID, + capture: 'fieldType', + }, { + name: 'SpecifiedCurve.FieldID.prime', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.INTEGER, + capture: 'p', + }] + }, { + name: 'SpecifiedCurve.Curve', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.SEQUENCE, + constructed: true, + value: [{ + name: 'SpecifiedCurve.Curve.a', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.OCTETSTRING, + capture: 'a', + }, { + name: 'SpecifiedCurve.Curve.b', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.OCTETSTRING, + capture: 'b', + }] + }, { + name: 'SpecifiedCurve.Generator', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.OCTETSTRING, + capture: 'g', + }, { + name: 'SpecifiedCurve.Order', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.INTEGER, + capture: 'n', + }, { + name: 'SpecifiedCurve.Confactor', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.INTEGER, + capture: 'c', + optional: true + }] +}; + +ecdsa.generateKeyPair = function(options) { + options = options || {}; + var curveName = options.name || 'p256'; + var seed = options.seed; + var errors = []; + + if (!(ecdsa.supportedCueves.includes(curveName))) { + var error = new Error('unsupported curveName: ' + curveName); + error.errors = errors; + throw error; + } + var curve = elliptic.curves[curveName]; + var ec = new elliptic.ec(curve); + ec.curveName = curveName; + var kp = ec.genKeyPair({ + entropy: seed, + }); + var privateKey = kp.getPrivate(); + var publicKey = kp.getPublic(); + return { + publicKey: new ecdsa.ECPublicKey(kp.ec, publicKey), + privateKey: new ecdsa.ECPrivateKey(kp.ec, privateKey) + }; +}; + +/** + * Converts a ECPrivateKey to an ASN.1 representation. + * + * @param key the ECPrivateKey. + * + * @return the ASN.1 representation of an ECPrivateKey. + */ +ecdsa.privateKeyToAsn1 = function(key, options) { + return key.toAsn1(options); +}; + +ecdsa.ECPublicKey = ECPublicKey = function(ec, publicKey) { + this._ec = ec; + this._publicKey = publicKey; +}; + +/** + * Converts a public key from a RFC8410 ASN.1 encoding. + * + * @param obj - The asn1 representation of a public key. + * + * @return {ECPublicKey} - ECPublicKey object. + */ +ECPublicKey.fromAsn1 = function(obj) { + var capture = {}; + var errors = []; + if(!forge.asn1.validate(obj, publicKeyInfoValidator, capture, errors)) { + var error = new Error('Cannot read PublicKeyInfo ASN.1 object.'); + error.errors = errors; + throw error; + } + + var publicKey = capture.subjectPublicKeyRaw; + var params = capture.parameters; + var curve; + var curveName; + if(params && params.type === forge.asn1.Type.OID) { + var oid = forge.asn1.derToOid(params.value); + curveName = forge.oids[oid]; + if(!ecdsa.supportedCueves.includes(curveName)) { + var error = new Error('Unsupported curveName: ' + curveName); + error.errors = errors; + throw error; + } + curve = elliptic.curves[curveName]; + } else if(params && params.type === forge.asn1.Type.SEQUENCE) { + var capture = {}; + if(!forge.asn1.validate(params, ecSpecifiedCurveValidator, capture, errors)) { + var error = new Error('Cannot read specified curve ASN.1 object.'); + error.errors = errors; + throw error; + } + var options = { + p: util.bytesToHex(capture.p), + a: util.bytesToHex(capture.a), + b: util.bytesToHex(capture.b), + n: util.bytesToHex(capture.n), + }; + var _curve = new elliptic.curve.short(options); + var g = _curve.decodePoint(util.bytesToHex(capture.g), 'hex'); + curve = { + curve: _curve, + n: _curve.n, + g: g + }; + } else { + var error = new Error('no ECParameters'); + error.errors = errors; + throw error; + } + var ec = new elliptic.ec({curve: curve}); + ec.curveName = curveName; + var kp = ec.keyFromPublic(publicKey); + return new ECPublicKey(ec, kp.getPublic()); +}; + +ECPublicKey.prototype.verify = function(msg, signature) { + var hexMsg = util.bytesToHex(msg); + var hexSignature = util.bytesToHex(signature); + return this._ec.verify(hexMsg, hexSignature, this._publicKey, 'hex'); +}; + +ECPublicKey.prototype.toString = function() { + return this._publicKey.encode('hex'); +}; + +ECPublicKey.prototype.getBytes = function() { + return String.fromCharCode.apply(null, this._publicKey.encode()); +}; + +ECPublicKey.prototype.toAsn1 = function(options) { + var curveOID = forge.oids[this._ec.curveName]; + if (!curveOID) { + var error = new Error('unsupported namedCurve or specifiedCurve.'); + error.errors = errors; + throw error; + } + + var obj = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, []); + var aid = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, + asn1.oidToDer(forge.oids['ecPublicKey']).getBytes()), + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, + asn1.oidToDer(curveOID).getBytes())]); + obj.value.push(aid); + obj.value.push( + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.BITSTRING, false, + "\0" + this.getBytes())); + return obj; +}; + +ECPublicKey.prototype.toDer = function() { + return asn1.toDer(this.toAsn1()).getBytes(); +}; + +ECPublicKey.prototype.toPem = function() { + return '-----BEGIN PUBLIC KEY-----\n' + + util.encode64(this.toDer(), 64) + + '\n-----END PUBLIC KEY-----\n'; +}; + + +ecdsa.ECPrivateKey = ECPrivateKey = function(ec, privateKey) { + this._ec = ec; + this._privateKey = privateKey; +}; + +/** + * Converts a private key from a RFC5915 ASN.1 Object. + * + * @param obj - The asn1 representation of a private key. + * + * @returns {Object} obj - The ASN.1 key object. + * @returns {ECPrivateKey} ECPrivateKey object. + */ +ECPrivateKey.fromAsn1 = function(obj) { + var capture = {}; + var errors = []; + var valid = forge.asn1.validate(obj, ecPrivateKeyValidator, capture, errors); + if(!valid) { + var error = new Error('Invalid ECPrivateKey object.'); + error.errors = errors; + throw error; + } + var params; + if (!capture.parameters) { + var error = new Error('no ECPrivateKey.parameters.'); + error.errors = errors; + throw error; + } + var oid = asn1.derToOid(capture.parameters.value) + var curveName = forge.oids[oid]; + if (!ecdsa.supportedCueves.includes(curveName)) { + var error = new Error('unsupported curveName: ' + curveName); + error.errors = errors; + throw error; + } + curve = elliptic.curves[curveName]; + var ec = new elliptic.ec({curve: curve}); + ec.curveName = curveName; + var kp = ec.keyFromPrivate(util.bytesToHex(capture.privateKey)); + return new ECPrivateKey(ec, kp.getPrivate()); +}; + +ECPrivateKey.prototype.sign = function(msg) { + var hexMsg = util.bytesToHex(msg); + var signature = this._ec.sign(hexMsg, this._privateKey); + return String.fromCharCode.apply(null, signature.toDER()); +}; + +ECPrivateKey.prototype.toString = function() { + return this._privateKey.toString('hex'); +}; + +ECPrivateKey.prototype.getBytes = function() { + return String.fromCharCode.apply(null, this._privateKey.toArray()); +}; + +ECPrivateKey.prototype.toAsn1 = function(options) { + var curveOID = forge.oids[this._ec.curveName]; + if (!curveOID) { + var error = new Error('unsupported namedCurve'); + error.errors = errors; + throw error; + } + return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ + asn1.create(asn1.Class.UNIVERSAL, + asn1.Type.INTEGER, false, + asn1.integerToDer(1).getBytes()), + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, + this.getBytes()), + asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0x0, true, [ + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, + asn1.oidToDer(curveOID).getBytes()) + ]), + ]); +}; + +ECPrivateKey.prototype.toDer = function(options) { + return asn1.toDer(this.toAsn1(options)).getBytes(); +}; + +ECPrivateKey.prototype.toPem = function(options) { + return '-----BEGIN EC PRIVATE KEY-----\n' + + util.encode64(this.toDer(options), 64) + + '\n-----END EC PRIVATE KEY-----\n'; +}; From 1adcfadf8706758f58ae78381a8763cece708adb Mon Sep 17 00:00:00 2001 From: HAMANO Tsukasa Date: Sat, 20 Nov 2021 23:16:34 +0900 Subject: [PATCH 08/11] support ecdca certificate validation --- lib/x509.js | 48 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/lib/x509.js b/lib/x509.js index 2877810c1..fdee54fd7 100644 --- a/lib/x509.js +++ b/lib/x509.js @@ -144,8 +144,8 @@ _shortNames['E'] = oids['emailAddress']; _shortNames['emailAddress'] = 'E'; // validator for an SubjectPublicKeyInfo structure -// Note: Currently only works with an RSA public key -var publicKeyValidator = forge.pki.rsa.publicKeyValidator; +var asn1Validator = require('./asn1-validator'); +var publicKeyInfoValidator = asn1Validator.publicKeyInfoValidator; // validator for an X.509v3 certificate var x509CertificateValidator = { @@ -252,7 +252,7 @@ var x509CertificateValidator = { captureAsn1: 'certSubject' }, // SubjectPublicKeyInfo - publicKeyValidator, + publicKeyInfoValidator, { // issuerUniqueID (optional) name: 'Certificate.TBSCertificate.issuerUniqueID', @@ -427,7 +427,7 @@ var certificationRequestInfoValidator = { captureAsn1: 'certificationRequestInfoSubject' }, // SubjectPublicKeyInfo - publicKeyValidator, + publicKeyInfoValidator, { name: 'CertificationRequestInfo.attributes', tagClass: asn1.Class.CONTEXT_SPECIFIC, @@ -1171,11 +1171,37 @@ pki.createCertificate = function() { var md = child.md; if(md === null) { - // create digest for OID signature types - md = _createSignatureDigest({ - signatureOid: child.signatureOid, - type: 'certificate' - }); + // check signature OID for supported signature types + if(child.signatureOid in oids) { + var oid = oids[child.signatureOid]; + switch(oid) { + case 'sha1WithRSAEncryption': + md = forge.md.sha1.create(); + break; + case 'md5WithRSAEncryption': + md = forge.md.md5.create(); + break; + case 'RSASSA-PSS': + case 'sha256WithRSAEncryption': + case 'ecdsa-with-SHA256': + md = forge.md.sha256.create(); + break; + case 'sha384WithRSAEncryption': + case 'ecdsa-with-SHA384': + md = forge.md.sha384.create(); + break; + case 'sha512WithRSAEncryption': + case 'ecdsa-with-SHA512': + md = forge.md.sha512.create(); + break; + } + } + if(md === null) { + var error = new Error('Could not compute certificate digest. ' + + 'Unknown signature OID.'); + error.signatureOid = child.signatureOid; + throw error; + } // produce DER formatted TBSCertificate and digest it var tbsCertificate = child.tbsCertificate || pki.getTBSCertificate(child); @@ -1312,8 +1338,8 @@ pki.certificateFromAsn1 = function(obj, computeHash) { // get oid var oid = asn1.derToOid(capture.publicKeyOid); - if(oid !== pki.oids.rsaEncryption) { - throw new Error('Cannot read public key. OID is not RSA.'); + if(![pki.oids.rsaEncryption, pki.oids.ecPublicKey].includes(oid)) { + throw new Error('Cannot read public key. publicKeyOid: ' + oid); } // create certificate From 1e34db7f6ce904de1578339b879e80ffc0966d9b Mon Sep 17 00:00:00 2001 From: HAMANO Tsukasa Date: Sun, 21 Nov 2021 22:05:15 +0900 Subject: [PATCH 09/11] add test for ECDSA --- tests/unit/ecdsa.js | 70 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 tests/unit/ecdsa.js diff --git a/tests/unit/ecdsa.js b/tests/unit/ecdsa.js new file mode 100644 index 000000000..dfa41ce95 --- /dev/null +++ b/tests/unit/ecdsa.js @@ -0,0 +1,70 @@ +var ASSERT = require('assert'); +var ECDSA = require('../../lib/ecdsa'); +var SHA256 = require('../../lib/sha256'); +var PKI = require('../../lib/pki'); +var UTIL = require('../../lib/util'); +var ASN1 = require('../../lib/asn1'); + +(function() { + describe('ECDSA', function() { + var testCurveNames = [ + 'p192', + 'p224', + 'p256', + 'p384', + 'p521', + 'secp256k1', + ] + + function test(curveName) { + var kp; + + beforeEach(function() { + // generate key pair from a seed + var pwd = 'password'; + var md = SHA256.create(); + md.update(pwd, 'utf8'); + var seed = md.digest().getBytes(); + kp = ECDSA.generateKeyPair({name: curveName, seed: seed}); + }); + + it(curveName + ': should sign and veriy', function() { + var msg = 'hello' + var signature = kp.privateKey.sign(msg); + ASSERT.ok(kp.publicKey.verify(msg, signature)); + ASSERT.ok(!kp.publicKey.verify('wrong', signature)); + }); + + it(curveName + ': should encode/decode privateKey to Der', function() { + var encodedPrivateKey = eb64(kp.privateKey.toDer()); + var decodedPrivateKey = ECPrivateKey.fromAsn1(ASN1.fromDer(db64(encodedPrivateKey))); + ASSERT.equal(encodedPrivateKey, eb64(decodedPrivateKey.toDer())); + }); + + it(curveName + ': should encode/decode publicKey to Der', function() { + var encodedPublicKey = eb64(kp.publicKey.toDer()); + var decodedPublicKey = ECPublicKey.fromAsn1(ASN1.fromDer(db64(encodedPublicKey))); + ASSERT.equal(encodedPublicKey, eb64(decodedPublicKey.toDer())); + }); + + it(curveName + ': should generate a random key pair', function() { + var kp = ECDSA.generateKeyPair({name: curveName}); + ASSERT.ok(kp.privateKey); + ASSERT.ok(kp.publicKey); + }); + } + + for (var curveName of testCurveNames) { + test(curveName); + } + + }); + + function eb64(buffer) { + return UTIL.encode64(new UTIL.ByteBuffer(buffer).bytes()); + } + + function db64(x) { + return new UTIL.ByteBuffer(UTIL.decode64(x), 'binary'); + } +})(); From dda3d17386c5de0cfddcfd838dceae53f797e16b Mon Sep 17 00:00:00 2001 From: HAMANO Tsukasa Date: Mon, 22 Nov 2021 11:59:25 +0900 Subject: [PATCH 10/11] add elliptic --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index ab7771410..52eeeb4d2 100644 --- a/package.json +++ b/package.json @@ -119,5 +119,8 @@ "buffer": false, "crypto": false, "process": false + }, + "dependencies": { + "elliptic": "^6.5.4" } } From 56bd38dc2709ec86529b45c7cee7ef13887b3f27 Mon Sep 17 00:00:00 2001 From: HAMANO Tsukasa Date: Fri, 26 Nov 2021 14:36:06 +0900 Subject: [PATCH 11/11] use same PublicKeyInfoValidator for ED25519 --- lib/asn1-validator.js | 53 ++----------------------------------------- lib/ecdsa.js | 1 - lib/ed25519.js | 4 ++-- 3 files changed, 4 insertions(+), 54 deletions(-) diff --git a/lib/asn1-validator.js b/lib/asn1-validator.js index ecc8cc442..4deb9f17e 100644 --- a/lib/asn1-validator.js +++ b/lib/asn1-validator.js @@ -68,56 +68,7 @@ exports.publicKeyInfoValidator = { tagClass: asn1.Class.UNIVERSAL, type: asn1.Type.BITSTRING, constructed: false, - captureAsn1: 'subjectPublicKey', - captureBitStringValue: 'subjectPublicKeyRaw' + captureAsn1: 'subjectPublicKey', // used by RSA + captureBitStringValue: 'subjectPublicKeyRaw', // used by ECDSA and ED25519 }] }; - -// obsolute: use publicKeyInfoValidator -exports.publicKeyValidator = { - name: 'SubjectPublicKeyInfo', - tagClass: asn1.Class.UNIVERSAL, - type: asn1.Type.SEQUENCE, - constructed: true, - captureAsn1: 'subjectPublicKeyInfo', - value: [{ - name: 'SubjectPublicKeyInfo.AlgorithmIdentifier', - tagClass: asn1.Class.UNIVERSAL, - type: asn1.Type.SEQUENCE, - constructed: true, - value: [{ - name: 'AlgorithmIdentifier.algorithm', - tagClass: asn1.Class.UNIVERSAL, - type: asn1.Type.OID, - constructed: false, - capture: 'publicKeyOid' - }] - }, - // capture group for ed25519PublicKey - { - tagClass: asn1.Class.UNIVERSAL, - type: asn1.Type.BITSTRING, - constructed: false, - composed: true, - captureBitStringValue: 'ed25519PublicKey' - } - // FIXME: this is capture group for rsaPublicKey, use it in this API or - // discard? - /* { - // subjectPublicKey - name: 'SubjectPublicKeyInfo.subjectPublicKey', - tagClass: asn1.Class.UNIVERSAL, - type: asn1.Type.BITSTRING, - constructed: false, - value: [{ - // RSAPublicKey - name: 'SubjectPublicKeyInfo.subjectPublicKey.RSAPublicKey', - tagClass: asn1.Class.UNIVERSAL, - type: asn1.Type.SEQUENCE, - constructed: true, - optional: true, - captureAsn1: 'rsaPublicKey' - }] - } */ - ] -}; diff --git a/lib/ecdsa.js b/lib/ecdsa.js index ba0bec1fc..ee4373416 100644 --- a/lib/ecdsa.js +++ b/lib/ecdsa.js @@ -210,7 +210,6 @@ ECPublicKey.fromAsn1 = function(obj) { error.errors = errors; throw error; } - var publicKey = capture.subjectPublicKeyRaw; var params = capture.parameters; var curve; diff --git a/lib/ed25519.js b/lib/ed25519.js index f3e6faaaa..8cfe62f09 100644 --- a/lib/ed25519.js +++ b/lib/ed25519.js @@ -114,7 +114,7 @@ ed25519.publicKeyFromAsn1 = function(obj) { // get SubjectPublicKeyInfo var capture = {}; var errors = []; - var valid = forge.asn1.validate(obj, publicKeyValidator, capture, errors); + var valid = forge.asn1.validate(obj, publicKeyInfoValidator, capture, errors); if(!valid) { var error = new Error('Invalid Key.'); error.errors = errors; @@ -126,7 +126,7 @@ ed25519.publicKeyFromAsn1 = function(obj) { throw new Error('Invalid OID "' + oid + '"; OID must be "' + ed25519Oid + '".'); } - var publicKeyBytes = capture.ed25519PublicKey; + var publicKeyBytes = capture.ed25519PublicKeyRaw; if(publicKeyBytes.length !== ed25519.constants.PUBLIC_KEY_BYTE_LENGTH) { throw new Error('Key length is invalid.'); }