sshpk
Advanced tools
Comparing version 1.15.2 to 1.16.0
@@ -1,2 +0,2 @@ | ||
// Copyright 2015 Joyent, Inc. | ||
// Copyright 2018 Joyent, Inc. | ||
@@ -11,2 +11,3 @@ module.exports = Fingerprint; | ||
var Key = require('./key'); | ||
var PrivateKey = require('./private-key'); | ||
var Certificate = require('./certificate'); | ||
@@ -30,2 +31,3 @@ var utils = require('./utils'); | ||
this.type = opts.type; | ||
this.hashType = opts.hashType; | ||
} | ||
@@ -35,3 +37,3 @@ | ||
if (format === undefined) { | ||
if (this.algorithm === 'md5') | ||
if (this.algorithm === 'md5' || this.hashType === 'spki') | ||
format = 'hex'; | ||
@@ -45,4 +47,8 @@ else | ||
case 'hex': | ||
if (this.hashType === 'spki') | ||
return (this.hash.toString('hex')); | ||
return (addColons(this.hash.toString('hex'))); | ||
case 'base64': | ||
if (this.hashType === 'spki') | ||
return (this.hash.toString('base64')); | ||
return (sshBase64Format(this.algorithm, | ||
@@ -57,3 +63,9 @@ this.hash.toString('base64'))); | ||
assert.object(other, 'key or certificate'); | ||
if (this.type === 'key') { | ||
if (this.type === 'key' && this.hashType !== 'ssh') { | ||
utils.assertCompatible(other, Key, [1, 7], 'key with spki'); | ||
if (PrivateKey.isPrivateKey(other)) { | ||
utils.assertCompatible(other, PrivateKey, [1, 6], | ||
'privatekey with spki support'); | ||
} | ||
} else if (this.type === 'key') { | ||
utils.assertCompatible(other, Key, [1, 0], 'key'); | ||
@@ -65,3 +77,3 @@ } else { | ||
var theirHash = other.hash(this.algorithm); | ||
var theirHash = other.hash(this.algorithm, this.hashType); | ||
var theirHash2 = crypto.createHash(this.algorithm). | ||
@@ -77,2 +89,7 @@ update(theirHash).digest('base64'); | ||
/*JSSTYLED*/ | ||
var base64RE = /^[A-Za-z0-9+\/=]+$/; | ||
/*JSSTYLED*/ | ||
var hexRE = /^[a-fA-F0-9]+$/; | ||
Fingerprint.parse = function (fp, options) { | ||
@@ -91,9 +108,14 @@ assert.string(fp, 'fingerprint'); | ||
enAlgs = options.enAlgs; | ||
if (options.algorithms !== undefined) | ||
enAlgs = options.algorithms; | ||
assert.optionalArrayOfString(enAlgs, 'algorithms'); | ||
var hashType = 'ssh'; | ||
if (options.hashType !== undefined) | ||
hashType = options.hashType; | ||
assert.string(hashType, 'options.hashType'); | ||
var parts = fp.split(':'); | ||
if (parts.length == 2) { | ||
alg = parts[0].toLowerCase(); | ||
/*JSSTYLED*/ | ||
var base64RE = /^[A-Za-z0-9+\/=]+$/; | ||
if (!base64RE.test(parts[1])) | ||
@@ -118,5 +140,3 @@ throw (new FingerprintFormatError(fp)); | ||
parts = parts.join(''); | ||
/*JSSTYLED*/ | ||
var md5RE = /^[a-fA-F0-9]+$/; | ||
if (!md5RE.test(parts) || parts.length % 2 !== 0) | ||
if (!hexRE.test(parts) || parts.length % 2 !== 0) | ||
throw (new FingerprintFormatError(fp)); | ||
@@ -128,2 +148,31 @@ try { | ||
} | ||
} else { | ||
if (hexRE.test(fp)) { | ||
hash = Buffer.from(fp, 'hex'); | ||
} else if (base64RE.test(fp)) { | ||
hash = Buffer.from(fp, 'base64'); | ||
} else { | ||
throw (new FingerprintFormatError(fp)); | ||
} | ||
switch (hash.length) { | ||
case 32: | ||
alg = 'sha256'; | ||
break; | ||
case 16: | ||
alg = 'md5'; | ||
break; | ||
case 20: | ||
alg = 'sha1'; | ||
break; | ||
case 64: | ||
alg = 'sha512'; | ||
break; | ||
default: | ||
throw (new FingerprintFormatError(fp)); | ||
} | ||
/* Plain hex/base64: guess it's probably SPKI unless told. */ | ||
if (options.hashType === undefined) | ||
hashType = 'spki'; | ||
} | ||
@@ -146,3 +195,4 @@ | ||
hash: hash, | ||
type: options.type || 'key' | ||
type: options.type || 'key', | ||
hashType: hashType | ||
})); | ||
@@ -173,4 +223,5 @@ }; | ||
* [1,1] -- first tagged ver | ||
* [1,2] -- hashType and spki support | ||
*/ | ||
Fingerprint.prototype._sshpkApiVersion = [1, 1]; | ||
Fingerprint.prototype._sshpkApiVersion = [1, 2]; | ||
@@ -177,0 +228,0 @@ Fingerprint._oldVersionDetect = function (obj) { |
@@ -1,2 +0,2 @@ | ||
// Copyright 2015 Joyent, Inc. | ||
// Copyright 2018 Joyent, Inc. | ||
@@ -18,2 +18,3 @@ module.exports = { | ||
var dnssec = require('./dnssec'); | ||
var putty = require('./putty'); | ||
@@ -30,2 +31,4 @@ var DNSSEC_PRIVKEY_HEADER_PREFIX = 'Private-key-format: v1'; | ||
return (ssh.read(buf, options)); | ||
if (buf.match(/^putty-user-key-file-2:/i)) | ||
return (putty.read(buf, options)); | ||
if (findDNSSECHeader(buf)) | ||
@@ -40,2 +43,4 @@ return (dnssec.read(buf, options)); | ||
return (ssh.read(buf, options)); | ||
if (findPuTTYHeader(buf)) | ||
return (putty.read(buf, options)); | ||
if (findDNSSECHeader(buf)) | ||
@@ -49,2 +54,14 @@ return (dnssec.read(buf, options)); | ||
function findPuTTYHeader(buf) { | ||
var offset = 0; | ||
while (offset < buf.length && | ||
(buf[offset] === 32 || buf[offset] === 10 || buf[offset] === 9)) | ||
++offset; | ||
if (offset + 22 <= buf.length && | ||
buf.slice(offset, offset + 22).toString('ascii').toLowerCase() === | ||
'putty-user-key-file-2:') | ||
return (true); | ||
return (false); | ||
} | ||
function findSSHHeader(buf) { | ||
@@ -51,0 +68,0 @@ var offset = 0; |
@@ -1,2 +0,2 @@ | ||
// Copyright 2015 Joyent, Inc. | ||
// Copyright 2018 Joyent, Inc. | ||
@@ -24,2 +24,25 @@ module.exports = { | ||
var OID_PBES2 = '1.2.840.113549.1.5.13'; | ||
var OID_PBKDF2 = '1.2.840.113549.1.5.12'; | ||
var OID_TO_CIPHER = { | ||
'1.2.840.113549.3.7': '3des-cbc', | ||
'2.16.840.1.101.3.4.1.2': 'aes128-cbc', | ||
'2.16.840.1.101.3.4.1.42': 'aes256-cbc' | ||
}; | ||
var CIPHER_TO_OID = {}; | ||
Object.keys(OID_TO_CIPHER).forEach(function (k) { | ||
CIPHER_TO_OID[OID_TO_CIPHER[k]] = k; | ||
}); | ||
var OID_TO_HASH = { | ||
'1.2.840.113549.2.7': 'sha1', | ||
'1.2.840.113549.2.9': 'sha256', | ||
'1.2.840.113549.2.11': 'sha512' | ||
}; | ||
var HASH_TO_OID = {}; | ||
Object.keys(OID_TO_HASH).forEach(function (k) { | ||
HASH_TO_OID[OID_TO_HASH[k]] = k; | ||
}); | ||
/* | ||
@@ -36,10 +59,18 @@ * For reading we support both PKCS#1 and PKCS#8. If we find a private key, | ||
var lines = buf.trim().split('\n'); | ||
var lines = buf.trim().split(/[\r\n]+/g); | ||
var m = lines[0].match(/*JSSTYLED*/ | ||
/[-]+[ ]*BEGIN ([A-Z0-9][A-Za-z0-9]+ )?(PUBLIC|PRIVATE) KEY[ ]*[-]+/); | ||
var m; | ||
var si = -1; | ||
while (!m && si < lines.length) { | ||
m = lines[++si].match(/*JSSTYLED*/ | ||
/[-]+[ ]*BEGIN ([A-Z0-9][A-Za-z0-9]+ )?(PUBLIC|PRIVATE) KEY[ ]*[-]+/); | ||
} | ||
assert.ok(m, 'invalid PEM header'); | ||
var m2 = lines[lines.length - 1].match(/*JSSTYLED*/ | ||
/[-]+[ ]*END ([A-Z0-9][A-Za-z0-9]+ )?(PUBLIC|PRIVATE) KEY[ ]*[-]+/); | ||
var m2; | ||
var ei = lines.length; | ||
while (!m2 && ei > 0) { | ||
m2 = lines[--ei].match(/*JSSTYLED*/ | ||
/[-]+[ ]*END ([A-Z0-9][A-Za-z0-9]+ )?(PUBLIC|PRIVATE) KEY[ ]*[-]+/); | ||
} | ||
assert.ok(m2, 'invalid PEM footer'); | ||
@@ -58,2 +89,4 @@ | ||
lines = lines.slice(si, ei + 1); | ||
var headers = {}; | ||
@@ -69,2 +102,6 @@ while (true) { | ||
/* Chop off the first and last lines */ | ||
lines = lines.slice(0, -1).join(''); | ||
buf = Buffer.from(lines, 'base64'); | ||
var cipher, key, iv; | ||
@@ -92,6 +129,67 @@ if (headers['proc-type']) { | ||
/* Chop off the first and last lines */ | ||
lines = lines.slice(0, -1).join(''); | ||
buf = Buffer.from(lines, 'base64'); | ||
if (alg && alg.toLowerCase() === 'encrypted') { | ||
var eder = new asn1.BerReader(buf); | ||
var pbesEnd; | ||
eder.readSequence(); | ||
eder.readSequence(); | ||
pbesEnd = eder.offset + eder.length; | ||
var method = eder.readOID(); | ||
if (method !== OID_PBES2) { | ||
throw (new Error('Unsupported PEM/PKCS8 encryption ' + | ||
'scheme: ' + method)); | ||
} | ||
eder.readSequence(); /* PBES2-params */ | ||
eder.readSequence(); /* keyDerivationFunc */ | ||
var kdfEnd = eder.offset + eder.length; | ||
var kdfOid = eder.readOID(); | ||
if (kdfOid !== OID_PBKDF2) | ||
throw (new Error('Unsupported PBES2 KDF: ' + kdfOid)); | ||
eder.readSequence(); | ||
var salt = eder.readString(asn1.Ber.OctetString, true); | ||
var iterations = eder.readInt(); | ||
var hashAlg = 'sha1'; | ||
if (eder.offset < kdfEnd) { | ||
eder.readSequence(); | ||
var hashAlgOid = eder.readOID(); | ||
hashAlg = OID_TO_HASH[hashAlgOid]; | ||
if (hashAlg === undefined) { | ||
throw (new Error('Unsupported PBKDF2 hash: ' + | ||
hashAlgOid)); | ||
} | ||
} | ||
eder._offset = kdfEnd; | ||
eder.readSequence(); /* encryptionScheme */ | ||
var cipherOid = eder.readOID(); | ||
cipher = OID_TO_CIPHER[cipherOid]; | ||
if (cipher === undefined) { | ||
throw (new Error('Unsupported PBES2 cipher: ' + | ||
cipherOid)); | ||
} | ||
iv = eder.readString(asn1.Ber.OctetString, true); | ||
eder._offset = pbesEnd; | ||
buf = eder.readString(asn1.Ber.OctetString, true); | ||
if (typeof (options.passphrase) === 'string') { | ||
options.passphrase = Buffer.from( | ||
options.passphrase, 'utf-8'); | ||
} | ||
if (!Buffer.isBuffer(options.passphrase)) { | ||
throw (new errors.KeyEncryptedError( | ||
options.filename, 'PEM')); | ||
} | ||
var cinfo = utils.opensshCipherInfo(cipher); | ||
cipher = cinfo.opensslName; | ||
key = utils.pbkdf2(hashAlg, salt, iterations, cinfo.keySize, | ||
options.passphrase); | ||
alg = undefined; | ||
} | ||
if (cipher && key && iv) { | ||
@@ -98,0 +196,0 @@ var cipherStream = crypto.createDecipheriv(cipher, key, iv); |
@@ -1,2 +0,2 @@ | ||
// Copyright 2015 Joyent, Inc. | ||
// Copyright 2018 Joyent, Inc. | ||
@@ -8,2 +8,3 @@ module.exports = { | ||
writePkcs8: writePkcs8, | ||
pkcs8ToBuffer: pkcs8ToBuffer, | ||
@@ -416,2 +417,8 @@ readECDSACurve: readECDSACurve, | ||
function pkcs8ToBuffer(key) { | ||
var der = new asn1.BerWriter(); | ||
writePkcs8(der, key); | ||
return (der.buffer); | ||
} | ||
function writePkcs8(der, key) { | ||
@@ -418,0 +425,0 @@ der.startSequence(); |
@@ -32,10 +32,20 @@ // Copyright 2016 Joyent, Inc. | ||
var m = lines[0].match(/*JSSTYLED*/ | ||
/[-]+[ ]*BEGIN CERTIFICATE[ ]*[-]+/); | ||
var m; | ||
var si = -1; | ||
while (!m && si < lines.length) { | ||
m = lines[++si].match(/*JSSTYLED*/ | ||
/[-]+[ ]*BEGIN CERTIFICATE[ ]*[-]+/); | ||
} | ||
assert.ok(m, 'invalid PEM header'); | ||
var m2 = lines[lines.length - 1].match(/*JSSTYLED*/ | ||
/[-]+[ ]*END CERTIFICATE[ ]*[-]+/); | ||
var m2; | ||
var ei = lines.length; | ||
while (!m2 && ei > 0) { | ||
m2 = lines[--ei].match(/*JSSTYLED*/ | ||
/[-]+[ ]*END CERTIFICATE[ ]*[-]+/); | ||
} | ||
assert.ok(m2, 'invalid PEM footer'); | ||
lines = lines.slice(si, ei + 1); | ||
var headers = {}; | ||
@@ -42,0 +52,0 @@ while (true) { |
@@ -1,2 +0,2 @@ | ||
// Copyright 2017 Joyent, Inc. | ||
// Copyright 2018 Joyent, Inc. | ||
@@ -35,2 +35,4 @@ module.exports = Key; | ||
formats['dnssec'] = require('./formats/dnssec'); | ||
formats['putty'] = require('./formats/putty'); | ||
formats['ppk'] = formats['putty']; | ||
@@ -102,4 +104,7 @@ function Key(opts) { | ||
Key.prototype.hash = function (algo) { | ||
Key.prototype.hash = function (algo, type) { | ||
assert.string(algo, 'algorithm'); | ||
assert.optionalString(type, 'type'); | ||
if (type === undefined) | ||
type = 'ssh'; | ||
algo = algo.toLowerCase(); | ||
@@ -109,18 +114,31 @@ if (algs.hashAlgs[algo] === undefined) | ||
if (this._hashCache[algo]) | ||
return (this._hashCache[algo]); | ||
var hash = crypto.createHash(algo). | ||
update(this.toBuffer('rfc4253')).digest(); | ||
this._hashCache[algo] = hash; | ||
var cacheKey = algo + '||' + type; | ||
if (this._hashCache[cacheKey]) | ||
return (this._hashCache[cacheKey]); | ||
var buf; | ||
if (type === 'ssh') { | ||
buf = this.toBuffer('rfc4253'); | ||
} else if (type === 'spki') { | ||
buf = formats.pkcs8.pkcs8ToBuffer(this); | ||
} else { | ||
throw (new Error('Hash type ' + type + ' not supported')); | ||
} | ||
var hash = crypto.createHash(algo).update(buf).digest(); | ||
this._hashCache[cacheKey] = hash; | ||
return (hash); | ||
}; | ||
Key.prototype.fingerprint = function (algo) { | ||
Key.prototype.fingerprint = function (algo, type) { | ||
if (algo === undefined) | ||
algo = 'sha256'; | ||
if (type === undefined) | ||
type = 'ssh'; | ||
assert.string(algo, 'algorithm'); | ||
assert.string(type, 'type'); | ||
var opts = { | ||
type: 'key', | ||
hash: this.hash(algo), | ||
algorithm: algo | ||
hash: this.hash(algo, type), | ||
algorithm: algo, | ||
hashType: type | ||
}; | ||
@@ -263,4 +281,5 @@ return (new Fingerprint(opts)); | ||
* [1,6] -- changed ed25519 part names | ||
* [1,7] -- spki hash types | ||
*/ | ||
Key.prototype._sshpkApiVersion = [1, 6]; | ||
Key.prototype._sshpkApiVersion = [1, 7]; | ||
@@ -267,0 +286,0 @@ Key._oldVersionDetect = function (obj) { |
@@ -57,6 +57,10 @@ // Copyright 2017 Joyent, Inc. | ||
PrivateKey.prototype.hash = function (algo) { | ||
return (this.toPublic().hash(algo)); | ||
PrivateKey.prototype.hash = function (algo, type) { | ||
return (this.toPublic().hash(algo, type)); | ||
}; | ||
PrivateKey.prototype.fingerprint = function (algo, type) { | ||
return (this.toPublic().fingerprint(algo, type)); | ||
}; | ||
PrivateKey.prototype.toPublic = function () { | ||
@@ -229,4 +233,5 @@ if (this._pubCache) | ||
* [1,5] -- changed ed25519 part names and format | ||
* [1,6] -- type arguments for hash() and fingerprint() | ||
*/ | ||
PrivateKey.prototype._sshpkApiVersion = [1, 5]; | ||
PrivateKey.prototype._sshpkApiVersion = [1, 6]; | ||
@@ -233,0 +238,0 @@ PrivateKey._oldVersionDetect = function (obj) { |
@@ -20,3 +20,4 @@ // Copyright 2015 Joyent, Inc. | ||
writeBitString: writeBitString, | ||
readBitString: readBitString | ||
readBitString: readBitString, | ||
pbkdf2: pbkdf2 | ||
}; | ||
@@ -128,2 +129,36 @@ | ||
/* See: RFC2898 */ | ||
function pbkdf2(hashAlg, salt, iterations, size, passphrase) { | ||
var hkey = Buffer.alloc(salt.length + 4); | ||
salt.copy(hkey); | ||
var gen = 0, ts = []; | ||
var i = 1; | ||
while (gen < size) { | ||
var t = T(i++); | ||
gen += t.length; | ||
ts.push(t); | ||
} | ||
return (Buffer.concat(ts).slice(0, size)); | ||
function T(I) { | ||
hkey.writeUInt32BE(I, hkey.length - 4); | ||
var hmac = crypto.createHmac(hashAlg, passphrase); | ||
hmac.update(hkey); | ||
var Ti = hmac.digest(); | ||
var Uc = Ti; | ||
var c = 1; | ||
while (c++ < iterations) { | ||
hmac = crypto.createHmac(hashAlg, passphrase); | ||
hmac.update(Uc); | ||
Uc = hmac.digest(); | ||
for (var x = 0; x < Ti.length; ++x) | ||
Ti[x] ^= Uc[x]; | ||
} | ||
return (Ti); | ||
} | ||
} | ||
/* Count leading zero bits on a buffer */ | ||
@@ -130,0 +165,0 @@ function countZeros(buf) { |
{ | ||
"name": "sshpk", | ||
"version": "1.15.2", | ||
"version": "1.16.0", | ||
"description": "A library for finding and using SSH public keys", | ||
@@ -5,0 +5,0 @@ "main": "lib/index.js", |
@@ -129,2 +129,5 @@ sshpk | ||
`ssh-keygen -o` | ||
- `dnssec`: `.key` file format output by `dnssec-keygen` etc | ||
- `putty`: the PuTTY `.ppk` file format (supports truncated variant without | ||
all the lines from `Private-Lines:` onwards) | ||
- `options` -- Optional Object, extra options, with keys: | ||
@@ -178,3 +181,3 @@ - `filename` -- Optional String, name for the key being parsed | ||
### `Key#fingerprint([algorithm = 'sha256'])` | ||
### `Key#fingerprint([algorithm = 'sha256'[, hashType = 'ssh']])` | ||
@@ -187,2 +190,5 @@ Creates a new `Fingerprint` object representing this Key's fingerprint. | ||
`sha1`, `sha256`, `sha384`, `sha512` | ||
- `hashType` -- String name of fingerprint hash type to use, valid options are | ||
`ssh` (the type of fingerprint used by OpenSSH, e.g. in | ||
`ssh-keygen`), `spki` (used by HPKP, some OpenSSL applications) | ||
@@ -237,2 +243,3 @@ ### `Key#createVerify([hashAlgorithm])` | ||
- `rfc4253`: raw OpenSSH wire format | ||
- `dnssec`: `.private` format output by `dnssec-keygen` etc. | ||
- `options` -- Optional Object, extra options, with keys: | ||
@@ -340,3 +347,3 @@ - `filename` -- Optional String, name for the key being parsed | ||
### `parseFingerprint(fingerprint[, algorithms])` | ||
### `parseFingerprint(fingerprint[, options])` | ||
@@ -349,5 +356,11 @@ Pre-parses a fingerprint, creating a `Fingerprint` object that can be used to | ||
- `fingerprint` -- String, the fingerprint value, in any supported format | ||
- `algorithms` -- Optional list of strings, names of hash algorithms to limit | ||
support to. If `fingerprint` uses a hash algorithm not on | ||
this list, throws `InvalidAlgorithmError`. | ||
- `options` -- Optional Object, with properties: | ||
- `algorithms` -- Array of strings, names of hash algorithms to limit | ||
support to. If `fingerprint` uses a hash algorithm not on | ||
this list, throws `InvalidAlgorithmError`. | ||
- `hashType` -- String, the type of hash the fingerprint uses, either `ssh` | ||
or `spki` (normally auto-detected based on the format, but | ||
can be overridden) | ||
- `type` -- String, the entity this fingerprint identifies, either `key` or | ||
`certificate` | ||
@@ -373,10 +386,15 @@ ### `Fingerprint.isFingerprint(obj)` | ||
### `Fingerprint#matches(key)` | ||
### `Fingerprint#matches(keyOrCertificate)` | ||
Verifies whether or not this `Fingerprint` matches a given `Key`. This function | ||
uses double-hashing to avoid leaking timing information. Returns a boolean. | ||
Verifies whether or not this `Fingerprint` matches a given `Key` or | ||
`Certificate`. This function uses double-hashing to avoid leaking timing | ||
information. Returns a boolean. | ||
Note that a `Key`-type Fingerprint will always return `false` if asked to match | ||
a `Certificate` and vice versa. | ||
Parameters | ||
- `key` -- a `Key` object, the key to match this fingerprint against | ||
- `keyOrCertificate` -- a `Key` object or `Certificate` object, the entity to | ||
match this fingerprint against | ||
@@ -383,0 +401,0 @@ ## Signatures |
Sorry, the diff of this file is not supported yet
224787
36
5830
805