sshpk
Advanced tools
Comparing version 1.10.2 to 1.11.0
@@ -42,2 +42,4 @@ // Copyright 2016 Joyent, Inc. | ||
assert.optionalArrayOfString(opts.purposes, 'options.purposes'); | ||
this._hashCache = {}; | ||
@@ -53,2 +55,3 @@ | ||
this.validUntil = opts.validUntil; | ||
this.purposes = opts.purposes; | ||
} | ||
@@ -113,2 +116,6 @@ | ||
return (false); | ||
if (this.issuer.purposes && this.issuer.purposes.length > 0 && | ||
this.issuer.purposes.indexOf('ca') === -1) { | ||
return (false); | ||
} | ||
@@ -186,2 +193,43 @@ return (this.isSignedByKey(issuerCert.subjectKey)); | ||
var purposes = options.purposes; | ||
if (purposes === undefined) | ||
purposes = []; | ||
if (purposes.indexOf('signature') === -1) | ||
purposes.push('signature'); | ||
/* Self-signed certs are always CAs. */ | ||
if (purposes.indexOf('ca') === -1) | ||
purposes.push('ca'); | ||
if (purposes.indexOf('crl') === -1) | ||
purposes.push('crl'); | ||
/* | ||
* If we weren't explicitly given any other purposes, do the sensible | ||
* thing and add some basic ones depending on the subject type. | ||
*/ | ||
if (purposes.length <= 3) { | ||
var hostSubjects = subjects.filter(function (subject) { | ||
return (subject.type === 'host'); | ||
}); | ||
var userSubjects = subjects.filter(function (subject) { | ||
return (subject.type === 'user'); | ||
}); | ||
if (hostSubjects.length > 0) { | ||
if (purposes.indexOf('serverAuth') === -1) | ||
purposes.push('serverAuth'); | ||
} | ||
if (userSubjects.length > 0) { | ||
if (purposes.indexOf('clientAuth') === -1) | ||
purposes.push('clientAuth'); | ||
} | ||
if (userSubjects.length > 0 || hostSubjects.length > 0) { | ||
if (purposes.indexOf('keyAgreement') === -1) | ||
purposes.push('keyAgreement'); | ||
if (key.type === 'rsa' && | ||
purposes.indexOf('encryption') === -1) | ||
purposes.push('encryption'); | ||
} | ||
} | ||
var cert = new Certificate({ | ||
@@ -195,3 +243,4 @@ subjects: subjects, | ||
validFrom: validFrom, | ||
validUntil: validUntil | ||
validUntil: validUntil, | ||
purposes: purposes | ||
}); | ||
@@ -244,2 +293,38 @@ cert.signWith(key); | ||
var purposes = options.purposes; | ||
if (purposes === undefined) | ||
purposes = []; | ||
if (purposes.indexOf('signature') === -1) | ||
purposes.push('signature'); | ||
if (options.ca === true) { | ||
if (purposes.indexOf('ca') === -1) | ||
purposes.push('ca'); | ||
if (purposes.indexOf('crl') === -1) | ||
purposes.push('crl'); | ||
} | ||
var hostSubjects = subjects.filter(function (subject) { | ||
return (subject.type === 'host'); | ||
}); | ||
var userSubjects = subjects.filter(function (subject) { | ||
return (subject.type === 'user'); | ||
}); | ||
if (hostSubjects.length > 0) { | ||
if (purposes.indexOf('serverAuth') === -1) | ||
purposes.push('serverAuth'); | ||
} | ||
if (userSubjects.length > 0) { | ||
if (purposes.indexOf('clientAuth') === -1) | ||
purposes.push('clientAuth'); | ||
} | ||
if (userSubjects.length > 0 || hostSubjects.length > 0) { | ||
if (purposes.indexOf('keyAgreement') === -1) | ||
purposes.push('keyAgreement'); | ||
if (key.type === 'rsa' && | ||
purposes.indexOf('encryption') === -1) | ||
purposes.push('encryption'); | ||
} | ||
var cert = new Certificate({ | ||
@@ -253,3 +338,4 @@ subjects: subjects, | ||
validFrom: validFrom, | ||
validUntil: validUntil | ||
validUntil: validUntil, | ||
purposes: purposes | ||
}); | ||
@@ -256,0 +342,0 @@ cert.signWith(issuerKey); |
@@ -82,3 +82,6 @@ // Copyright 2016 Joyent, Inc. | ||
'issuerKeyId': '2.5.29.35', | ||
'altName': '2.5.29.17' | ||
'altName': '2.5.29.17', | ||
'basicConstraints': '2.5.29.19', | ||
'keyUsage': '2.5.29.15', | ||
'extKeyUsage': '2.5.29.37' | ||
}; | ||
@@ -214,2 +217,22 @@ | ||
/* RFC5280, section 4.2.1.12 (KeyPurposeId) */ | ||
var EXTPURPOSE = { | ||
'serverAuth': '1.3.6.1.5.5.7.3.1', | ||
'clientAuth': '1.3.6.1.5.5.7.3.2', | ||
'codeSigning': '1.3.6.1.5.5.7.3.3', | ||
/* See https://github.com/joyent/oid-docs/blob/master/root.md */ | ||
'joyentDocker': '1.3.6.1.4.1.38678.1.4.1', | ||
'joyentCmon': '1.3.6.1.4.1.38678.1.4.2' | ||
}; | ||
var EXTPURPOSE_REV = {}; | ||
Object.keys(EXTPURPOSE).forEach(function (k) { | ||
EXTPURPOSE_REV[EXTPURPOSE[k]] = k; | ||
}); | ||
var KEYUSEBITS = [ | ||
'signature', 'identity', 'keyEncryption', | ||
'encryption', 'keyAgreement', 'ca', 'crl' | ||
]; | ||
function readExtension(cert, buf, der) { | ||
@@ -228,2 +251,77 @@ der.readSequence(); | ||
switch (extId) { | ||
case (EXTS.basicConstraints): | ||
der.readSequence(asn1.Ber.OctetString); | ||
der.readSequence(); | ||
var bcEnd = der.offset + der.length; | ||
var ca = false; | ||
if (der.peek() === asn1.Ber.Boolean) | ||
ca = der.readBoolean(); | ||
if (cert.purposes === undefined) | ||
cert.purposes = []; | ||
if (ca === true) | ||
cert.purposes.push('ca'); | ||
var bc = { oid: extId, critical: critical }; | ||
if (der.offset < bcEnd && der.peek() === asn1.Ber.Integer) | ||
bc.pathLen = der.readInt(); | ||
sig.extras.exts.push(bc); | ||
break; | ||
case (EXTS.extKeyUsage): | ||
der.readSequence(asn1.Ber.OctetString); | ||
der.readSequence(); | ||
if (cert.purposes === undefined) | ||
cert.purposes = []; | ||
var ekEnd = der.offset + der.length; | ||
while (der.offset < ekEnd) { | ||
var oid = der.readOID(); | ||
cert.purposes.push(EXTPURPOSE_REV[oid] || oid); | ||
} | ||
/* | ||
* This is a bit of a hack: in the case where we have a cert | ||
* that's only allowed to do serverAuth or clientAuth (and not | ||
* the other), we want to make sure all our Subjects are of | ||
* the right type. But we already parsed our Subjects and | ||
* decided if they were hosts or users earlier (since it appears | ||
* first in the cert). | ||
* | ||
* So we go through and mutate them into the right kind here if | ||
* it doesn't match. This might not be hugely beneficial, as it | ||
* seems that single-purpose certs are not often seen in the | ||
* wild. | ||
*/ | ||
if (cert.purposes.indexOf('serverAuth') !== -1 && | ||
cert.purposes.indexOf('clientAuth') === -1) { | ||
cert.subjects.forEach(function (ide) { | ||
if (ide.type !== 'host') { | ||
ide.type = 'host'; | ||
ide.hostname = ide.uid || | ||
ide.email || | ||
ide.components[0].value; | ||
} | ||
}); | ||
} else if (cert.purposes.indexOf('clientAuth') !== -1 && | ||
cert.purposes.indexOf('serverAuth') === -1) { | ||
cert.subjects.forEach(function (ide) { | ||
if (ide.type !== 'user') { | ||
ide.type = 'user'; | ||
ide.uid = ide.hostname || | ||
ide.email || | ||
ide.components[0].value; | ||
} | ||
}); | ||
} | ||
sig.extras.exts.push({ oid: extId, critical: critical }); | ||
break; | ||
case (EXTS.keyUsage): | ||
der.readSequence(asn1.Ber.OctetString); | ||
var bits = der.readString(asn1.Ber.BitString, true); | ||
var setBits = readBitField(bits, KEYUSEBITS); | ||
setBits.forEach(function (bit) { | ||
if (cert.purposes === undefined) | ||
cert.purposes = []; | ||
if (cert.purposes.indexOf(bit) === -1) | ||
cert.purposes.push(bit); | ||
}); | ||
sig.extras.exts.push({ oid: extId, critical: critical, | ||
bits: bits }); | ||
break; | ||
case (EXTS.altName): | ||
@@ -427,2 +525,3 @@ der.readSequence(asn1.Ber.OctetString); | ||
if (altNames.length > 0 || subject.type === 'host' || | ||
(cert.purposes !== undefined && cert.purposes.length > 0) || | ||
(sig.extras && sig.extras.exts)) { | ||
@@ -432,5 +531,18 @@ der.startSequence(Local(3)); | ||
var exts = [ | ||
{ oid: EXTS.altName } | ||
]; | ||
var exts = []; | ||
if (cert.purposes !== undefined && cert.purposes.length > 0) { | ||
exts.push({ | ||
oid: EXTS.basicConstraints, | ||
critical: true | ||
}); | ||
exts.push({ | ||
oid: EXTS.keyUsage, | ||
critical: true | ||
}); | ||
exts.push({ | ||
oid: EXTS.extKeyUsage, | ||
critical: true | ||
}); | ||
} | ||
exts.push({ oid: EXTS.altName }); | ||
if (sig.extras && sig.extras.exts) | ||
@@ -476,2 +588,50 @@ exts = sig.extras.exts; | ||
der.endSequence(); | ||
} else if (exts[i].oid === EXTS.basicConstraints) { | ||
der.startSequence(asn1.Ber.OctetString); | ||
der.startSequence(); | ||
var ca = (cert.purposes.indexOf('ca') !== -1); | ||
var pathLen = exts[i].pathLen; | ||
der.writeBoolean(ca); | ||
if (pathLen !== undefined) | ||
der.writeInt(pathLen); | ||
der.endSequence(); | ||
der.endSequence(); | ||
} else if (exts[i].oid === EXTS.extKeyUsage) { | ||
der.startSequence(asn1.Ber.OctetString); | ||
der.startSequence(); | ||
cert.purposes.forEach(function (purpose) { | ||
if (purpose === 'ca') | ||
return; | ||
if (KEYUSEBITS.indexOf(purpose) !== -1) | ||
return; | ||
var oid = purpose; | ||
if (EXTPURPOSE[purpose] !== undefined) | ||
oid = EXTPURPOSE[purpose]; | ||
der.writeOID(oid); | ||
}); | ||
der.endSequence(); | ||
der.endSequence(); | ||
} else if (exts[i].oid === EXTS.keyUsage) { | ||
der.startSequence(asn1.Ber.OctetString); | ||
/* | ||
* If we parsed this certificate from a byte | ||
* stream (i.e. we didn't generate it in sshpk) | ||
* then we'll have a ".bits" property on the | ||
* ext with the original raw byte contents. | ||
* | ||
* If we have this, use it here instead of | ||
* regenerating it. This guarantees we output | ||
* the same data we parsed, so signatures still | ||
* validate. | ||
*/ | ||
if (exts[i].bits !== undefined) { | ||
der.writeBuffer(exts[i].bits, | ||
asn1.Ber.BitString); | ||
} else { | ||
var bits = writeBitField(cert.purposes, | ||
KEYUSEBITS); | ||
der.writeBuffer(bits, | ||
asn1.Ber.BitString); | ||
} | ||
der.endSequence(); | ||
} else { | ||
@@ -491,1 +651,56 @@ der.writeBuffer(exts[i].data, | ||
} | ||
/* | ||
* Reads an ASN.1 BER bitfield out of the Buffer produced by doing | ||
* `BerReader#readString(asn1.Ber.BitString)`. That function gives us the raw | ||
* contents of the BitString tag, which is a count of unused bits followed by | ||
* the bits as a right-padded byte string. | ||
* | ||
* `bits` is the Buffer, `bitIndex` should contain an array of string names | ||
* for the bits in the string, ordered starting with bit #0 in the ASN.1 spec. | ||
* | ||
* Returns an array of Strings, the names of the bits that were set to 1. | ||
*/ | ||
function readBitField(bits, bitIndex) { | ||
var bitLen = 8 * (bits.length - 1) - bits[0]; | ||
var setBits = {}; | ||
for (var i = 0; i < bitLen; ++i) { | ||
var byteN = 1 + Math.floor(i / 8); | ||
var bit = 7 - (i % 8); | ||
var mask = 1 << bit; | ||
var bitVal = ((bits[byteN] & mask) !== 0); | ||
var name = bitIndex[i]; | ||
if (bitVal && typeof (name) === 'string') { | ||
setBits[name] = true; | ||
} | ||
} | ||
return (Object.keys(setBits)); | ||
} | ||
/* | ||
* `setBits` is an array of strings, containing the names for each bit that | ||
* sould be set to 1. `bitIndex` is same as in `readBitField()`. | ||
* | ||
* Returns a Buffer, ready to be written out with `BerWriter#writeString()`. | ||
*/ | ||
function writeBitField(setBits, bitIndex) { | ||
var bitLen = bitIndex.length; | ||
var blen = Math.ceil(bitLen / 8); | ||
var unused = blen * 8 - bitLen; | ||
var bits = new Buffer(1 + blen); | ||
bits.fill(0); | ||
bits[0] = unused; | ||
for (var i = 0; i < bitLen; ++i) { | ||
var byteN = 1 + Math.floor(i / 8); | ||
var bit = 7 - (i % 8); | ||
var mask = 1 << bit; | ||
var name = bitIndex[i]; | ||
if (name === undefined) | ||
continue; | ||
var bitVal = (setBits.indexOf(name) !== -1); | ||
if (bitVal) { | ||
bits[byteN] |= mask; | ||
} | ||
} | ||
return (bits); | ||
} |
{ | ||
"name": "sshpk", | ||
"version": "1.10.2", | ||
"version": "1.11.0", | ||
"description": "A library for finding and using SSH public keys", | ||
@@ -5,0 +5,0 @@ "main": "lib/index.js", |
@@ -411,6 +411,6 @@ sshpk | ||
Notably, there is no implementation of CA chain-of-trust verification, and no | ||
support for key usage restrictions (or other kinds of restrictions). Please do | ||
the security world a favour, and DO NOT use this code for certificate | ||
verification in the traditional X.509 CA chain style. | ||
Notably, there is no implementation of CA chain-of-trust verification, and only | ||
very minimal support for key usage restrictions. Please do the security world | ||
a favour, and DO NOT use this code for certificate verification in the | ||
traditional X.509 CA chain style. | ||
@@ -440,2 +440,3 @@ ### `parseCertificate(data, format)` | ||
- `serial` -- optional Buffer, the serial number of the certificate | ||
- `purposes` -- optional Array of String, X.509 key usage restrictions | ||
@@ -457,2 +458,3 @@ ### `createCertificate(subject, key, issuer, issuerKey[, options])` | ||
- `serial` -- optional Buffer, the serial number of the certificate | ||
- `purposes` -- optional Array of String, X.509 key usage restrictions | ||
@@ -481,2 +483,19 @@ ### `Certificate#subjects` | ||
### `Certificate#purposes` | ||
Array of Strings indicating the X.509 key usage purposes that this certificate | ||
is valid for. The possible strings at the moment are: | ||
* `'signature'` -- key can be used for digital signatures | ||
* `'identity'` -- key can be used to attest about the identity of the signer | ||
(X.509 calls this `nonRepudiation`) | ||
* `'codeSigning'` -- key can be used to sign executable code | ||
* `'keyEncryption'` -- key can be used to encrypt other keys | ||
* `'encryption'` -- key can be used to encrypt data (only applies for RSA) | ||
* `'keyAgreement'` -- key can be used for key exchange protocols such as | ||
Diffie-Hellman | ||
* `'ca'` -- key can be used to sign other certificates (is a Certificate | ||
Authority) | ||
* `'crl'` -- key can be used to sign Certificate Revocation Lists (CRLs) | ||
### `Certificate#isExpired([when])` | ||
@@ -483,0 +502,0 @@ |
183817
4632
686