Spaces:
Configuration error
Configuration error
/** | |
* 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 | |
* } | |
*/ | |
var forge = require('./forge'); | |
require('./aes'); | |
require('./asn1'); | |
require('./des'); | |
require('./md'); | |
require('./mgf'); | |
require('./oids'); | |
require('./pem'); | |
require('./pss'); | |
require('./rsa'); | |
require('./util'); | |
// shortcut for asn.1 API | |
var asn1 = forge.asn1; | |
/* Public Key Infrastructure (PKI) implementation. */ | |
var pki = module.exports = 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, | |
// TODO: support arbitrary bit length ids | |
captureBitStringValue: '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, | |
// TODO: support arbitrary bit length ids | |
captureBitStringValue: '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, | |
captureBitStringValue: '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, | |
captureBitStringValue: '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; | |
}; | |
/** | |
* Create signature digest for OID. | |
* | |
* @param options | |
* signatureOid: the OID specifying the signature algorithm. | |
* type: a human readable type for error messages | |
* @return a created md instance. throws if unknown oid. | |
*/ | |
var _createSignatureDigest = function(options) { | |
switch(oids[options.signatureOid]) { | |
case 'sha1WithRSAEncryption': | |
// deprecated alias | |
case 'sha1WithRSASignature': | |
return forge.md.sha1.create(); | |
case 'md5WithRSAEncryption': | |
return forge.md.md5.create(); | |
case 'sha256WithRSAEncryption': | |
return forge.md.sha256.create(); | |
case 'sha384WithRSAEncryption': | |
return forge.md.sha384.create(); | |
case 'sha512WithRSAEncryption': | |
return forge.md.sha512.create(); | |
case 'RSASSA-PSS': | |
return forge.md.sha256.create(); | |
default: | |
var error = new Error( | |
'Could not compute ' + options.type + ' digest. ' + | |
'Unknown signature OID.'); | |
error.signatureOid = options.signatureOid; | |
throw error; | |
} | |
}; | |
/** | |
* Verify signature on certificate or CSR. | |
* | |
* @param options: | |
* certificate the certificate or CSR to verify. | |
* md the signature digest. | |
* signature the signature | |
* @return a created md instance. throws if unknown oid. | |
*/ | |
var _verifySignature = function(options) { | |
var cert = options.certificate; | |
var scheme; | |
switch(cert.signatureOid) { | |
case oids.sha1WithRSAEncryption: | |
// deprecated alias | |
case oids.sha1WithRSASignature: | |
/* use PKCS#1 v1.5 padding scheme */ | |
break; | |
case oids['RSASSA-PSS']: | |
var hash, mgf; | |
/* initialize mgf */ | |
hash = oids[cert.signatureParameters.mgf.hash.algorithmOid]; | |
if(hash === undefined || forge.md[hash] === undefined) { | |
var error = new Error('Unsupported MGF hash function.'); | |
error.oid = cert.signatureParameters.mgf.hash.algorithmOid; | |
error.name = hash; | |
throw error; | |
} | |
mgf = oids[cert.signatureParameters.mgf.algorithmOid]; | |
if(mgf === undefined || forge.mgf[mgf] === undefined) { | |
var error = new Error('Unsupported MGF function.'); | |
error.oid = cert.signatureParameters.mgf.algorithmOid; | |
error.name = mgf; | |
throw error; | |
} | |
mgf = forge.mgf[mgf].create(forge.md[hash].create()); | |
/* initialize hash function */ | |
hash = oids[cert.signatureParameters.hash.algorithmOid]; | |
if(hash === undefined || forge.md[hash] === undefined) { | |
var error = new Error('Unsupported RSASSA-PSS hash function.'); | |
error.oid = cert.signatureParameters.hash.algorithmOid; | |
error.name = hash; | |
throw error; | |
} | |
scheme = forge.pss.create( | |
forge.md[hash].create(), mgf, cert.signatureParameters.saltLength | |
); | |
break; | |
} | |
// verify signature on cert using public key | |
return cert.publicKey.verify( | |
options.md.digest().getBytes(), options.signature, scheme | |
); | |
}; | |
/** | |
* 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) { | |
// TODO: support arbitrary bit length ids | |
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) { | |
// TODO: support arbitrary bit length ids | |
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 = subject.attributes; | |
error.actualIssuer = issuer.attributes; | |
throw error; | |
} | |
var md = child.md; | |
if(md === null) { | |
// create digest for OID signature types | |
md = _createSignatureDigest({ | |
signatureOid: child.signatureOid, | |
type: 'certificate' | |
}); | |
// 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) { | |
rval = _verifySignature({ | |
certificate: cert, md: md, signature: child.signature | |
}); | |
} | |
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; | |
} | |
// 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); | |
cert.signature = capture.certSignature; | |
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) { | |
// create digest for OID signature type | |
cert.md = _createSignatureDigest({ | |
signatureOid: cert.signatureOid, | |
type: 'certificate' | |
}); | |
// 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(); | |
var ibytes = asn1.toDer(capture.certIssuer); | |
imd.update(ibytes.getBytes()); | |
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); | |
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(); | |
var sbytes = asn1.toDer(capture.certSubject); | |
smd.update(sbytes.getBytes()); | |
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); | |
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; | |
} | |
// 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); | |
csr.signature = capture.csrSignature; | |
// keep CertificationRequestInfo to preserve signature when exporting | |
csr.certificationRequestInfo = capture.certificationRequestInfo; | |
if(computeHash) { | |
// create digest for OID signature type | |
csr.md = _createSignatureDigest({ | |
signatureOid: csr.signatureOid, | |
type: 'certification request' | |
}); | |
// 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) { | |
md = _createSignatureDigest({ | |
signatureOid: csr.signatureOid, | |
type: 'certification request' | |
}); | |
// 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) { | |
rval = _verifySignature({ | |
certificate: csr, md: md, signature: csr.signature | |
}); | |
} | |
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 e; | |
} | |
// 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 === 'nsComment' && options.cert) { | |
// sanity check value is ASCII (req'd) and not too big | |
if(!(/^[\x00-\x7F]*$/.test(e.comment)) || | |
(e.comment.length < 1) || (e.comment.length > 128)) { | |
throw new Error('Invalid "nsComment" content.'); | |
} | |
// IA5STRING opaque comment | |
e.value = asn1.create( | |
asn1.Class.UNIVERSAL, asn1.Type.IA5STRING, false, e.comment); | |
} 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()); | |
} else if(e.name === 'authorityKeyIdentifier' && options.cert) { | |
// SYNTAX SEQUENCE | |
e.value = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, []); | |
var seq = e.value.value; | |
if(e.keyIdentifier) { | |
var keyIdentifier = (e.keyIdentifier === true ? | |
options.cert.generateSubjectKeyIdentifier().getBytes() : | |
e.keyIdentifier); | |
seq.push( | |
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, false, keyIdentifier)); | |
} | |
if(e.authorityCertIssuer) { | |
var authorityCertIssuer = [ | |
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 4, true, [ | |
_dnToAsn1(e.authorityCertIssuer === true ? | |
options.cert.issuer : e.authorityCertIssuer) | |
]) | |
]; | |
seq.push( | |
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, authorityCertIssuer)); | |
} | |
if(e.serialNumber) { | |
var serialNumber = forge.util.hexToBytes(e.serialNumber === true ? | |
options.cert.serialNumber : e.serialNumber); | |
seq.push( | |
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 2, false, serialNumber)); | |
} | |
} else if(e.name === 'cRLDistributionPoints') { | |
e.value = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, []); | |
var seq = e.value.value; | |
// Create sub SEQUENCE of DistributionPointName | |
var subSeq = asn1.create( | |
asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, []); | |
// Create fullName CHOICE | |
var fullNameGeneralNames = asn1.create( | |
asn1.Class.CONTEXT_SPECIFIC, 0, 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); | |
} | |
} | |
fullNameGeneralNames.value.push(asn1.create( | |
asn1.Class.CONTEXT_SPECIFIC, altName.type, false, | |
value)); | |
} | |
// Add to the parent SEQUENCE | |
subSeq.value.push(asn1.create( | |
asn1.Class.CONTEXT_SPECIFIC, 0, true, [fullNameGeneralNames])); | |
seq.push(subSeq); | |
} | |
// 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; | |
} | |
var jan_1_1950 = new Date('1950-01-01T00:00:00Z'); | |
var jan_1_2050 = new Date('2050-01-01T00:00:00Z'); | |
/** | |
* Converts a Date object to ASN.1 | |
* Handles the different format before and after 1st January 2050 | |
* | |
* @param date date object. | |
* | |
* @return the ASN.1 object representing the date. | |
*/ | |
function _dateToAsn1(date) { | |
if(date >= jan_1_1950 && date < jan_1_2050) { | |
return asn1.create( | |
asn1.Class.UNIVERSAL, asn1.Type.UTCTIME, false, | |
asn1.dateToUtcTime(date)); | |
} else { | |
return asn1.create( | |
asn1.Class.UNIVERSAL, asn1.Type.GENERALIZEDTIME, false, | |
asn1.dateToGeneralizedTime(date)); | |
} | |
} | |
/** | |
* 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 notBefore = _dateToAsn1(cert.validity.notBefore); | |
var notAfter = _dateToAsn1(cert.validity.notAfter); | |
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, | |
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, | |
// TODO: support arbitrary bit length ids | |
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, | |
// TODO: support arbitrary bit length ids | |
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); | |
} | |
ensureSubjectHasHash(cert.subject); | |
if(!caStore.hasCertificate(cert)) { // avoid duplicate certificates in store | |
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); | |
caStore.certs[cert.subject.hash] = tmp; | |
} else { | |
caStore.certs[cert.subject.hash] = cert; | |
} | |
} | |
}; | |
/** | |
* Checks to see if the given certificate is in the store. | |
* | |
* @param cert the certificate to check (either a pki.certificate or a | |
* PEM-formatted certificate). | |
* | |
* @return true if the certificate is in the store, false if not. | |
*/ | |
caStore.hasCertificate = function(cert) { | |
// convert from pem if necessary | |
if(typeof cert === 'string') { | |
cert = forge.pki.certificateFromPem(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; | |
}; | |
/** | |
* Lists all of the certificates kept in the store. | |
* | |
* @return an array of all of the pki.certificate objects in the store. | |
*/ | |
caStore.listAllCertificates = function() { | |
var certList = []; | |
for(var hash in caStore.certs) { | |
if(caStore.certs.hasOwnProperty(hash)) { | |
var value = caStore.certs[hash]; | |
if(!forge.util.isArray(value)) { | |
certList.push(value); | |
} else { | |
for(var i = 0; i < value.length; ++i) { | |
certList.push(value[i]); | |
} | |
} | |
} | |
} | |
return certList; | |
}; | |
/** | |
* Removes a certificate from the store. | |
* | |
* @param cert the certificate to remove (either a pki.certificate or a | |
* PEM-formatted certificate). | |
* | |
* @return the certificate that was removed or null if the certificate | |
* wasn't in store. | |
*/ | |
caStore.removeCertificate = function(cert) { | |
var result; | |
// convert from pem if necessary | |
if(typeof cert === 'string') { | |
cert = forge.pki.certificateFromPem(cert); | |
} | |
ensureSubjectHasHash(cert.subject); | |
if(!caStore.hasCertificate(cert)) { | |
return null; | |
} | |
var match = getBySubject(cert.subject); | |
if(!forge.util.isArray(match)) { | |
result = caStore.certs[cert.subject.hash]; | |
delete caStore.certs[cert.subject.hash]; | |
return result; | |
} | |
// 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) { | |
result = match[i]; | |
match.splice(i, 1); | |
} | |
} | |
if(match.length === 0) { | |
delete caStore.certs[cert.subject.hash]; | |
} | |
return result; | |
}; | |
function getBySubject(subject) { | |
ensureSubjectHasHash(subject); | |
return caStore.certs[subject.hash] || null; | |
} | |
function ensureSubjectHasHash(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(); | |
} | |
} | |
// 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 options a callback to be called for every certificate in the chain or | |
* an object with: | |
* verify a callback to be called for every certificate in the | |
* chain | |
* validityCheckDate the date against which the certificate | |
* validity period should be checked. Pass null to not check | |
* the validity period. By default, the current date is used. | |
* | |
* 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, options) { | |
/* 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. */ | |
// if a verify callback is passed as the third parameter, package it within | |
// the options object. This is to support a legacy function signature that | |
// expected the verify callback as the third parameter. | |
if(typeof options === 'function') { | |
options = {verify: options}; | |
} | |
options = options || {}; | |
// copy cert chain references to another array to protect against changes | |
// in verify callback | |
chain = chain.slice(0); | |
var certs = chain.slice(0); | |
var validityCheckDate = options.validityCheckDate; | |
// if no validityCheckDate is specified, default to the current date. Make | |
// sure to maintain the value null because it indicates that the validity | |
// period should not be checked. | |
if(typeof validityCheckDate === 'undefined') { | |
validityCheckDate = 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; | |
if(validityCheckDate) { | |
// 1. check valid time | |
if(validityCheckDate < cert.validity.notBefore || | |
validityCheckDate > 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, | |
// TODO: we might want to reconsider renaming 'now' to | |
// 'validityCheckDate' should this API be changed in the future. | |
now: validityCheckDate | |
}; | |
} | |
} | |
// 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 = options.verify ? options.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; | |
}; | |