| /** |
| * Javascript implementation of X.509 and related components (such as |
| * Certification Signing Requests) of a Public Key Infrastructure. |
| * |
| * @author Dave Longley |
| * |
| * Copyright (c) 2010-2014 Digital Bazaar, Inc. |
| * |
| * The ASN.1 representation of an X.509v3 certificate is as follows |
| * (see RFC 2459): |
| * |
| * Certificate ::= SEQUENCE { |
| * tbsCertificate TBSCertificate, |
| * signatureAlgorithm AlgorithmIdentifier, |
| * signatureValue BIT STRING |
| * } |
| * |
| * TBSCertificate ::= SEQUENCE { |
| * version [0] EXPLICIT Version DEFAULT v1, |
| * serialNumber CertificateSerialNumber, |
| * signature AlgorithmIdentifier, |
| * issuer Name, |
| * validity Validity, |
| * subject Name, |
| * subjectPublicKeyInfo SubjectPublicKeyInfo, |
| * issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL, |
| * -- If present, version shall be v2 or v3 |
| * subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL, |
| * -- If present, version shall be v2 or v3 |
| * extensions [3] EXPLICIT Extensions OPTIONAL |
| * -- If present, version shall be v3 |
| * } |
| * |
| * Version ::= INTEGER { v1(0), v2(1), v3(2) } |
| * |
| * CertificateSerialNumber ::= INTEGER |
| * |
| * Name ::= CHOICE { |
| * // only one possible choice for now |
| * RDNSequence |
| * } |
| * |
| * RDNSequence ::= SEQUENCE OF RelativeDistinguishedName |
| * |
| * RelativeDistinguishedName ::= SET OF AttributeTypeAndValue |
| * |
| * AttributeTypeAndValue ::= SEQUENCE { |
| * type AttributeType, |
| * value AttributeValue |
| * } |
| * AttributeType ::= OBJECT IDENTIFIER |
| * AttributeValue ::= ANY DEFINED BY AttributeType |
| * |
| * Validity ::= SEQUENCE { |
| * notBefore Time, |
| * notAfter Time |
| * } |
| * |
| * Time ::= CHOICE { |
| * utcTime UTCTime, |
| * generalTime GeneralizedTime |
| * } |
| * |
| * UniqueIdentifier ::= BIT STRING |
| * |
| * SubjectPublicKeyInfo ::= SEQUENCE { |
| * algorithm AlgorithmIdentifier, |
| * subjectPublicKey BIT STRING |
| * } |
| * |
| * Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension |
| * |
| * Extension ::= SEQUENCE { |
| * extnID OBJECT IDENTIFIER, |
| * critical BOOLEAN DEFAULT FALSE, |
| * extnValue OCTET STRING |
| * } |
| * |
| * The only key algorithm currently supported for PKI is RSA. |
| * |
| * RSASSA-PSS signatures are described in RFC 3447 and RFC 4055. |
| * |
| * PKCS#10 v1.7 describes certificate signing requests: |
| * |
| * CertificationRequestInfo: |
| * |
| * CertificationRequestInfo ::= SEQUENCE { |
| * version INTEGER { v1(0) } (v1,...), |
| * subject Name, |
| * subjectPKInfo SubjectPublicKeyInfo{{ PKInfoAlgorithms }}, |
| * attributes [0] Attributes{{ CRIAttributes }} |
| * } |
| * |
| * Attributes { ATTRIBUTE:IOSet } ::= SET OF Attribute{{ IOSet }} |
| * |
| * CRIAttributes ATTRIBUTE ::= { |
| * ... -- add any locally defined attributes here -- } |
| * |
| * Attribute { ATTRIBUTE:IOSet } ::= SEQUENCE { |
| * type ATTRIBUTE.&id({IOSet}), |
| * values SET SIZE(1..MAX) OF ATTRIBUTE.&Type({IOSet}{@type}) |
| * } |
| * |
| * CertificationRequest ::= SEQUENCE { |
| * certificationRequestInfo CertificationRequestInfo, |
| * signatureAlgorithm AlgorithmIdentifier{{ SignatureAlgorithms }}, |
| * signature BIT STRING |
| * } |
| */ |
| (function() { |
| /* ########## Begin module implementation ########## */ |
| function initModule(forge) { |
| |
| // shortcut for asn.1 API |
| var asn1 = forge.asn1; |
| |
| /* Public Key Infrastructure (PKI) implementation. */ |
| var pki = forge.pki = forge.pki || {}; |
| var oids = pki.oids; |
| |
| // short name OID mappings |
| var _shortNames = {}; |
| _shortNames['CN'] = oids['commonName']; |
| _shortNames['commonName'] = 'CN'; |
| _shortNames['C'] = oids['countryName']; |
| _shortNames['countryName'] = 'C'; |
| _shortNames['L'] = oids['localityName']; |
| _shortNames['localityName'] = 'L'; |
| _shortNames['ST'] = oids['stateOrProvinceName']; |
| _shortNames['stateOrProvinceName'] = 'ST'; |
| _shortNames['O'] = oids['organizationName']; |
| _shortNames['organizationName'] = 'O'; |
| _shortNames['OU'] = oids['organizationalUnitName']; |
| _shortNames['organizationalUnitName'] = 'OU'; |
| _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; |
| |
| // validator for an X.509v3 certificate |
| var x509CertificateValidator = { |
| name: 'Certificate', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.SEQUENCE, |
| constructed: true, |
| value: [{ |
| name: 'Certificate.TBSCertificate', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.SEQUENCE, |
| constructed: true, |
| captureAsn1: 'tbsCertificate', |
| value: [{ |
| name: 'Certificate.TBSCertificate.version', |
| tagClass: asn1.Class.CONTEXT_SPECIFIC, |
| type: 0, |
| constructed: true, |
| optional: true, |
| value: [{ |
| name: 'Certificate.TBSCertificate.version.integer', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.INTEGER, |
| constructed: false, |
| capture: 'certVersion' |
| }] |
| }, { |
| name: 'Certificate.TBSCertificate.serialNumber', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.INTEGER, |
| constructed: false, |
| capture: 'certSerialNumber' |
| }, { |
| name: 'Certificate.TBSCertificate.signature', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.SEQUENCE, |
| constructed: true, |
| value: [{ |
| name: 'Certificate.TBSCertificate.signature.algorithm', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.OID, |
| constructed: false, |
| capture: 'certinfoSignatureOid' |
| }, { |
| name: 'Certificate.TBSCertificate.signature.parameters', |
| tagClass: asn1.Class.UNIVERSAL, |
| optional: true, |
| captureAsn1: 'certinfoSignatureParams' |
| }] |
| }, { |
| name: 'Certificate.TBSCertificate.issuer', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.SEQUENCE, |
| constructed: true, |
| captureAsn1: 'certIssuer' |
| }, { |
| name: 'Certificate.TBSCertificate.validity', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.SEQUENCE, |
| constructed: true, |
| // Note: UTC and generalized times may both appear so the capture |
| // names are based on their detected order, the names used below |
| // are only for the common case, which validity time really means |
| // "notBefore" and which means "notAfter" will be determined by order |
| value: [{ |
| // notBefore (Time) (UTC time case) |
| name: 'Certificate.TBSCertificate.validity.notBefore (utc)', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.UTCTIME, |
| constructed: false, |
| optional: true, |
| capture: 'certValidity1UTCTime' |
| }, { |
| // notBefore (Time) (generalized time case) |
| name: 'Certificate.TBSCertificate.validity.notBefore (generalized)', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.GENERALIZEDTIME, |
| constructed: false, |
| optional: true, |
| capture: 'certValidity2GeneralizedTime' |
| }, { |
| // notAfter (Time) (only UTC time is supported) |
| name: 'Certificate.TBSCertificate.validity.notAfter (utc)', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.UTCTIME, |
| constructed: false, |
| optional: true, |
| capture: 'certValidity3UTCTime' |
| }, { |
| // notAfter (Time) (only UTC time is supported) |
| name: 'Certificate.TBSCertificate.validity.notAfter (generalized)', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.GENERALIZEDTIME, |
| constructed: false, |
| optional: true, |
| capture: 'certValidity4GeneralizedTime' |
| }] |
| }, { |
| // Name (subject) (RDNSequence) |
| name: 'Certificate.TBSCertificate.subject', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.SEQUENCE, |
| constructed: true, |
| captureAsn1: 'certSubject' |
| }, |
| // SubjectPublicKeyInfo |
| publicKeyValidator, |
| { |
| // issuerUniqueID (optional) |
| name: 'Certificate.TBSCertificate.issuerUniqueID', |
| tagClass: asn1.Class.CONTEXT_SPECIFIC, |
| type: 1, |
| constructed: true, |
| optional: true, |
| value: [{ |
| name: 'Certificate.TBSCertificate.issuerUniqueID.id', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.BITSTRING, |
| constructed: false, |
| capture: 'certIssuerUniqueId' |
| }] |
| }, { |
| // subjectUniqueID (optional) |
| name: 'Certificate.TBSCertificate.subjectUniqueID', |
| tagClass: asn1.Class.CONTEXT_SPECIFIC, |
| type: 2, |
| constructed: true, |
| optional: true, |
| value: [{ |
| name: 'Certificate.TBSCertificate.subjectUniqueID.id', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.BITSTRING, |
| constructed: false, |
| capture: 'certSubjectUniqueId' |
| }] |
| }, { |
| // Extensions (optional) |
| name: 'Certificate.TBSCertificate.extensions', |
| tagClass: asn1.Class.CONTEXT_SPECIFIC, |
| type: 3, |
| constructed: true, |
| captureAsn1: 'certExtensions', |
| optional: true |
| }] |
| }, { |
| // AlgorithmIdentifier (signature algorithm) |
| name: 'Certificate.signatureAlgorithm', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.SEQUENCE, |
| constructed: true, |
| value: [{ |
| // algorithm |
| name: 'Certificate.signatureAlgorithm.algorithm', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.OID, |
| constructed: false, |
| capture: 'certSignatureOid' |
| }, { |
| name: 'Certificate.TBSCertificate.signature.parameters', |
| tagClass: asn1.Class.UNIVERSAL, |
| optional: true, |
| captureAsn1: 'certSignatureParams' |
| }] |
| }, { |
| // SignatureValue |
| name: 'Certificate.signatureValue', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.BITSTRING, |
| constructed: false, |
| capture: 'certSignature' |
| }] |
| }; |
| |
| var rsassaPssParameterValidator = { |
| name: 'rsapss', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.SEQUENCE, |
| constructed: true, |
| value: [{ |
| name: 'rsapss.hashAlgorithm', |
| tagClass: asn1.Class.CONTEXT_SPECIFIC, |
| type: 0, |
| constructed: true, |
| value: [{ |
| name: 'rsapss.hashAlgorithm.AlgorithmIdentifier', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Class.SEQUENCE, |
| constructed: true, |
| optional: true, |
| value: [{ |
| name: 'rsapss.hashAlgorithm.AlgorithmIdentifier.algorithm', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.OID, |
| constructed: false, |
| capture: 'hashOid' |
| /* parameter block omitted, for SHA1 NULL anyhow. */ |
| }] |
| }] |
| }, { |
| name: 'rsapss.maskGenAlgorithm', |
| tagClass: asn1.Class.CONTEXT_SPECIFIC, |
| type: 1, |
| constructed: true, |
| value: [{ |
| name: 'rsapss.maskGenAlgorithm.AlgorithmIdentifier', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Class.SEQUENCE, |
| constructed: true, |
| optional: true, |
| value: [{ |
| name: 'rsapss.maskGenAlgorithm.AlgorithmIdentifier.algorithm', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.OID, |
| constructed: false, |
| capture: 'maskGenOid' |
| }, { |
| name: 'rsapss.maskGenAlgorithm.AlgorithmIdentifier.params', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.SEQUENCE, |
| constructed: true, |
| value: [{ |
| name: 'rsapss.maskGenAlgorithm.AlgorithmIdentifier.params.algorithm', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.OID, |
| constructed: false, |
| capture: 'maskGenHashOid' |
| /* parameter block omitted, for SHA1 NULL anyhow. */ |
| }] |
| }] |
| }] |
| }, { |
| name: 'rsapss.saltLength', |
| tagClass: asn1.Class.CONTEXT_SPECIFIC, |
| type: 2, |
| optional: true, |
| value: [{ |
| name: 'rsapss.saltLength.saltLength', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Class.INTEGER, |
| constructed: false, |
| capture: 'saltLength' |
| }] |
| }, { |
| name: 'rsapss.trailerField', |
| tagClass: asn1.Class.CONTEXT_SPECIFIC, |
| type: 3, |
| optional: true, |
| value: [{ |
| name: 'rsapss.trailer.trailer', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Class.INTEGER, |
| constructed: false, |
| capture: 'trailer' |
| }] |
| }] |
| }; |
| |
| // validator for a CertificationRequestInfo structure |
| var certificationRequestInfoValidator = { |
| name: 'CertificationRequestInfo', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.SEQUENCE, |
| constructed: true, |
| captureAsn1: 'certificationRequestInfo', |
| value: [{ |
| name: 'CertificationRequestInfo.integer', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.INTEGER, |
| constructed: false, |
| capture: 'certificationRequestInfoVersion' |
| }, { |
| // Name (subject) (RDNSequence) |
| name: 'CertificationRequestInfo.subject', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.SEQUENCE, |
| constructed: true, |
| captureAsn1: 'certificationRequestInfoSubject' |
| }, |
| // SubjectPublicKeyInfo |
| publicKeyValidator, |
| { |
| name: 'CertificationRequestInfo.attributes', |
| tagClass: asn1.Class.CONTEXT_SPECIFIC, |
| type: 0, |
| constructed: true, |
| optional: true, |
| capture: 'certificationRequestInfoAttributes', |
| value: [{ |
| name: 'CertificationRequestInfo.attributes', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.SEQUENCE, |
| constructed: true, |
| value: [{ |
| name: 'CertificationRequestInfo.attributes.type', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.OID, |
| constructed: false |
| }, { |
| name: 'CertificationRequestInfo.attributes.value', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.SET, |
| constructed: true |
| }] |
| }] |
| }] |
| }; |
| |
| // validator for a CertificationRequest structure |
| var certificationRequestValidator = { |
| name: 'CertificationRequest', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.SEQUENCE, |
| constructed: true, |
| captureAsn1: 'csr', |
| value: [ |
| certificationRequestInfoValidator, { |
| // AlgorithmIdentifier (signature algorithm) |
| name: 'CertificationRequest.signatureAlgorithm', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.SEQUENCE, |
| constructed: true, |
| value: [{ |
| // algorithm |
| name: 'CertificationRequest.signatureAlgorithm.algorithm', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.OID, |
| constructed: false, |
| capture: 'csrSignatureOid' |
| }, { |
| name: 'CertificationRequest.signatureAlgorithm.parameters', |
| tagClass: asn1.Class.UNIVERSAL, |
| optional: true, |
| captureAsn1: 'csrSignatureParams' |
| }] |
| }, { |
| // signature |
| name: 'CertificationRequest.signature', |
| tagClass: asn1.Class.UNIVERSAL, |
| type: asn1.Type.BITSTRING, |
| constructed: false, |
| capture: 'csrSignature' |
| }] |
| }; |
| |
| /** |
| * Converts an RDNSequence of ASN.1 DER-encoded RelativeDistinguishedName |
| * sets into an array with objects that have type and value properties. |
| * |
| * @param rdn the RDNSequence to convert. |
| * @param md a message digest to append type and value to if provided. |
| */ |
| pki.RDNAttributesAsArray = function(rdn, md) { |
| var rval = []; |
| |
| // each value in 'rdn' in is a SET of RelativeDistinguishedName |
| var set, attr, obj; |
| for(var si = 0; si < rdn.value.length; ++si) { |
| // get the RelativeDistinguishedName set |
| set = rdn.value[si]; |
| |
| // each value in the SET is an AttributeTypeAndValue sequence |
| // containing first a type (an OID) and second a value (defined by |
| // the OID) |
| for(var i = 0; i < set.value.length; ++i) { |
| obj = {}; |
| attr = set.value[i]; |
| obj.type = asn1.derToOid(attr.value[0].value); |
| obj.value = attr.value[1].value; |
| obj.valueTagClass = attr.value[1].type; |
| // if the OID is known, get its name and short name |
| if(obj.type in oids) { |
| obj.name = oids[obj.type]; |
| if(obj.name in _shortNames) { |
| obj.shortName = _shortNames[obj.name]; |
| } |
| } |
| if(md) { |
| md.update(obj.type); |
| md.update(obj.value); |
| } |
| rval.push(obj); |
| } |
| } |
| |
| return rval; |
| }; |
| |
| /** |
| * Converts ASN.1 CRIAttributes into an array with objects that have type and |
| * value properties. |
| * |
| * @param attributes the CRIAttributes to convert. |
| */ |
| pki.CRIAttributesAsArray = function(attributes) { |
| var rval = []; |
| |
| // each value in 'attributes' in is a SEQUENCE with an OID and a SET |
| for(var si = 0; si < attributes.length; ++si) { |
| // get the attribute sequence |
| var seq = attributes[si]; |
| |
| // each value in the SEQUENCE containing first a type (an OID) and |
| // second a set of values (defined by the OID) |
| var type = asn1.derToOid(seq.value[0].value); |
| var values = seq.value[1].value; |
| for(var vi = 0; vi < values.length; ++vi) { |
| var obj = {}; |
| obj.type = type; |
| obj.value = values[vi].value; |
| obj.valueTagClass = values[vi].type; |
| // if the OID is known, get its name and short name |
| if(obj.type in oids) { |
| obj.name = oids[obj.type]; |
| if(obj.name in _shortNames) { |
| obj.shortName = _shortNames[obj.name]; |
| } |
| } |
| // parse extensions |
| if(obj.type === oids.extensionRequest) { |
| obj.extensions = []; |
| for(var ei = 0; ei < obj.value.length; ++ei) { |
| obj.extensions.push(pki.certificateExtensionFromAsn1(obj.value[ei])); |
| } |
| } |
| rval.push(obj); |
| } |
| } |
| |
| return rval; |
| }; |
| |
| /** |
| * Gets an issuer or subject attribute from its name, type, or short name. |
| * |
| * @param obj the issuer or subject object. |
| * @param options a short name string or an object with: |
| * shortName the short name for the attribute. |
| * name the name for the attribute. |
| * type the type for the attribute. |
| * |
| * @return the attribute. |
| */ |
| function _getAttribute(obj, options) { |
| if(typeof options === 'string') { |
| options = {shortName: options}; |
| } |
| |
| var rval = null; |
| var attr; |
| for(var i = 0; rval === null && i < obj.attributes.length; ++i) { |
| attr = obj.attributes[i]; |
| if(options.type && options.type === attr.type) { |
| rval = attr; |
| } else if(options.name && options.name === attr.name) { |
| rval = attr; |
| } else if(options.shortName && options.shortName === attr.shortName) { |
| rval = attr; |
| } |
| } |
| return rval; |
| } |
| |
| /** |
| * Converts signature parameters from ASN.1 structure. |
| * |
| * Currently only RSASSA-PSS supported. The PKCS#1 v1.5 signature scheme had |
| * no parameters. |
| * |
| * RSASSA-PSS-params ::= SEQUENCE { |
| * hashAlgorithm [0] HashAlgorithm DEFAULT |
| * sha1Identifier, |
| * maskGenAlgorithm [1] MaskGenAlgorithm DEFAULT |
| * mgf1SHA1Identifier, |
| * saltLength [2] INTEGER DEFAULT 20, |
| * trailerField [3] INTEGER DEFAULT 1 |
| * } |
| * |
| * HashAlgorithm ::= AlgorithmIdentifier |
| * |
| * MaskGenAlgorithm ::= AlgorithmIdentifier |
| * |
| * AlgorithmIdentifer ::= SEQUENCE { |
| * algorithm OBJECT IDENTIFIER, |
| * parameters ANY DEFINED BY algorithm OPTIONAL |
| * } |
| * |
| * @param oid The OID specifying the signature algorithm |
| * @param obj The ASN.1 structure holding the parameters |
| * @param fillDefaults Whether to use return default values where omitted |
| * @return signature parameter object |
| */ |
| var _readSignatureParameters = function(oid, obj, fillDefaults) { |
| var params = {}; |
| |
| if(oid !== oids['RSASSA-PSS']) { |
| return params; |
| } |
| |
| if(fillDefaults) { |
| params = { |
| hash: { |
| algorithmOid: oids['sha1'] |
| }, |
| mgf: { |
| algorithmOid: oids['mgf1'], |
| hash: { |
| algorithmOid: oids['sha1'] |
| } |
| }, |
| saltLength: 20 |
| }; |
| } |
| |
| var capture = {}; |
| var errors = []; |
| if(!asn1.validate(obj, rsassaPssParameterValidator, capture, errors)) { |
| var error = new Error('Cannot read RSASSA-PSS parameter block.'); |
| error.errors = errors; |
| throw error; |
| } |
| |
| if(capture.hashOid !== undefined) { |
| params.hash = params.hash || {}; |
| params.hash.algorithmOid = asn1.derToOid(capture.hashOid); |
| } |
| |
| if(capture.maskGenOid !== undefined) { |
| params.mgf = params.mgf || {}; |
| params.mgf.algorithmOid = asn1.derToOid(capture.maskGenOid); |
| params.mgf.hash = params.mgf.hash || {}; |
| params.mgf.hash.algorithmOid = asn1.derToOid(capture.maskGenHashOid); |
| } |
| |
| if(capture.saltLength !== undefined) { |
| params.saltLength = capture.saltLength.charCodeAt(0); |
| } |
| |
| return params; |
| }; |
| |
| /** |
| * Converts an X.509 certificate from PEM format. |
| * |
| * Note: If the certificate is to be verified then compute hash should |
| * be set to true. This will scan the TBSCertificate part of the ASN.1 |
| * object while it is converted so it doesn't need to be converted back |
| * to ASN.1-DER-encoding later. |
| * |
| * @param pem the PEM-formatted certificate. |
| * @param computeHash true to compute the hash for verification. |
| * @param strict true to be strict when checking ASN.1 value lengths, false to |
| * allow truncated values (default: true). |
| * |
| * @return the certificate. |
| */ |
| pki.certificateFromPem = function(pem, computeHash, strict) { |
| var msg = forge.pem.decode(pem)[0]; |
| |
| if(msg.type !== 'CERTIFICATE' && |
| msg.type !== 'X509 CERTIFICATE' && |
| msg.type !== 'TRUSTED CERTIFICATE') { |
| var error = new Error('Could not convert certificate from PEM; PEM header type ' + |
| 'is not "CERTIFICATE", "X509 CERTIFICATE", or "TRUSTED CERTIFICATE".'); |
| error.headerType = msg.type; |
| throw error; |
| } |
| if(msg.procType && msg.procType.type === 'ENCRYPTED') { |
| throw new Error('Could not convert certificate from PEM; PEM is encrypted.'); |
| } |
| |
| // convert DER to ASN.1 object |
| var obj = asn1.fromDer(msg.body, strict); |
| |
| return pki.certificateFromAsn1(obj, computeHash); |
| }; |
| |
| /** |
| * Converts an X.509 certificate to PEM format. |
| * |
| * @param cert the certificate. |
| * @param maxline the maximum characters per line, defaults to 64. |
| * |
| * @return the PEM-formatted certificate. |
| */ |
| pki.certificateToPem = function(cert, maxline) { |
| // convert to ASN.1, then DER, then PEM-encode |
| var msg = { |
| type: 'CERTIFICATE', |
| body: asn1.toDer(pki.certificateToAsn1(cert)).getBytes() |
| }; |
| return forge.pem.encode(msg, {maxline: maxline}); |
| }; |
| |
| /** |
| * Converts an RSA public key from PEM format. |
| * |
| * @param pem the PEM-formatted public key. |
| * |
| * @return the public key. |
| */ |
| pki.publicKeyFromPem = function(pem) { |
| var msg = forge.pem.decode(pem)[0]; |
| |
| if(msg.type !== 'PUBLIC KEY' && msg.type !== 'RSA PUBLIC KEY') { |
| var error = new Error('Could not convert public key from PEM; PEM header ' + |
| 'type is not "PUBLIC KEY" or "RSA PUBLIC KEY".'); |
| error.headerType = msg.type; |
| throw error; |
| } |
| if(msg.procType && msg.procType.type === 'ENCRYPTED') { |
| throw new Error('Could not convert public key from PEM; PEM is encrypted.'); |
| } |
| |
| // convert DER to ASN.1 object |
| var obj = asn1.fromDer(msg.body); |
| |
| return pki.publicKeyFromAsn1(obj); |
| }; |
| |
| /** |
| * Converts an RSA public key to PEM format (using a SubjectPublicKeyInfo). |
| * |
| * @param key the public key. |
| * @param maxline the maximum characters per line, defaults to 64. |
| * |
| * @return the PEM-formatted public key. |
| */ |
| pki.publicKeyToPem = function(key, maxline) { |
| // convert to ASN.1, then DER, then PEM-encode |
| var msg = { |
| type: 'PUBLIC KEY', |
| body: asn1.toDer(pki.publicKeyToAsn1(key)).getBytes() |
| }; |
| return forge.pem.encode(msg, {maxline: maxline}); |
| }; |
| |
| /** |
| * Converts an RSA public key to PEM format (using an RSAPublicKey). |
| * |
| * @param key the public key. |
| * @param maxline the maximum characters per line, defaults to 64. |
| * |
| * @return the PEM-formatted public key. |
| */ |
| pki.publicKeyToRSAPublicKeyPem = function(key, maxline) { |
| // convert to ASN.1, then DER, then PEM-encode |
| var msg = { |
| type: 'RSA PUBLIC KEY', |
| body: asn1.toDer(pki.publicKeyToRSAPublicKey(key)).getBytes() |
| }; |
| return forge.pem.encode(msg, {maxline: maxline}); |
| }; |
| |
| /** |
| * Gets a fingerprint for the given public key. |
| * |
| * @param options the options to use. |
| * [md] the message digest object to use (defaults to forge.md.sha1). |
| * [type] the type of fingerprint, such as 'RSAPublicKey', |
| * 'SubjectPublicKeyInfo' (defaults to 'RSAPublicKey'). |
| * [encoding] an alternative output encoding, such as 'hex' |
| * (defaults to none, outputs a byte buffer). |
| * [delimiter] the delimiter to use between bytes for 'hex' encoded |
| * output, eg: ':' (defaults to none). |
| * |
| * @return the fingerprint as a byte buffer or other encoding based on options. |
| */ |
| pki.getPublicKeyFingerprint = function(key, options) { |
| options = options || {}; |
| var md = options.md || forge.md.sha1.create(); |
| var type = options.type || 'RSAPublicKey'; |
| |
| var bytes; |
| switch(type) { |
| case 'RSAPublicKey': |
| bytes = asn1.toDer(pki.publicKeyToRSAPublicKey(key)).getBytes(); |
| break; |
| case 'SubjectPublicKeyInfo': |
| bytes = asn1.toDer(pki.publicKeyToAsn1(key)).getBytes(); |
| break; |
| default: |
| throw new Error('Unknown fingerprint type "' + options.type + '".'); |
| } |
| |
| // hash public key bytes |
| md.start(); |
| md.update(bytes); |
| var digest = md.digest(); |
| if(options.encoding === 'hex') { |
| var hex = digest.toHex(); |
| if(options.delimiter) { |
| return hex.match(/.{2}/g).join(options.delimiter); |
| } |
| return hex; |
| } else if(options.encoding === 'binary') { |
| return digest.getBytes(); |
| } else if(options.encoding) { |
| throw new Error('Unknown encoding "' + options.encoding + '".'); |
| } |
| return digest; |
| }; |
| |
| /** |
| * Converts a PKCS#10 certification request (CSR) from PEM format. |
| * |
| * Note: If the certification request is to be verified then compute hash |
| * should be set to true. This will scan the CertificationRequestInfo part of |
| * the ASN.1 object while it is converted so it doesn't need to be converted |
| * back to ASN.1-DER-encoding later. |
| * |
| * @param pem the PEM-formatted certificate. |
| * @param computeHash true to compute the hash for verification. |
| * @param strict true to be strict when checking ASN.1 value lengths, false to |
| * allow truncated values (default: true). |
| * |
| * @return the certification request (CSR). |
| */ |
| pki.certificationRequestFromPem = function(pem, computeHash, strict) { |
| var msg = forge.pem.decode(pem)[0]; |
| |
| if(msg.type !== 'CERTIFICATE REQUEST') { |
| var error = new Error('Could not convert certification request from PEM; ' + |
| 'PEM header type is not "CERTIFICATE REQUEST".'); |
| error.headerType = msg.type; |
| throw error; |
| } |
| if(msg.procType && msg.procType.type === 'ENCRYPTED') { |
| throw new Error('Could not convert certification request from PEM; ' + |
| 'PEM is encrypted.'); |
| } |
| |
| // convert DER to ASN.1 object |
| var obj = asn1.fromDer(msg.body, strict); |
| |
| return pki.certificationRequestFromAsn1(obj, computeHash); |
| }; |
| |
| /** |
| * Converts a PKCS#10 certification request (CSR) to PEM format. |
| * |
| * @param csr the certification request. |
| * @param maxline the maximum characters per line, defaults to 64. |
| * |
| * @return the PEM-formatted certification request. |
| */ |
| pki.certificationRequestToPem = function(csr, maxline) { |
| // convert to ASN.1, then DER, then PEM-encode |
| var msg = { |
| type: 'CERTIFICATE REQUEST', |
| body: asn1.toDer(pki.certificationRequestToAsn1(csr)).getBytes() |
| }; |
| return forge.pem.encode(msg, {maxline: maxline}); |
| }; |
| |
| /** |
| * Creates an empty X.509v3 RSA certificate. |
| * |
| * @return the certificate. |
| */ |
| pki.createCertificate = function() { |
| var cert = {}; |
| cert.version = 0x02; |
| cert.serialNumber = '00'; |
| cert.signatureOid = null; |
| cert.signature = null; |
| cert.siginfo = {}; |
| cert.siginfo.algorithmOid = null; |
| cert.validity = {}; |
| cert.validity.notBefore = new Date(); |
| cert.validity.notAfter = new Date(); |
| |
| cert.issuer = {}; |
| cert.issuer.getField = function(sn) { |
| return _getAttribute(cert.issuer, sn); |
| }; |
| cert.issuer.addField = function(attr) { |
| _fillMissingFields([attr]); |
| cert.issuer.attributes.push(attr); |
| }; |
| cert.issuer.attributes = []; |
| cert.issuer.hash = null; |
| |
| cert.subject = {}; |
| cert.subject.getField = function(sn) { |
| return _getAttribute(cert.subject, sn); |
| }; |
| cert.subject.addField = function(attr) { |
| _fillMissingFields([attr]); |
| cert.subject.attributes.push(attr); |
| }; |
| cert.subject.attributes = []; |
| cert.subject.hash = null; |
| |
| cert.extensions = []; |
| cert.publicKey = null; |
| cert.md = null; |
| |
| /** |
| * Sets the subject of this certificate. |
| * |
| * @param attrs the array of subject attributes to use. |
| * @param uniqueId an optional a unique ID to use. |
| */ |
| cert.setSubject = function(attrs, uniqueId) { |
| // set new attributes, clear hash |
| _fillMissingFields(attrs); |
| cert.subject.attributes = attrs; |
| delete cert.subject.uniqueId; |
| if(uniqueId) { |
| cert.subject.uniqueId = uniqueId; |
| } |
| cert.subject.hash = null; |
| }; |
| |
| /** |
| * Sets the issuer of this certificate. |
| * |
| * @param attrs the array of issuer attributes to use. |
| * @param uniqueId an optional a unique ID to use. |
| */ |
| cert.setIssuer = function(attrs, uniqueId) { |
| // set new attributes, clear hash |
| _fillMissingFields(attrs); |
| cert.issuer.attributes = attrs; |
| delete cert.issuer.uniqueId; |
| if(uniqueId) { |
| cert.issuer.uniqueId = uniqueId; |
| } |
| cert.issuer.hash = null; |
| }; |
| |
| /** |
| * Sets the extensions of this certificate. |
| * |
| * @param exts the array of extensions to use. |
| */ |
| cert.setExtensions = function(exts) { |
| for(var i = 0; i < exts.length; ++i) { |
| _fillMissingExtensionFields(exts[i], {cert: cert}); |
| } |
| // set new extensions |
| cert.extensions = exts; |
| }; |
| |
| /** |
| * Gets an extension by its name or id. |
| * |
| * @param options the name to use or an object with: |
| * name the name to use. |
| * id the id to use. |
| * |
| * @return the extension or null if not found. |
| */ |
| cert.getExtension = function(options) { |
| if(typeof options === 'string') { |
| options = {name: options}; |
| } |
| |
| var rval = null; |
| var ext; |
| for(var i = 0; rval === null && i < cert.extensions.length; ++i) { |
| ext = cert.extensions[i]; |
| if(options.id && ext.id === options.id) { |
| rval = ext; |
| } else if(options.name && ext.name === options.name) { |
| rval = ext; |
| } |
| } |
| return rval; |
| }; |
| |
| /** |
| * Signs this certificate using the given private key. |
| * |
| * @param key the private key to sign with. |
| * @param md the message digest object to use (defaults to forge.md.sha1). |
| */ |
| cert.sign = function(key, md) { |
| // TODO: get signature OID from private key |
| cert.md = md || forge.md.sha1.create(); |
| var algorithmOid = oids[cert.md.algorithm + 'WithRSAEncryption']; |
| if(!algorithmOid) { |
| var error = new Error('Could not compute certificate digest. ' + |
| 'Unknown message digest algorithm OID.'); |
| error.algorithm = cert.md.algorithm; |
| throw error; |
| } |
| cert.signatureOid = cert.siginfo.algorithmOid = algorithmOid; |
| |
| // get TBSCertificate, convert to DER |
| cert.tbsCertificate = pki.getTBSCertificate(cert); |
| var bytes = asn1.toDer(cert.tbsCertificate); |
| |
| // digest and sign |
| cert.md.update(bytes.getBytes()); |
| cert.signature = key.sign(cert.md); |
| }; |
| |
| /** |
| * Attempts verify the signature on the passed certificate using this |
| * certificate's public key. |
| * |
| * @param child the certificate to verify. |
| * |
| * @return true if verified, false if not. |
| */ |
| cert.verify = function(child) { |
| var rval = false; |
| |
| if(!cert.issued(child)) { |
| var issuer = child.issuer; |
| var subject = cert.subject; |
| var error = new Error('The parent certificate did not issue the given child ' + |
| 'certificate; the child certificate\'s issuer does not match the ' + |
| 'parent\'s subject.'); |
| error.expectedIssuer = issuer.attributes; |
| error.actualIssuer = subject.attributes; |
| throw error; |
| } |
| |
| var md = child.md; |
| if(md === null) { |
| // 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 'sha256WithRSAEncryption': |
| md = forge.md.sha256.create(); |
| break; |
| case 'RSASSA-PSS': |
| md = forge.md.sha256.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); |
| var bytes = asn1.toDer(tbsCertificate); |
| md.update(bytes.getBytes()); |
| } |
| |
| if(md !== null) { |
| var scheme; |
| |
| switch(child.signatureOid) { |
| case oids.sha1WithRSAEncryption: |
| scheme = undefined; /* use PKCS#1 v1.5 padding scheme */ |
| break; |
| case oids['RSASSA-PSS']: |
| var hash, mgf; |
| |
| /* initialize mgf */ |
| hash = oids[child.signatureParameters.mgf.hash.algorithmOid]; |
| if(hash === undefined || forge.md[hash] === undefined) { |
| var error = new Error('Unsupported MGF hash function.'); |
| error.oid = child.signatureParameters.mgf.hash.algorithmOid; |
| error.name = hash; |
| throw error; |
| } |
| |
| mgf = oids[child.signatureParameters.mgf.algorithmOid]; |
| if(mgf === undefined || forge.mgf[mgf] === undefined) { |
| var error = new Error('Unsupported MGF function.'); |
| error.oid = child.signatureParameters.mgf.algorithmOid; |
| error.name = mgf; |
| throw error; |
| } |
| |
| mgf = forge.mgf[mgf].create(forge.md[hash].create()); |
| |
| /* initialize hash function */ |
| hash = oids[child.signatureParameters.hash.algorithmOid]; |
| if(hash === undefined || forge.md[hash] === undefined) { |
| throw { |
| message: 'Unsupported RSASSA-PSS hash function.', |
| oid: child.signatureParameters.hash.algorithmOid, |
| name: hash |
| }; |
| } |
| |
| scheme = forge.pss.create(forge.md[hash].create(), mgf, |
| child.signatureParameters.saltLength); |
| break; |
| } |
| |
| // verify signature on cert using public key |
| rval = cert.publicKey.verify( |
| md.digest().getBytes(), child.signature, scheme); |
| } |
| |
| return rval; |
| }; |
| |
| /** |
| * Returns true if this certificate's issuer matches the passed |
| * certificate's subject. Note that no signature check is performed. |
| * |
| * @param parent the certificate to check. |
| * |
| * @return true if this certificate's issuer matches the passed certificate's |
| * subject. |
| */ |
| cert.isIssuer = function(parent) { |
| var rval = false; |
| |
| var i = cert.issuer; |
| var s = parent.subject; |
| |
| // compare hashes if present |
| if(i.hash && s.hash) { |
| rval = (i.hash === s.hash); |
| } else if(i.attributes.length === s.attributes.length) { |
| // all attributes are the same so issuer matches subject |
| rval = true; |
| var iattr, sattr; |
| for(var n = 0; rval && n < i.attributes.length; ++n) { |
| iattr = i.attributes[n]; |
| sattr = s.attributes[n]; |
| if(iattr.type !== sattr.type || iattr.value !== sattr.value) { |
| // attribute mismatch |
| rval = false; |
| } |
| } |
| } |
| |
| return rval; |
| }; |
| |
| /** |
| * Returns true if this certificate's subject matches the issuer of the |
| * given certificate). Note that not signature check is performed. |
| * |
| * @param child the certificate to check. |
| * |
| * @return true if this certificate's subject matches the passed |
| * certificate's issuer. |
| */ |
| cert.issued = function(child) { |
| return child.isIssuer(cert); |
| }; |
| |
| /** |
| * Generates the subjectKeyIdentifier for this certificate as byte buffer. |
| * |
| * @return the subjectKeyIdentifier for this certificate as byte buffer. |
| */ |
| cert.generateSubjectKeyIdentifier = function() { |
| /* See: 4.2.1.2 section of the the RFC3280, keyIdentifier is either: |
| |
| (1) The keyIdentifier is composed of the 160-bit SHA-1 hash of the |
| value of the BIT STRING subjectPublicKey (excluding the tag, |
| length, and number of unused bits). |
| |
| (2) The keyIdentifier is composed of a four bit type field with |
| the value 0100 followed by the least significant 60 bits of the |
| SHA-1 hash of the value of the BIT STRING subjectPublicKey |
| (excluding the tag, length, and number of unused bit string bits). |
| */ |
| |
| // skipping the tag, length, and number of unused bits is the same |
| // as just using the RSAPublicKey (for RSA keys, which are the |
| // only ones supported) |
| return pki.getPublicKeyFingerprint(cert.publicKey, {type: 'RSAPublicKey'}); |
| }; |
| |
| /** |
| * Verifies the subjectKeyIdentifier extension value for this certificate |
| * against its public key. If no extension is found, false will be |
| * returned. |
| * |
| * @return true if verified, false if not. |
| */ |
| cert.verifySubjectKeyIdentifier = function() { |
| var oid = oids['subjectKeyIdentifier']; |
| for(var i = 0; i < cert.extensions.length; ++i) { |
| var ext = cert.extensions[i]; |
| if(ext.id === oid) { |
| var ski = cert.generateSubjectKeyIdentifier().getBytes(); |
| return (forge.util.hexToBytes(ext.subjectKeyIdentifier) === ski); |
| } |
| } |
| return false; |
| }; |
| |
| return cert; |
| }; |
| |
| /** |
| * Converts an X.509v3 RSA certificate from an ASN.1 object. |
| * |
| * Note: If the certificate is to be verified then compute hash should |
| * be set to true. There is currently no implementation for converting |
| * a certificate back to ASN.1 so the TBSCertificate part of the ASN.1 |
| * object needs to be scanned before the cert object is created. |
| * |
| * @param obj the asn1 representation of an X.509v3 RSA certificate. |
| * @param computeHash true to compute the hash for verification. |
| * |
| * @return the certificate. |
| */ |
| pki.certificateFromAsn1 = function(obj, computeHash) { |
| // validate certificate and capture data |
| var capture = {}; |
| var errors = []; |
| if(!asn1.validate(obj, x509CertificateValidator, capture, errors)) { |
| var error = new Error('Cannot read X.509 certificate. ' + |
| 'ASN.1 object is not an X509v3 Certificate.'); |
| error.errors = errors; |
| throw error; |
| } |
| |
| // ensure signature is not interpreted as an embedded ASN.1 object |
| if(typeof capture.certSignature !== 'string') { |
| var certSignature = '\x00'; |
| for(var i = 0; i < capture.certSignature.length; ++i) { |
| certSignature += asn1.toDer(capture.certSignature[i]).getBytes(); |
| } |
| capture.certSignature = certSignature; |
| } |
| |
| // get oid |
| var oid = asn1.derToOid(capture.publicKeyOid); |
| if(oid !== pki.oids['rsaEncryption']) { |
| throw new Error('Cannot read public key. OID is not RSA.'); |
| } |
| |
| // create certificate |
| var cert = pki.createCertificate(); |
| cert.version = capture.certVersion ? |
| capture.certVersion.charCodeAt(0) : 0; |
| var serial = forge.util.createBuffer(capture.certSerialNumber); |
| cert.serialNumber = serial.toHex(); |
| cert.signatureOid = forge.asn1.derToOid(capture.certSignatureOid); |
| cert.signatureParameters = _readSignatureParameters( |
| cert.signatureOid, capture.certSignatureParams, true); |
| cert.siginfo.algorithmOid = forge.asn1.derToOid(capture.certinfoSignatureOid); |
| cert.siginfo.parameters = _readSignatureParameters(cert.siginfo.algorithmOid, |
| capture.certinfoSignatureParams, false); |
| // skip "unused bits" in signature value BITSTRING |
| var signature = forge.util.createBuffer(capture.certSignature); |
| ++signature.read; |
| cert.signature = signature.getBytes(); |
| |
| var validity = []; |
| if(capture.certValidity1UTCTime !== undefined) { |
| validity.push(asn1.utcTimeToDate(capture.certValidity1UTCTime)); |
| } |
| if(capture.certValidity2GeneralizedTime !== undefined) { |
| validity.push(asn1.generalizedTimeToDate( |
| capture.certValidity2GeneralizedTime)); |
| } |
| if(capture.certValidity3UTCTime !== undefined) { |
| validity.push(asn1.utcTimeToDate(capture.certValidity3UTCTime)); |
| } |
| if(capture.certValidity4GeneralizedTime !== undefined) { |
| validity.push(asn1.generalizedTimeToDate( |
| capture.certValidity4GeneralizedTime)); |
| } |
| if(validity.length > 2) { |
| throw new Error('Cannot read notBefore/notAfter validity times; more ' + |
| 'than two times were provided in the certificate.'); |
| } |
| if(validity.length < 2) { |
| throw new Error('Cannot read notBefore/notAfter validity times; they ' + |
| 'were not provided as either UTCTime or GeneralizedTime.'); |
| } |
| cert.validity.notBefore = validity[0]; |
| cert.validity.notAfter = validity[1]; |
| |
| // keep TBSCertificate to preserve signature when exporting |
| cert.tbsCertificate = capture.tbsCertificate; |
| |
| if(computeHash) { |
| // check signature OID for supported signature types |
| cert.md = null; |
| if(cert.signatureOid in oids) { |
| var oid = oids[cert.signatureOid]; |
| switch(oid) { |
| case 'sha1WithRSAEncryption': |
| cert.md = forge.md.sha1.create(); |
| break; |
| case 'md5WithRSAEncryption': |
| cert.md = forge.md.md5.create(); |
| break; |
| case 'sha256WithRSAEncryption': |
| cert.md = forge.md.sha256.create(); |
| break; |
| case 'RSASSA-PSS': |
| cert.md = forge.md.sha256.create(); |
| break; |
| } |
| } |
| if(cert.md === null) { |
| var error = new Error('Could not compute certificate digest. ' + |
| 'Unknown signature OID.'); |
| error.signatureOid = cert.signatureOid; |
| throw error; |
| } |
| |
| // produce DER formatted TBSCertificate and digest it |
| var bytes = asn1.toDer(cert.tbsCertificate); |
| cert.md.update(bytes.getBytes()); |
| } |
| |
| // handle issuer, build issuer message digest |
| var imd = forge.md.sha1.create(); |
| cert.issuer.getField = function(sn) { |
| return _getAttribute(cert.issuer, sn); |
| }; |
| cert.issuer.addField = function(attr) { |
| _fillMissingFields([attr]); |
| cert.issuer.attributes.push(attr); |
| }; |
| cert.issuer.attributes = pki.RDNAttributesAsArray(capture.certIssuer, imd); |
| if(capture.certIssuerUniqueId) { |
| cert.issuer.uniqueId = capture.certIssuerUniqueId; |
| } |
| cert.issuer.hash = imd.digest().toHex(); |
| |
| // handle subject, build subject message digest |
| var smd = forge.md.sha1.create(); |
| cert.subject.getField = function(sn) { |
| return _getAttribute(cert.subject, sn); |
| }; |
| cert.subject.addField = function(attr) { |
| _fillMissingFields([attr]); |
| cert.subject.attributes.push(attr); |
| }; |
| cert.subject.attributes = pki.RDNAttributesAsArray(capture.certSubject, smd); |
| if(capture.certSubjectUniqueId) { |
| cert.subject.uniqueId = capture.certSubjectUniqueId; |
| } |
| cert.subject.hash = smd.digest().toHex(); |
| |
| // handle extensions |
| if(capture.certExtensions) { |
| cert.extensions = pki.certificateExtensionsFromAsn1(capture.certExtensions); |
| } else { |
| cert.extensions = []; |
| } |
| |
| // convert RSA public key from ASN.1 |
| cert.publicKey = pki.publicKeyFromAsn1(capture.subjectPublicKeyInfo); |
| |
| return cert; |
| }; |
| |
| /** |
| * Converts an ASN.1 extensions object (with extension sequences as its |
| * values) into an array of extension objects with types and values. |
| * |
| * Supported extensions: |
| * |
| * id-ce-keyUsage OBJECT IDENTIFIER ::= { id-ce 15 } |
| * KeyUsage ::= BIT STRING { |
| * digitalSignature (0), |
| * nonRepudiation (1), |
| * keyEncipherment (2), |
| * dataEncipherment (3), |
| * keyAgreement (4), |
| * keyCertSign (5), |
| * cRLSign (6), |
| * encipherOnly (7), |
| * decipherOnly (8) |
| * } |
| * |
| * id-ce-basicConstraints OBJECT IDENTIFIER ::= { id-ce 19 } |
| * BasicConstraints ::= SEQUENCE { |
| * cA BOOLEAN DEFAULT FALSE, |
| * pathLenConstraint INTEGER (0..MAX) OPTIONAL |
| * } |
| * |
| * subjectAltName EXTENSION ::= { |
| * SYNTAX GeneralNames |
| * IDENTIFIED BY id-ce-subjectAltName |
| * } |
| * |
| * GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName |
| * |
| * GeneralName ::= CHOICE { |
| * otherName [0] INSTANCE OF OTHER-NAME, |
| * rfc822Name [1] IA5String, |
| * dNSName [2] IA5String, |
| * x400Address [3] ORAddress, |
| * directoryName [4] Name, |
| * ediPartyName [5] EDIPartyName, |
| * uniformResourceIdentifier [6] IA5String, |
| * IPAddress [7] OCTET STRING, |
| * registeredID [8] OBJECT IDENTIFIER |
| * } |
| * |
| * OTHER-NAME ::= TYPE-IDENTIFIER |
| * |
| * EDIPartyName ::= SEQUENCE { |
| * nameAssigner [0] DirectoryString {ub-name} OPTIONAL, |
| * partyName [1] DirectoryString {ub-name} |
| * } |
| * |
| * @param exts the extensions ASN.1 with extension sequences to parse. |
| * |
| * @return the array. |
| */ |
| pki.certificateExtensionsFromAsn1 = function(exts) { |
| var rval = []; |
| for(var i = 0; i < exts.value.length; ++i) { |
| // get extension sequence |
| var extseq = exts.value[i]; |
| for(var ei = 0; ei < extseq.value.length; ++ei) { |
| rval.push(pki.certificateExtensionFromAsn1(extseq.value[ei])); |
| } |
| } |
| |
| return rval; |
| }; |
| |
| /** |
| * Parses a single certificate extension from ASN.1. |
| * |
| * @param ext the extension in ASN.1 format. |
| * |
| * @return the parsed extension as an object. |
| */ |
| pki.certificateExtensionFromAsn1 = function(ext) { |
| // an extension has: |
| // [0] extnID OBJECT IDENTIFIER |
| // [1] critical BOOLEAN DEFAULT FALSE |
| // [2] extnValue OCTET STRING |
| var e = {}; |
| e.id = asn1.derToOid(ext.value[0].value); |
| e.critical = false; |
| if(ext.value[1].type === asn1.Type.BOOLEAN) { |
| e.critical = (ext.value[1].value.charCodeAt(0) !== 0x00); |
| e.value = ext.value[2].value; |
| } else { |
| e.value = ext.value[1].value; |
| } |
| // if the oid is known, get its name |
| if(e.id in oids) { |
| e.name = oids[e.id]; |
| |
| // handle key usage |
| if(e.name === 'keyUsage') { |
| // get value as BIT STRING |
| var ev = asn1.fromDer(e.value); |
| var b2 = 0x00; |
| var b3 = 0x00; |
| if(ev.value.length > 1) { |
| // skip first byte, just indicates unused bits which |
| // will be padded with 0s anyway |
| // get bytes with flag bits |
| b2 = ev.value.charCodeAt(1); |
| b3 = ev.value.length > 2 ? ev.value.charCodeAt(2) : 0; |
| } |
| // set flags |
| e.digitalSignature = (b2 & 0x80) === 0x80; |
| e.nonRepudiation = (b2 & 0x40) === 0x40; |
| e.keyEncipherment = (b2 & 0x20) === 0x20; |
| e.dataEncipherment = (b2 & 0x10) === 0x10; |
| e.keyAgreement = (b2 & 0x08) === 0x08; |
| e.keyCertSign = (b2 & 0x04) === 0x04; |
| e.cRLSign = (b2 & 0x02) === 0x02; |
| e.encipherOnly = (b2 & 0x01) === 0x01; |
| e.decipherOnly = (b3 & 0x80) === 0x80; |
| } else if(e.name === 'basicConstraints') { |
| // handle basic constraints |
| // get value as SEQUENCE |
| var ev = asn1.fromDer(e.value); |
| // get cA BOOLEAN flag (defaults to false) |
| if(ev.value.length > 0 && ev.value[0].type === asn1.Type.BOOLEAN) { |
| e.cA = (ev.value[0].value.charCodeAt(0) !== 0x00); |
| } else { |
| e.cA = false; |
| } |
| // get path length constraint |
| var value = null; |
| if(ev.value.length > 0 && ev.value[0].type === asn1.Type.INTEGER) { |
| value = ev.value[0].value; |
| } else if(ev.value.length > 1) { |
| value = ev.value[1].value; |
| } |
| if(value !== null) { |
| e.pathLenConstraint = asn1.derToInteger(value); |
| } |
| } else if(e.name === 'extKeyUsage') { |
| // handle extKeyUsage |
| // value is a SEQUENCE of OIDs |
| var ev = asn1.fromDer(e.value); |
| for(var vi = 0; vi < ev.value.length; ++vi) { |
| var oid = asn1.derToOid(ev.value[vi].value); |
| if(oid in oids) { |
| e[oids[oid]] = true; |
| } else { |
| e[oid] = true; |
| } |
| } |
| } else if(e.name === 'nsCertType') { |
| // handle nsCertType |
| // get value as BIT STRING |
| var ev = asn1.fromDer(e.value); |
| var b2 = 0x00; |
| if(ev.value.length > 1) { |
| // skip first byte, just indicates unused bits which |
| // will be padded with 0s anyway |
| // get bytes with flag bits |
| b2 = ev.value.charCodeAt(1); |
| } |
| // set flags |
| e.client = (b2 & 0x80) === 0x80; |
| e.server = (b2 & 0x40) === 0x40; |
| e.email = (b2 & 0x20) === 0x20; |
| e.objsign = (b2 & 0x10) === 0x10; |
| e.reserved = (b2 & 0x08) === 0x08; |
| e.sslCA = (b2 & 0x04) === 0x04; |
| e.emailCA = (b2 & 0x02) === 0x02; |
| e.objCA = (b2 & 0x01) === 0x01; |
| } else if( |
| e.name === 'subjectAltName' || |
| e.name === 'issuerAltName') { |
| // handle subjectAltName/issuerAltName |
| e.altNames = []; |
| |
| // ev is a SYNTAX SEQUENCE |
| var gn; |
| var ev = asn1.fromDer(e.value); |
| for(var n = 0; n < ev.value.length; ++n) { |
| // get GeneralName |
| gn = ev.value[n]; |
| |
| var altName = { |
| type: gn.type, |
| value: gn.value |
| }; |
| e.altNames.push(altName); |
| |
| // Note: Support for types 1,2,6,7,8 |
| switch(gn.type) { |
| // rfc822Name |
| case 1: |
| // dNSName |
| case 2: |
| // uniformResourceIdentifier (URI) |
| case 6: |
| break; |
| // IPAddress |
| case 7: |
| // convert to IPv4/IPv6 string representation |
| altName.ip = forge.util.bytesToIP(gn.value); |
| break; |
| // registeredID |
| case 8: |
| altName.oid = asn1.derToOid(gn.value); |
| break; |
| default: |
| // unsupported |
| } |
| } |
| } else if(e.name === 'subjectKeyIdentifier') { |
| // value is an OCTETSTRING w/the hash of the key-type specific |
| // public key structure (eg: RSAPublicKey) |
| var ev = asn1.fromDer(e.value); |
| e.subjectKeyIdentifier = forge.util.bytesToHex(ev.value); |
| } |
| } |
| return e; |
| }; |
| |
| /** |
| * Converts a PKCS#10 certification request (CSR) from an ASN.1 object. |
| * |
| * Note: If the certification request is to be verified then compute hash |
| * should be set to true. There is currently no implementation for converting |
| * a certificate back to ASN.1 so the CertificationRequestInfo part of the |
| * ASN.1 object needs to be scanned before the csr object is created. |
| * |
| * @param obj the asn1 representation of a PKCS#10 certification request (CSR). |
| * @param computeHash true to compute the hash for verification. |
| * |
| * @return the certification request (CSR). |
| */ |
| pki.certificationRequestFromAsn1 = function(obj, computeHash) { |
| // validate certification request and capture data |
| var capture = {}; |
| var errors = []; |
| if(!asn1.validate(obj, certificationRequestValidator, capture, errors)) { |
| var error = new Error('Cannot read PKCS#10 certificate request. ' + |
| 'ASN.1 object is not a PKCS#10 CertificationRequest.'); |
| error.errors = errors; |
| throw error; |
| } |
| |
| // ensure signature is not interpreted as an embedded ASN.1 object |
| if(typeof capture.csrSignature !== 'string') { |
| var csrSignature = '\x00'; |
| for(var i = 0; i < capture.csrSignature.length; ++i) { |
| csrSignature += asn1.toDer(capture.csrSignature[i]).getBytes(); |
| } |
| capture.csrSignature = csrSignature; |
| } |
| |
| // get oid |
| var oid = asn1.derToOid(capture.publicKeyOid); |
| if(oid !== pki.oids.rsaEncryption) { |
| throw new Error('Cannot read public key. OID is not RSA.'); |
| } |
| |
| // create certification request |
| var csr = pki.createCertificationRequest(); |
| csr.version = capture.csrVersion ? capture.csrVersion.charCodeAt(0) : 0; |
| csr.signatureOid = forge.asn1.derToOid(capture.csrSignatureOid); |
| csr.signatureParameters = _readSignatureParameters( |
| csr.signatureOid, capture.csrSignatureParams, true); |
| csr.siginfo.algorithmOid = forge.asn1.derToOid(capture.csrSignatureOid); |
| csr.siginfo.parameters = _readSignatureParameters( |
| csr.siginfo.algorithmOid, capture.csrSignatureParams, false); |
| // skip "unused bits" in signature value BITSTRING |
| var signature = forge.util.createBuffer(capture.csrSignature); |
| ++signature.read; |
| csr.signature = signature.getBytes(); |
| |
| // keep CertificationRequestInfo to preserve signature when exporting |
| csr.certificationRequestInfo = capture.certificationRequestInfo; |
| |
| if(computeHash) { |
| // check signature OID for supported signature types |
| csr.md = null; |
| if(csr.signatureOid in oids) { |
| var oid = oids[csr.signatureOid]; |
| switch(oid) { |
| case 'sha1WithRSAEncryption': |
| csr.md = forge.md.sha1.create(); |
| break; |
| case 'md5WithRSAEncryption': |
| csr.md = forge.md.md5.create(); |
| break; |
| case 'sha256WithRSAEncryption': |
| csr.md = forge.md.sha256.create(); |
| break; |
| case 'RSASSA-PSS': |
| csr.md = forge.md.sha256.create(); |
| break; |
| } |
| } |
| if(csr.md === null) { |
| var error = new Error('Could not compute certification request digest. ' + |
| 'Unknown signature OID.'); |
| error.signatureOid = csr.signatureOid; |
| throw error; |
| } |
| |
| // produce DER formatted CertificationRequestInfo and digest it |
| var bytes = asn1.toDer(csr.certificationRequestInfo); |
| csr.md.update(bytes.getBytes()); |
| } |
| |
| // handle subject, build subject message digest |
| var smd = forge.md.sha1.create(); |
| csr.subject.getField = function(sn) { |
| return _getAttribute(csr.subject, sn); |
| }; |
| csr.subject.addField = function(attr) { |
| _fillMissingFields([attr]); |
| csr.subject.attributes.push(attr); |
| }; |
| csr.subject.attributes = pki.RDNAttributesAsArray( |
| capture.certificationRequestInfoSubject, smd); |
| csr.subject.hash = smd.digest().toHex(); |
| |
| // convert RSA public key from ASN.1 |
| csr.publicKey = pki.publicKeyFromAsn1(capture.subjectPublicKeyInfo); |
| |
| // convert attributes from ASN.1 |
| csr.getAttribute = function(sn) { |
| return _getAttribute(csr, sn); |
| }; |
| csr.addAttribute = function(attr) { |
| _fillMissingFields([attr]); |
| csr.attributes.push(attr); |
| }; |
| csr.attributes = pki.CRIAttributesAsArray( |
| capture.certificationRequestInfoAttributes || []); |
| |
| return csr; |
| }; |
| |
| /** |
| * Creates an empty certification request (a CSR or certificate signing |
| * request). Once created, its public key and attributes can be set and then |
| * it can be signed. |
| * |
| * @return the empty certification request. |
| */ |
| pki.createCertificationRequest = function() { |
| var csr = {}; |
| csr.version = 0x00; |
| csr.signatureOid = null; |
| csr.signature = null; |
| csr.siginfo = {}; |
| csr.siginfo.algorithmOid = null; |
| |
| csr.subject = {}; |
| csr.subject.getField = function(sn) { |
| return _getAttribute(csr.subject, sn); |
| }; |
| csr.subject.addField = function(attr) { |
| _fillMissingFields([attr]); |
| csr.subject.attributes.push(attr); |
| }; |
| csr.subject.attributes = []; |
| csr.subject.hash = null; |
| |
| csr.publicKey = null; |
| csr.attributes = []; |
| csr.getAttribute = function(sn) { |
| return _getAttribute(csr, sn); |
| }; |
| csr.addAttribute = function(attr) { |
| _fillMissingFields([attr]); |
| csr.attributes.push(attr); |
| }; |
| csr.md = null; |
| |
| /** |
| * Sets the subject of this certification request. |
| * |
| * @param attrs the array of subject attributes to use. |
| */ |
| csr.setSubject = function(attrs) { |
| // set new attributes |
| _fillMissingFields(attrs); |
| csr.subject.attributes = attrs; |
| csr.subject.hash = null; |
| }; |
| |
| /** |
| * Sets the attributes of this certification request. |
| * |
| * @param attrs the array of attributes to use. |
| */ |
| csr.setAttributes = function(attrs) { |
| // set new attributes |
| _fillMissingFields(attrs); |
| csr.attributes = attrs; |
| }; |
| |
| /** |
| * Signs this certification request using the given private key. |
| * |
| * @param key the private key to sign with. |
| * @param md the message digest object to use (defaults to forge.md.sha1). |
| */ |
| csr.sign = function(key, md) { |
| // TODO: get signature OID from private key |
| csr.md = md || forge.md.sha1.create(); |
| var algorithmOid = oids[csr.md.algorithm + 'WithRSAEncryption']; |
| if(!algorithmOid) { |
| var error = new Error('Could not compute certification request digest. ' + |
| 'Unknown message digest algorithm OID.'); |
| error.algorithm = csr.md.algorithm; |
| throw error; |
| } |
| csr.signatureOid = csr.siginfo.algorithmOid = algorithmOid; |
| |
| // get CertificationRequestInfo, convert to DER |
| csr.certificationRequestInfo = pki.getCertificationRequestInfo(csr); |
| var bytes = asn1.toDer(csr.certificationRequestInfo); |
| |
| // digest and sign |
| csr.md.update(bytes.getBytes()); |
| csr.signature = key.sign(csr.md); |
| }; |
| |
| /** |
| * Attempts verify the signature on the passed certification request using |
| * its public key. |
| * |
| * A CSR that has been exported to a file in PEM format can be verified using |
| * OpenSSL using this command: |
| * |
| * openssl req -in <the-csr-pem-file> -verify -noout -text |
| * |
| * @return true if verified, false if not. |
| */ |
| csr.verify = function() { |
| var rval = false; |
| |
| var md = csr.md; |
| if(md === null) { |
| // check signature OID for supported signature types |
| if(csr.signatureOid in oids) { |
| var oid = oids[csr.signatureOid]; |
| switch(oid) { |
| case 'sha1WithRSAEncryption': |
| md = forge.md.sha1.create(); |
| break; |
| case 'md5WithRSAEncryption': |
| md = forge.md.md5.create(); |
| break; |
| case 'sha256WithRSAEncryption': |
| md = forge.md.sha256.create(); |
| break; |
| case 'RSASSA-PSS': |
| md = forge.md.sha256.create(); |
| break; |
| } |
| } |
| if(md === null) { |
| var error = new Error('Could not compute certification request digest. ' + |
| 'Unknown signature OID.'); |
| error.signatureOid = csr.signatureOid; |
| throw error; |
| } |
| |
| // produce DER formatted CertificationRequestInfo and digest it |
| var cri = csr.certificationRequestInfo || |
| pki.getCertificationRequestInfo(csr); |
| var bytes = asn1.toDer(cri); |
| md.update(bytes.getBytes()); |
| } |
| |
| if(md !== null) { |
| var scheme; |
| |
| switch(csr.signatureOid) { |
| case oids.sha1WithRSAEncryption: |
| /* use PKCS#1 v1.5 padding scheme */ |
| break; |
| case oids['RSASSA-PSS']: |
| var hash, mgf; |
| |
| /* initialize mgf */ |
| hash = oids[csr.signatureParameters.mgf.hash.algorithmOid]; |
| if(hash === undefined || forge.md[hash] === undefined) { |
| var error = new Error('Unsupported MGF hash function.'); |
| error.oid = csr.signatureParameters.mgf.hash.algorithmOid; |
| error.name = hash; |
| throw error; |
| } |
| |
| mgf = oids[csr.signatureParameters.mgf.algorithmOid]; |
| if(mgf === undefined || forge.mgf[mgf] === undefined) { |
| var error = new Error('Unsupported MGF function.'); |
| error.oid = csr.signatureParameters.mgf.algorithmOid; |
| error.name = mgf; |
| throw error; |
| } |
| |
| mgf = forge.mgf[mgf].create(forge.md[hash].create()); |
| |
| /* initialize hash function */ |
| hash = oids[csr.signatureParameters.hash.algorithmOid]; |
| if(hash === undefined || forge.md[hash] === undefined) { |
| var error = new Error('Unsupported RSASSA-PSS hash function.'); |
| error.oid = csr.signatureParameters.hash.algorithmOid; |
| error.name = hash; |
| throw error; |
| } |
| |
| scheme = forge.pss.create(forge.md[hash].create(), mgf, |
| csr.signatureParameters.saltLength); |
| break; |
| } |
| |
| // verify signature on csr using its public key |
| rval = csr.publicKey.verify( |
| md.digest().getBytes(), csr.signature, scheme); |
| } |
| |
| return rval; |
| }; |
| |
| return csr; |
| }; |
| |
| /** |
| * Converts an X.509 subject or issuer to an ASN.1 RDNSequence. |
| * |
| * @param obj the subject or issuer (distinguished name). |
| * |
| * @return the ASN.1 RDNSequence. |
| */ |
| function _dnToAsn1(obj) { |
| // create an empty RDNSequence |
| var rval = asn1.create( |
| asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, []); |
| |
| // iterate over attributes |
| var attr, set; |
| var attrs = obj.attributes; |
| for(var i = 0; i < attrs.length; ++i) { |
| attr = attrs[i]; |
| var value = attr.value; |
| |
| // reuse tag class for attribute value if available |
| var valueTagClass = asn1.Type.PRINTABLESTRING; |
| if('valueTagClass' in attr) { |
| valueTagClass = attr.valueTagClass; |
| |
| if(valueTagClass === asn1.Type.UTF8) { |
| value = forge.util.encodeUtf8(value); |
| } |
| // FIXME: handle more encodings |
| } |
| |
| // create a RelativeDistinguishedName set |
| // each value in the set is an AttributeTypeAndValue first |
| // containing the type (an OID) and second the value |
| set = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SET, true, [ |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| // AttributeType |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, |
| asn1.oidToDer(attr.type).getBytes()), |
| // AttributeValue |
| asn1.create(asn1.Class.UNIVERSAL, valueTagClass, false, value) |
| ]) |
| ]); |
| rval.value.push(set); |
| } |
| |
| return rval; |
| } |
| |
| /** |
| * Gets all printable attributes (typically of an issuer or subject) in a |
| * simplified JSON format for display. |
| * |
| * @param attrs the attributes. |
| * |
| * @return the JSON for display. |
| */ |
| function _getAttributesAsJson(attrs) { |
| var rval = {}; |
| for(var i = 0; i < attrs.length; ++i) { |
| var attr = attrs[i]; |
| if(attr.shortName && ( |
| attr.valueTagClass === asn1.Type.UTF8 || |
| attr.valueTagClass === asn1.Type.PRINTABLESTRING || |
| attr.valueTagClass === asn1.Type.IA5STRING)) { |
| var value = attr.value; |
| if(attr.valueTagClass === asn1.Type.UTF8) { |
| value = forge.util.encodeUtf8(attr.value); |
| } |
| if(!(attr.shortName in rval)) { |
| rval[attr.shortName] = value; |
| } else if(forge.util.isArray(rval[attr.shortName])) { |
| rval[attr.shortName].push(value); |
| } else { |
| rval[attr.shortName] = [rval[attr.shortName], value]; |
| } |
| } |
| } |
| return rval; |
| } |
| |
| /** |
| * Fills in missing fields in attributes. |
| * |
| * @param attrs the attributes to fill missing fields in. |
| */ |
| function _fillMissingFields(attrs) { |
| var attr; |
| for(var i = 0; i < attrs.length; ++i) { |
| attr = attrs[i]; |
| |
| // populate missing name |
| if(typeof attr.name === 'undefined') { |
| if(attr.type && attr.type in pki.oids) { |
| attr.name = pki.oids[attr.type]; |
| } else if(attr.shortName && attr.shortName in _shortNames) { |
| attr.name = pki.oids[_shortNames[attr.shortName]]; |
| } |
| } |
| |
| // populate missing type (OID) |
| if(typeof attr.type === 'undefined') { |
| if(attr.name && attr.name in pki.oids) { |
| attr.type = pki.oids[attr.name]; |
| } else { |
| var error = new Error('Attribute type not specified.'); |
| error.attribute = attr; |
| throw error; |
| } |
| } |
| |
| // populate missing shortname |
| if(typeof attr.shortName === 'undefined') { |
| if(attr.name && attr.name in _shortNames) { |
| attr.shortName = _shortNames[attr.name]; |
| } |
| } |
| |
| // convert extensions to value |
| if(attr.type === oids.extensionRequest) { |
| attr.valueConstructed = true; |
| attr.valueTagClass = asn1.Type.SEQUENCE; |
| if(!attr.value && attr.extensions) { |
| attr.value = []; |
| for(var ei = 0; ei < attr.extensions.length; ++ei) { |
| attr.value.push(pki.certificateExtensionToAsn1( |
| _fillMissingExtensionFields(attr.extensions[ei]))); |
| } |
| } |
| } |
| |
| if(typeof attr.value === 'undefined') { |
| var error = new Error('Attribute value not specified.'); |
| error.attribute = attr; |
| throw error; |
| } |
| } |
| } |
| |
| /** |
| * Fills in missing fields in certificate extensions. |
| * |
| * @param e the extension. |
| * @param [options] the options to use. |
| * [cert] the certificate the extensions are for. |
| * |
| * @return the extension. |
| */ |
| function _fillMissingExtensionFields(e, options) { |
| options = options || {}; |
| |
| // populate missing name |
| if(typeof e.name === 'undefined') { |
| if(e.id && e.id in pki.oids) { |
| e.name = pki.oids[e.id]; |
| } |
| } |
| |
| // populate missing id |
| if(typeof e.id === 'undefined') { |
| if(e.name && e.name in pki.oids) { |
| e.id = pki.oids[e.name]; |
| } else { |
| var error = new Error('Extension ID not specified.'); |
| error.extension = e; |
| throw error; |
| } |
| } |
| |
| if(typeof e.value !== 'undefined') { |
| return; |
| } |
| |
| // handle missing value: |
| |
| // value is a BIT STRING |
| if(e.name === 'keyUsage') { |
| // build flags |
| var unused = 0; |
| var b2 = 0x00; |
| var b3 = 0x00; |
| if(e.digitalSignature) { |
| b2 |= 0x80; |
| unused = 7; |
| } |
| if(e.nonRepudiation) { |
| b2 |= 0x40; |
| unused = 6; |
| } |
| if(e.keyEncipherment) { |
| b2 |= 0x20; |
| unused = 5; |
| } |
| if(e.dataEncipherment) { |
| b2 |= 0x10; |
| unused = 4; |
| } |
| if(e.keyAgreement) { |
| b2 |= 0x08; |
| unused = 3; |
| } |
| if(e.keyCertSign) { |
| b2 |= 0x04; |
| unused = 2; |
| } |
| if(e.cRLSign) { |
| b2 |= 0x02; |
| unused = 1; |
| } |
| if(e.encipherOnly) { |
| b2 |= 0x01; |
| unused = 0; |
| } |
| if(e.decipherOnly) { |
| b3 |= 0x80; |
| unused = 7; |
| } |
| |
| // create bit string |
| var value = String.fromCharCode(unused); |
| if(b3 !== 0) { |
| value += String.fromCharCode(b2) + String.fromCharCode(b3); |
| } else if(b2 !== 0) { |
| value += String.fromCharCode(b2); |
| } |
| e.value = asn1.create( |
| asn1.Class.UNIVERSAL, asn1.Type.BITSTRING, false, value); |
| } else if(e.name === 'basicConstraints') { |
| // basicConstraints is a SEQUENCE |
| e.value = asn1.create( |
| asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, []); |
| // cA BOOLEAN flag defaults to false |
| if(e.cA) { |
| e.value.value.push(asn1.create( |
| asn1.Class.UNIVERSAL, asn1.Type.BOOLEAN, false, |
| String.fromCharCode(0xFF))); |
| } |
| if('pathLenConstraint' in e) { |
| e.value.value.push(asn1.create( |
| asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, |
| asn1.integerToDer(e.pathLenConstraint).getBytes())); |
| } |
| } else if(e.name === 'extKeyUsage') { |
| // extKeyUsage is a SEQUENCE of OIDs |
| e.value = asn1.create( |
| asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, []); |
| var seq = e.value.value; |
| for(var key in e) { |
| if(e[key] !== true) { |
| continue; |
| } |
| // key is name in OID map |
| if(key in oids) { |
| seq.push(asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, |
| false, asn1.oidToDer(oids[key]).getBytes())); |
| } else if(key.indexOf('.') !== -1) { |
| // assume key is an OID |
| seq.push(asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, |
| false, asn1.oidToDer(key).getBytes())); |
| } |
| } |
| } else if(e.name === 'nsCertType') { |
| // nsCertType is a BIT STRING |
| // build flags |
| var unused = 0; |
| var b2 = 0x00; |
| |
| if(e.client) { |
| b2 |= 0x80; |
| unused = 7; |
| } |
| if(e.server) { |
| b2 |= 0x40; |
| unused = 6; |
| } |
| if(e.email) { |
| b2 |= 0x20; |
| unused = 5; |
| } |
| if(e.objsign) { |
| b2 |= 0x10; |
| unused = 4; |
| } |
| if(e.reserved) { |
| b2 |= 0x08; |
| unused = 3; |
| } |
| if(e.sslCA) { |
| b2 |= 0x04; |
| unused = 2; |
| } |
| if(e.emailCA) { |
| b2 |= 0x02; |
| unused = 1; |
| } |
| if(e.objCA) { |
| b2 |= 0x01; |
| unused = 0; |
| } |
| |
| // create bit string |
| var value = String.fromCharCode(unused); |
| if(b2 !== 0) { |
| value += String.fromCharCode(b2); |
| } |
| e.value = asn1.create( |
| asn1.Class.UNIVERSAL, asn1.Type.BITSTRING, false, value); |
| } else if(e.name === 'subjectAltName' || e.name === 'issuerAltName') { |
| // SYNTAX SEQUENCE |
| e.value = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, []); |
| |
| var altName; |
| for(var n = 0; n < e.altNames.length; ++n) { |
| altName = e.altNames[n]; |
| var value = altName.value; |
| // handle IP |
| if(altName.type === 7 && altName.ip) { |
| value = forge.util.bytesFromIP(altName.ip); |
| if(value === null) { |
| var error = new Error( |
| 'Extension "ip" value is not a valid IPv4 or IPv6 address.'); |
| error.extension = e; |
| throw error; |
| } |
| } else if(altName.type === 8) { |
| // handle OID |
| if(altName.oid) { |
| value = asn1.oidToDer(asn1.oidToDer(altName.oid)); |
| } else { |
| // deprecated ... convert value to OID |
| value = asn1.oidToDer(value); |
| } |
| } |
| e.value.value.push(asn1.create( |
| asn1.Class.CONTEXT_SPECIFIC, altName.type, false, |
| value)); |
| } |
| } else if(e.name === 'subjectKeyIdentifier' && options.cert) { |
| var ski = options.cert.generateSubjectKeyIdentifier(); |
| e.subjectKeyIdentifier = ski.toHex(); |
| // OCTETSTRING w/digest |
| e.value = asn1.create( |
| asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, ski.getBytes()); |
| } |
| |
| // ensure value has been defined by now |
| if(typeof e.value === 'undefined') { |
| var error = new Error('Extension value not specified.'); |
| error.extension = e; |
| throw error; |
| } |
| |
| return e; |
| } |
| |
| /** |
| * Convert signature parameters object to ASN.1 |
| * |
| * @param {String} oid Signature algorithm OID |
| * @param params The signature parametrs object |
| * @return ASN.1 object representing signature parameters |
| */ |
| function _signatureParametersToAsn1(oid, params) { |
| switch(oid) { |
| case oids['RSASSA-PSS']: |
| var parts = []; |
| |
| if(params.hash.algorithmOid !== undefined) { |
| parts.push(asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, |
| asn1.oidToDer(params.hash.algorithmOid).getBytes()), |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.NULL, false, '') |
| ]) |
| ])); |
| } |
| |
| if(params.mgf.algorithmOid !== undefined) { |
| parts.push(asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [ |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, |
| asn1.oidToDer(params.mgf.algorithmOid).getBytes()), |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, |
| asn1.oidToDer(params.mgf.hash.algorithmOid).getBytes()), |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.NULL, false, '') |
| ]) |
| ]) |
| ])); |
| } |
| |
| if(params.saltLength !== undefined) { |
| parts.push(asn1.create(asn1.Class.CONTEXT_SPECIFIC, 2, true, [ |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, |
| asn1.integerToDer(params.saltLength).getBytes()) |
| ])); |
| } |
| |
| return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, parts); |
| |
| default: |
| return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.NULL, false, ''); |
| } |
| } |
| |
| /** |
| * Converts a certification request's attributes to an ASN.1 set of |
| * CRIAttributes. |
| * |
| * @param csr certification request. |
| * |
| * @return the ASN.1 set of CRIAttributes. |
| */ |
| function _CRIAttributesToAsn1(csr) { |
| // create an empty context-specific container |
| var rval = asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, []); |
| |
| // no attributes, return empty container |
| if(csr.attributes.length === 0) { |
| return rval; |
| } |
| |
| // each attribute has a sequence with a type and a set of values |
| var attrs = csr.attributes; |
| for(var i = 0; i < attrs.length; ++i) { |
| var attr = attrs[i]; |
| var value = attr.value; |
| |
| // reuse tag class for attribute value if available |
| var valueTagClass = asn1.Type.UTF8; |
| if('valueTagClass' in attr) { |
| valueTagClass = attr.valueTagClass; |
| } |
| if(valueTagClass === asn1.Type.UTF8) { |
| value = forge.util.encodeUtf8(value); |
| } |
| var valueConstructed = false; |
| if('valueConstructed' in attr) { |
| valueConstructed = attr.valueConstructed; |
| } |
| // FIXME: handle more encodings |
| |
| // create a RelativeDistinguishedName set |
| // each value in the set is an AttributeTypeAndValue first |
| // containing the type (an OID) and second the value |
| var seq = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| // AttributeType |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, |
| asn1.oidToDer(attr.type).getBytes()), |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SET, true, [ |
| // AttributeValue |
| asn1.create( |
| asn1.Class.UNIVERSAL, valueTagClass, valueConstructed, value) |
| ]) |
| ]); |
| rval.value.push(seq); |
| } |
| |
| return rval; |
| } |
| |
| /** |
| * Gets the ASN.1 TBSCertificate part of an X.509v3 certificate. |
| * |
| * @param cert the certificate. |
| * |
| * @return the asn1 TBSCertificate. |
| */ |
| pki.getTBSCertificate = function(cert) { |
| // TBSCertificate |
| var tbs = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| // version |
| asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ |
| // integer |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, |
| asn1.integerToDer(cert.version).getBytes()) |
| ]), |
| // serialNumber |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, |
| forge.util.hexToBytes(cert.serialNumber)), |
| // signature |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| // algorithm |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, |
| asn1.oidToDer(cert.siginfo.algorithmOid).getBytes()), |
| // parameters |
| _signatureParametersToAsn1( |
| cert.siginfo.algorithmOid, cert.siginfo.parameters) |
| ]), |
| // issuer |
| _dnToAsn1(cert.issuer), |
| // validity |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| // notBefore |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.UTCTIME, false, |
| asn1.dateToUtcTime(cert.validity.notBefore)), |
| // notAfter |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.UTCTIME, false, |
| asn1.dateToUtcTime(cert.validity.notAfter)) |
| ]), |
| // subject |
| _dnToAsn1(cert.subject), |
| // SubjectPublicKeyInfo |
| pki.publicKeyToAsn1(cert.publicKey) |
| ]); |
| |
| if(cert.issuer.uniqueId) { |
| // issuerUniqueID (optional) |
| tbs.value.push( |
| asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [ |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.BITSTRING, false, |
| String.fromCharCode(0x00) + |
| cert.issuer.uniqueId |
| ) |
| ]) |
| ); |
| } |
| if(cert.subject.uniqueId) { |
| // subjectUniqueID (optional) |
| tbs.value.push( |
| asn1.create(asn1.Class.CONTEXT_SPECIFIC, 2, true, [ |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.BITSTRING, false, |
| String.fromCharCode(0x00) + |
| cert.subject.uniqueId |
| ) |
| ]) |
| ); |
| } |
| |
| if(cert.extensions.length > 0) { |
| // extensions (optional) |
| tbs.value.push(pki.certificateExtensionsToAsn1(cert.extensions)); |
| } |
| |
| return tbs; |
| }; |
| |
| /** |
| * Gets the ASN.1 CertificationRequestInfo part of a |
| * PKCS#10 CertificationRequest. |
| * |
| * @param csr the certification request. |
| * |
| * @return the asn1 CertificationRequestInfo. |
| */ |
| pki.getCertificationRequestInfo = function(csr) { |
| // CertificationRequestInfo |
| var cri = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| // version |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, |
| asn1.integerToDer(csr.version).getBytes()), |
| // subject |
| _dnToAsn1(csr.subject), |
| // SubjectPublicKeyInfo |
| pki.publicKeyToAsn1(csr.publicKey), |
| // attributes |
| _CRIAttributesToAsn1(csr) |
| ]); |
| |
| return cri; |
| }; |
| |
| /** |
| * Converts a DistinguishedName (subject or issuer) to an ASN.1 object. |
| * |
| * @param dn the DistinguishedName. |
| * |
| * @return the asn1 representation of a DistinguishedName. |
| */ |
| pki.distinguishedNameToAsn1 = function(dn) { |
| return _dnToAsn1(dn); |
| }; |
| |
| /** |
| * Converts an X.509v3 RSA certificate to an ASN.1 object. |
| * |
| * @param cert the certificate. |
| * |
| * @return the asn1 representation of an X.509v3 RSA certificate. |
| */ |
| pki.certificateToAsn1 = function(cert) { |
| // prefer cached TBSCertificate over generating one |
| var tbsCertificate = cert.tbsCertificate || pki.getTBSCertificate(cert); |
| |
| // Certificate |
| return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| // TBSCertificate |
| tbsCertificate, |
| // AlgorithmIdentifier (signature algorithm) |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| // algorithm |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, |
| asn1.oidToDer(cert.signatureOid).getBytes()), |
| // parameters |
| _signatureParametersToAsn1(cert.signatureOid, cert.signatureParameters) |
| ]), |
| // SignatureValue |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.BITSTRING, false, |
| String.fromCharCode(0x00) + cert.signature) |
| ]); |
| }; |
| |
| /** |
| * Converts X.509v3 certificate extensions to ASN.1. |
| * |
| * @param exts the extensions to convert. |
| * |
| * @return the extensions in ASN.1 format. |
| */ |
| pki.certificateExtensionsToAsn1 = function(exts) { |
| // create top-level extension container |
| var rval = asn1.create(asn1.Class.CONTEXT_SPECIFIC, 3, true, []); |
| |
| // create extension sequence (stores a sequence for each extension) |
| var seq = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, []); |
| rval.value.push(seq); |
| |
| for(var i = 0; i < exts.length; ++i) { |
| seq.value.push(pki.certificateExtensionToAsn1(exts[i])); |
| } |
| |
| return rval; |
| }; |
| |
| /** |
| * Converts a single certificate extension to ASN.1. |
| * |
| * @param ext the extension to convert. |
| * |
| * @return the extension in ASN.1 format. |
| */ |
| pki.certificateExtensionToAsn1 = function(ext) { |
| // create a sequence for each extension |
| var extseq = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, []); |
| |
| // extnID (OID) |
| extseq.value.push(asn1.create( |
| asn1.Class.UNIVERSAL, asn1.Type.OID, false, |
| asn1.oidToDer(ext.id).getBytes())); |
| |
| // critical defaults to false |
| if(ext.critical) { |
| // critical BOOLEAN DEFAULT FALSE |
| extseq.value.push(asn1.create( |
| asn1.Class.UNIVERSAL, asn1.Type.BOOLEAN, false, |
| String.fromCharCode(0xFF))); |
| } |
| |
| var value = ext.value; |
| if(typeof ext.value !== 'string') { |
| // value is asn.1 |
| value = asn1.toDer(value).getBytes(); |
| } |
| |
| // extnValue (OCTET STRING) |
| extseq.value.push(asn1.create( |
| asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, value)); |
| |
| return extseq; |
| }; |
| |
| /** |
| * Converts a PKCS#10 certification request to an ASN.1 object. |
| * |
| * @param csr the certification request. |
| * |
| * @return the asn1 representation of a certification request. |
| */ |
| pki.certificationRequestToAsn1 = function(csr) { |
| // prefer cached CertificationRequestInfo over generating one |
| var cri = csr.certificationRequestInfo || |
| pki.getCertificationRequestInfo(csr); |
| |
| // Certificate |
| return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| // CertificationRequestInfo |
| cri, |
| // AlgorithmIdentifier (signature algorithm) |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| // algorithm |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, |
| asn1.oidToDer(csr.signatureOid).getBytes()), |
| // parameters |
| _signatureParametersToAsn1(csr.signatureOid, csr.signatureParameters) |
| ]), |
| // signature |
| asn1.create(asn1.Class.UNIVERSAL, asn1.Type.BITSTRING, false, |
| String.fromCharCode(0x00) + csr.signature) |
| ]); |
| }; |
| |
| /** |
| * Creates a CA store. |
| * |
| * @param certs an optional array of certificate objects or PEM-formatted |
| * certificate strings to add to the CA store. |
| * |
| * @return the CA store. |
| */ |
| pki.createCaStore = function(certs) { |
| // create CA store |
| var caStore = { |
| // stored certificates |
| certs: {} |
| }; |
| |
| /** |
| * Gets the certificate that issued the passed certificate or its |
| * 'parent'. |
| * |
| * @param cert the certificate to get the parent for. |
| * |
| * @return the parent certificate or null if none was found. |
| */ |
| caStore.getIssuer = function(cert) { |
| var rval = getBySubject(cert.issuer); |
| |
| // see if there are multiple matches |
| /*if(forge.util.isArray(rval)) { |
| // TODO: resolve multiple matches by checking |
| // authorityKey/subjectKey/issuerUniqueID/other identifiers, etc. |
| // FIXME: or alternatively do authority key mapping |
| // if possible (X.509v1 certs can't work?) |
| throw new Error('Resolving multiple issuer matches not implemented yet.'); |
| }*/ |
| |
| return rval; |
| }; |
| |
| /** |
| * Adds a trusted certificate to the store. |
| * |
| * @param cert the certificate to add as a trusted certificate (either a |
| * pki.certificate object or a PEM-formatted certificate). |
| */ |
| caStore.addCertificate = function(cert) { |
| // convert from pem if necessary |
| if(typeof cert === 'string') { |
| cert = forge.pki.certificateFromPem(cert); |
| } |
| |
| // produce subject hash if it doesn't exist |
| if(!cert.subject.hash) { |
| var md = forge.md.sha1.create(); |
| cert.subject.attributes = pki.RDNAttributesAsArray( |
| _dnToAsn1(cert.subject), md); |
| cert.subject.hash = md.digest().toHex(); |
| } |
| |
| if(cert.subject.hash in caStore.certs) { |
| // subject hash already exists, append to array |
| var tmp = caStore.certs[cert.subject.hash]; |
| if(!forge.util.isArray(tmp)) { |
| tmp = [tmp]; |
| } |
| tmp.push(cert); |
| } else { |
| caStore.certs[cert.subject.hash] = cert; |
| } |
| }; |
| |
| /** |
| * Checks to see if the given certificate is in the store. |
| * |
| * @param cert the certificate to check. |
| * |
| * @return true if the certificate is in the store, false if not. |
| */ |
| caStore.hasCertificate = function(cert) { |
| var match = getBySubject(cert.subject); |
| if(!match) { |
| return false; |
| } |
| if(!forge.util.isArray(match)) { |
| match = [match]; |
| } |
| // compare DER-encoding of certificates |
| var der1 = asn1.toDer(pki.certificateToAsn1(cert)).getBytes(); |
| for(var i = 0; i < match.length; ++i) { |
| var der2 = asn1.toDer(pki.certificateToAsn1(match[i])).getBytes(); |
| if(der1 === der2) { |
| return true; |
| } |
| } |
| return false; |
| }; |
| |
| function getBySubject(subject) { |
| // produce subject hash if it doesn't exist |
| if(!subject.hash) { |
| var md = forge.md.sha1.create(); |
| subject.attributes = pki.RDNAttributesAsArray(_dnToAsn1(subject), md); |
| subject.hash = md.digest().toHex(); |
| } |
| return caStore.certs[subject.hash] || null; |
| } |
| |
| // auto-add passed in certs |
| if(certs) { |
| // parse PEM-formatted certificates as necessary |
| for(var i = 0; i < certs.length; ++i) { |
| var cert = certs[i]; |
| caStore.addCertificate(cert); |
| } |
| } |
| |
| return caStore; |
| }; |
| |
| /** |
| * Certificate verification errors, based on TLS. |
| */ |
| pki.certificateError = { |
| bad_certificate: 'forge.pki.BadCertificate', |
| unsupported_certificate: 'forge.pki.UnsupportedCertificate', |
| certificate_revoked: 'forge.pki.CertificateRevoked', |
| certificate_expired: 'forge.pki.CertificateExpired', |
| certificate_unknown: 'forge.pki.CertificateUnknown', |
| unknown_ca: 'forge.pki.UnknownCertificateAuthority' |
| }; |
| |
| /** |
| * Verifies a certificate chain against the given Certificate Authority store |
| * with an optional custom verify callback. |
| * |
| * @param caStore a certificate store to verify against. |
| * @param chain the certificate chain to verify, with the root or highest |
| * authority at the end (an array of certificates). |
| * @param verify called for every certificate in the chain. |
| * |
| * The verify callback has the following signature: |
| * |
| * verified - Set to true if certificate was verified, otherwise the |
| * pki.certificateError for why the certificate failed. |
| * depth - The current index in the chain, where 0 is the end point's cert. |
| * certs - The certificate chain, *NOTE* an empty chain indicates an anonymous |
| * end point. |
| * |
| * The function returns true on success and on failure either the appropriate |
| * pki.certificateError or an object with 'error' set to the appropriate |
| * pki.certificateError and 'message' set to a custom error message. |
| * |
| * @return true if successful, error thrown if not. |
| */ |
| pki.verifyCertificateChain = function(caStore, chain, verify) { |
| /* From: RFC3280 - Internet X.509 Public Key Infrastructure Certificate |
| Section 6: Certification Path Validation |
| See inline parentheticals related to this particular implementation. |
| |
| The primary goal of path validation is to verify the binding between |
| a subject distinguished name or a subject alternative name and subject |
| public key, as represented in the end entity certificate, based on the |
| public key of the trust anchor. This requires obtaining a sequence of |
| certificates that support that binding. That sequence should be provided |
| in the passed 'chain'. The trust anchor should be in the given CA |
| store. The 'end entity' certificate is the certificate provided by the |
| end point (typically a server) and is the first in the chain. |
| |
| To meet this goal, the path validation process verifies, among other |
| things, that a prospective certification path (a sequence of n |
| certificates or a 'chain') satisfies the following conditions: |
| |
| (a) for all x in {1, ..., n-1}, the subject of certificate x is |
| the issuer of certificate x+1; |
| |
| (b) certificate 1 is issued by the trust anchor; |
| |
| (c) certificate n is the certificate to be validated; and |
| |
| (d) for all x in {1, ..., n}, the certificate was valid at the |
| time in question. |
| |
| Note that here 'n' is index 0 in the chain and 1 is the last certificate |
| in the chain and it must be signed by a certificate in the connection's |
| CA store. |
| |
| The path validation process also determines the set of certificate |
| policies that are valid for this path, based on the certificate policies |
| extension, policy mapping extension, policy constraints extension, and |
| inhibit any-policy extension. |
| |
| Note: Policy mapping extension not supported (Not Required). |
| |
| Note: If the certificate has an unsupported critical extension, then it |
| must be rejected. |
| |
| Note: A certificate is self-issued if the DNs that appear in the subject |
| and issuer fields are identical and are not empty. |
| |
| The path validation algorithm assumes the following seven inputs are |
| provided to the path processing logic. What this specific implementation |
| will use is provided parenthetically: |
| |
| (a) a prospective certification path of length n (the 'chain') |
| (b) the current date/time: ('now'). |
| (c) user-initial-policy-set: A set of certificate policy identifiers |
| naming the policies that are acceptable to the certificate user. |
| The user-initial-policy-set contains the special value any-policy |
| if the user is not concerned about certificate policy |
| (Not implemented. Any policy is accepted). |
| (d) trust anchor information, describing a CA that serves as a trust |
| anchor for the certification path. The trust anchor information |
| includes: |
| |
| (1) the trusted issuer name, |
| (2) the trusted public key algorithm, |
| (3) the trusted public key, and |
| (4) optionally, the trusted public key parameters associated |
| with the public key. |
| |
| (Trust anchors are provided via certificates in the CA store). |
| |
| The trust anchor information may be provided to the path processing |
| procedure in the form of a self-signed certificate. The trusted anchor |
| information is trusted because it was delivered to the path processing |
| procedure by some trustworthy out-of-band procedure. If the trusted |
| public key algorithm requires parameters, then the parameters are |
| provided along with the trusted public key (No parameters used in this |
| implementation). |
| |
| (e) initial-policy-mapping-inhibit, which indicates if policy mapping is |
| allowed in the certification path. |
| (Not implemented, no policy checking) |
| |
| (f) initial-explicit-policy, which indicates if the path must be valid |
| for at least one of the certificate policies in the user-initial- |
| policy-set. |
| (Not implemented, no policy checking) |
| |
| (g) initial-any-policy-inhibit, which indicates whether the |
| anyPolicy OID should be processed if it is included in a |
| certificate. |
| (Not implemented, so any policy is valid provided that it is |
| not marked as critical) */ |
| |
| /* Basic Path Processing: |
| |
| For each certificate in the 'chain', the following is checked: |
| |
| 1. The certificate validity period includes the current time. |
| 2. The certificate was signed by its parent (where the parent is either |
| the next in the chain or from the CA store). Allow processing to |
| continue to the next step if no parent is found but the certificate is |
| in the CA store. |
| 3. TODO: The certificate has not been revoked. |
| 4. The certificate issuer name matches the parent's subject name. |
| 5. TODO: If the certificate is self-issued and not the final certificate |
| in the chain, skip this step, otherwise verify that the subject name |
| is within one of the permitted subtrees of X.500 distinguished names |
| and that each of the alternative names in the subjectAltName extension |
| (critical or non-critical) is within one of the permitted subtrees for |
| that name type. |
| 6. TODO: If the certificate is self-issued and not the final certificate |
| in the chain, skip this step, otherwise verify that the subject name |
| is not within one of the excluded subtrees for X.500 distinguished |
| names and none of the subjectAltName extension names are excluded for |
| that name type. |
| 7. The other steps in the algorithm for basic path processing involve |
| handling the policy extension which is not presently supported in this |
| implementation. Instead, if a critical policy extension is found, the |
| certificate is rejected as not supported. |
| 8. If the certificate is not the first or if its the only certificate in |
| the chain (having no parent from the CA store or is self-signed) and it |
| has a critical key usage extension, verify that the keyCertSign bit is |
| set. If the key usage extension exists, verify that the basic |
| constraints extension exists. If the basic constraints extension exists, |
| verify that the cA flag is set. If pathLenConstraint is set, ensure that |
| the number of certificates that precede in the chain (come earlier |
| in the chain as implemented below), excluding the very first in the |
| chain (typically the end-entity one), isn't greater than the |
| pathLenConstraint. This constraint limits the number of intermediate |
| CAs that may appear below a CA before only end-entity certificates |
| may be issued. */ |
| |
| // copy cert chain references to another array to protect against changes |
| // in verify callback |
| chain = chain.slice(0); |
| var certs = chain.slice(0); |
| |
| // get current date |
| var now = new Date(); |
| |
| // verify each cert in the chain using its parent, where the parent |
| // is either the next in the chain or from the CA store |
| var first = true; |
| var error = null; |
| var depth = 0; |
| do { |
| var cert = chain.shift(); |
| var parent = null; |
| var selfSigned = false; |
| |
| // 1. check valid time |
| if(now < cert.validity.notBefore || now > cert.validity.notAfter) { |
| error = { |
| message: 'Certificate is not valid yet or has expired.', |
| error: pki.certificateError.certificate_expired, |
| notBefore: cert.validity.notBefore, |
| notAfter: cert.validity.notAfter, |
| now: now |
| }; |
| } |
| |
| // 2. verify with parent from chain or CA store |
| if(error === null) { |
| parent = chain[0] || caStore.getIssuer(cert); |
| if(parent === null) { |
| // check for self-signed cert |
| if(cert.isIssuer(cert)) { |
| selfSigned = true; |
| parent = cert; |
| } |
| } |
| |
| if(parent) { |
| // FIXME: current CA store implementation might have multiple |
| // certificates where the issuer can't be determined from the |
| // certificate (happens rarely with, eg: old certificates) so normalize |
| // by always putting parents into an array |
| // TODO: there's may be an extreme degenerate case currently uncovered |
| // where an old intermediate certificate seems to have a matching parent |
| // but none of the parents actually verify ... but the intermediate |
| // is in the CA and it should pass this check; needs investigation |
| var parents = parent; |
| if(!forge.util.isArray(parents)) { |
| parents = [parents]; |
| } |
| |
| // try to verify with each possible parent (typically only one) |
| var verified = false; |
| while(!verified && parents.length > 0) { |
| parent = parents.shift(); |
| try { |
| verified = parent.verify(cert); |
| } catch(ex) { |
| // failure to verify, don't care why, try next one |
| } |
| } |
| |
| if(!verified) { |
| error = { |
| message: 'Certificate signature is invalid.', |
| error: pki.certificateError.bad_certificate |
| }; |
| } |
| } |
| |
| if(error === null && (!parent || selfSigned) && |
| !caStore.hasCertificate(cert)) { |
| // no parent issuer and certificate itself is not trusted |
| error = { |
| message: 'Certificate is not trusted.', |
| error: pki.certificateError.unknown_ca |
| }; |
| } |
| } |
| |
| // TODO: 3. check revoked |
| |
| // 4. check for matching issuer/subject |
| if(error === null && parent && !cert.isIssuer(parent)) { |
| // parent is not issuer |
| error = { |
| message: 'Certificate issuer is invalid.', |
| error: pki.certificateError.bad_certificate |
| }; |
| } |
| |
| // 5. TODO: check names with permitted names tree |
| |
| // 6. TODO: check names against excluded names tree |
| |
| // 7. check for unsupported critical extensions |
| if(error === null) { |
| // supported extensions |
| var se = { |
| keyUsage: true, |
| basicConstraints: true |
| }; |
| for(var i = 0; error === null && i < cert.extensions.length; ++i) { |
| var ext = cert.extensions[i]; |
| if(ext.critical && !(ext.name in se)) { |
| error = { |
| message: |
| 'Certificate has an unsupported critical extension.', |
| error: pki.certificateError.unsupported_certificate |
| }; |
| } |
| } |
| } |
| |
| // 8. check for CA if cert is not first or is the only certificate |
| // remaining in chain with no parent or is self-signed |
| if(error === null && |
| (!first || (chain.length === 0 && (!parent || selfSigned)))) { |
| // first check keyUsage extension and then basic constraints |
| var bcExt = cert.getExtension('basicConstraints'); |
| var keyUsageExt = cert.getExtension('keyUsage'); |
| if(keyUsageExt !== null) { |
| // keyCertSign must be true and there must be a basic |
| // constraints extension |
| if(!keyUsageExt.keyCertSign || bcExt === null) { |
| // bad certificate |
| error = { |
| message: |
| 'Certificate keyUsage or basicConstraints conflict ' + |
| 'or indicate that the certificate is not a CA. ' + |
| 'If the certificate is the only one in the chain or ' + |
| 'isn\'t the first then the certificate must be a ' + |
| 'valid CA.', |
| error: pki.certificateError.bad_certificate |
| }; |
| } |
| } |
| // basic constraints cA flag must be set |
| if(error === null && bcExt !== null && !bcExt.cA) { |
| // bad certificate |
| error = { |
| message: |
| 'Certificate basicConstraints indicates the certificate ' + |
| 'is not a CA.', |
| error: pki.certificateError.bad_certificate |
| }; |
| } |
| // if error is not null and keyUsage is available, then we know it |
| // has keyCertSign and there is a basic constraints extension too, |
| // which means we can check pathLenConstraint (if it exists) |
| if(error === null && keyUsageExt !== null && |
| 'pathLenConstraint' in bcExt) { |
| // pathLen is the maximum # of intermediate CA certs that can be |
| // found between the current certificate and the end-entity (depth 0) |
| // certificate; this number does not include the end-entity (depth 0, |
| // last in the chain) even if it happens to be a CA certificate itself |
| var pathLen = depth - 1; |
| if(pathLen > bcExt.pathLenConstraint) { |
| // pathLenConstraint violated, bad certificate |
| error = { |
| message: |
| 'Certificate basicConstraints pathLenConstraint violated.', |
| error: pki.certificateError.bad_certificate |
| }; |
| } |
| } |
| } |
| |
| // call application callback |
| var vfd = (error === null) ? true : error.error; |
| var ret = verify ? verify(vfd, depth, certs) : vfd; |
| if(ret === true) { |
| // clear any set error |
| error = null; |
| } else { |
| // if passed basic tests, set default message and alert |
| if(vfd === true) { |
| error = { |
| message: 'The application rejected the certificate.', |
| error: pki.certificateError.bad_certificate |
| }; |
| } |
| |
| // check for custom error info |
| if(ret || ret === 0) { |
| // set custom message and error |
| if(typeof ret === 'object' && !forge.util.isArray(ret)) { |
| if(ret.message) { |
| error.message = ret.message; |
| } |
| if(ret.error) { |
| error.error = ret.error; |
| } |
| } else if(typeof ret === 'string') { |
| // set custom error |
| error.error = ret; |
| } |
| } |
| |
| // throw error |
| throw error; |
| } |
| |
| // no longer first cert in chain |
| first = false; |
| ++depth; |
| } while(chain.length > 0); |
| |
| return true; |
| }; |
| |
| } // end module implementation |
| |
| /* ########## Begin module wrapper ########## */ |
| var name = 'x509'; |
| if(typeof define !== 'function') { |
| // NodeJS -> AMD |
| if(typeof module === 'object' && module.exports) { |
| var nodeJS = true; |
| define = function(ids, factory) { |
| factory(require, module); |
| }; |
| } else { |
| // <script> |
| if(typeof forge === 'undefined') { |
| forge = {}; |
| } |
| return initModule(forge); |
| } |
| } |
| // AMD |
| var deps; |
| var defineFunc = function(require, module) { |
| module.exports = function(forge) { |
| var mods = deps.map(function(dep) { |
| return require(dep); |
| }).concat(initModule); |
| // handle circular dependencies |
| forge = forge || {}; |
| forge.defined = forge.defined || {}; |
| if(forge.defined[name]) { |
| return forge[name]; |
| } |
| forge.defined[name] = true; |
| for(var i = 0; i < mods.length; ++i) { |
| mods[i](forge); |
| } |
| return forge.pki; |
| }; |
| }; |
| var tmpDefine = define; |
| define = function(ids, factory) { |
| deps = (typeof ids === 'string') ? factory.slice(2) : ids.slice(2); |
| if(nodeJS) { |
| delete define; |
| return tmpDefine.apply(null, Array.prototype.slice.call(arguments, 0)); |
| } |
| define = tmpDefine; |
| return define.apply(null, Array.prototype.slice.call(arguments, 0)); |
| }; |
| define([ |
| 'require', |
| 'module', |
| './aes', |
| './asn1', |
| './des', |
| './md', |
| './mgf', |
| './oids', |
| './pem', |
| './pss', |
| './rsa', |
| './util' |
| ], function() { |
| defineFunc.apply(null, Array.prototype.slice.call(arguments, 0)); |
| }); |
| })(); |