Skip to content

Commit

Permalink
#59 want support for SPKI fingerprint format
Browse files Browse the repository at this point in the history
Reviewed by: Isaac Davis <[email protected]>
Reviewed by: Cody Peter Mello <[email protected]>
  • Loading branch information
arekinath committed Dec 19, 2018
1 parent 385ff11 commit 44aec4a
Show file tree
Hide file tree
Showing 8 changed files with 220 additions and 54 deletions.
32 changes: 23 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,14 +174,17 @@ Parameters

Same as `this.toBuffer(format).toString()`.

### `Key#fingerprint([algorithm = 'sha256'])`
### `Key#fingerprint([algorithm = 'sha256'[, hashType = 'ssh']])`

Creates a new `Fingerprint` object representing this Key's fingerprint.

Parameters

- `algorithm` -- String name of hash algorithm to use, valid options are `md5`,
`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)

### `Key#createVerify([hashAlgorithm])`

Expand Down Expand Up @@ -333,17 +336,23 @@ Parameters

## Fingerprints

### `parseFingerprint(fingerprint[, algorithms])`
### `parseFingerprint(fingerprint[, options])`

Pre-parses a fingerprint, creating a `Fingerprint` object that can be used to
quickly locate a key by using the `Fingerprint#matches` function.

Parameters

- `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`

### `Fingerprint.isFingerprint(obj)`

Expand All @@ -364,14 +373,19 @@ Parameters
`base64`. If this `Fingerprint` uses the `md5` algorithm, the
default format is `hex`. Otherwise, the default is `base64`.

### `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

## Signatures

Expand Down
70 changes: 53 additions & 17 deletions bin/sshpk-conv
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env node
// -*- mode: js -*-
// vim: set filetype=javascript :
// Copyright 2015 Joyent, Inc. All rights reserved.
// Copyright 2018 Joyent, Inc. All rights reserved.

var dashdash = require('dashdash');
var sshpk = require('../lib/index');
Expand Down Expand Up @@ -47,6 +47,21 @@ var options = [
type: 'bool',
help: 'Print key metadata instead of converting'
},
{
names: ['fingerprint', 'F'],
type: 'bool',
help: 'Output key fingerprint'
},
{
names: ['hash', 'H'],
type: 'string',
help: 'Hash function to use for key fingeprint with -F'
},
{
names: ['spki', 's'],
type: 'bool',
help: 'With -F, generates an SPKI fingerprint instead of SSH'
},
{
names: ['comment', 'c'],
type: 'string',
Expand Down Expand Up @@ -75,13 +90,17 @@ if (require.main === module) {
var help = parser.help({}).trimRight();
console.error('sshpk-conv: converts between SSH key formats\n');
console.error(help);
console.error('\navailable formats:');
console.error('\navailable key formats:');
console.error(' - pem, pkcs1 eg id_rsa');
console.error(' - ssh eg id_rsa.pub');
console.error(' - pkcs8 format you want for openssl');
console.error(' - openssh like output of ssh-keygen -o');
console.error(' - rfc4253 raw OpenSSH wire format');
console.error(' - dnssec dnssec-keygen format');
console.error('\navailable fingerprint formats:');
console.error(' - hex colon-separated hex for SSH');
console.error(' straight hex for SPKI');
console.error(' - base64 SHA256:* format from OpenSSH');
process.exit(1);
}

Expand Down Expand Up @@ -172,18 +191,7 @@ if (require.main === module) {
if (opts.comment)
key.comment = opts.comment;

if (!opts.identify) {
fmt = undefined;
if (opts.outformat)
fmt = opts.outformat;
outFile.write(key.toBuffer(fmt));
if (fmt === 'ssh' ||
(!opts.private && fmt === undefined))
outFile.write('\n');
outFile.once('drain', function () {
process.exit(0);
});
} else {
if (opts.identify) {
var kind = 'public';
if (sshpk.PrivateKey.isPrivateKey(key))
kind = 'private';
Expand All @@ -193,10 +201,38 @@ if (require.main === module) {
console.log('ECDSA curve: %s', key.curve);
if (key.comment)
console.log('Comment: %s', key.comment);
console.log('Fingerprint:');
console.log(' ' + key.fingerprint().toString());
console.log(' ' + key.fingerprint('md5').toString());
console.log('SHA256 fingerprint: ' +
key.fingerprint('sha256').toString());
console.log('MD5 fingerprint: ' +
key.fingerprint('md5').toString());
console.log('SPKI-SHA256 fingerprint: ' +
key.fingerprint('sha256', 'spki').toString());
process.exit(0);
return;
}

if (opts.fingerprint) {
var hash = opts.hash;
var type = opts.spki ? 'spki' : 'ssh';
var format = opts.outformat;
var fp = key.fingerprint(hash, type).toString(format);
outFile.write(fp);
outFile.write('\n');
outFile.once('drain', function () {
process.exit(0);
});
return;
}

fmt = undefined;
if (opts.outformat)
fmt = opts.outformat;
outFile.write(key.toBuffer(fmt));
if (fmt === 'ssh' ||
(!opts.private && fmt === undefined))
outFile.write('\n');
outFile.once('drain', function () {
process.exit(0);
});
});
}
73 changes: 62 additions & 11 deletions lib/fingerprint.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2015 Joyent, Inc.
// Copyright 2018 Joyent, Inc.

module.exports = Fingerprint;

Expand All @@ -8,6 +8,7 @@ var algs = require('./algs');
var crypto = require('crypto');
var errs = require('./errors');
var Key = require('./key');
var PrivateKey = require('./private-key');
var Certificate = require('./certificate');
var utils = require('./utils');

Expand All @@ -26,11 +27,12 @@ function Fingerprint(opts) {

this.hash = opts.hash;
this.type = opts.type;
this.hashType = opts.hashType;
}

Fingerprint.prototype.toString = function (format) {
if (format === undefined) {
if (this.algorithm === 'md5')
if (this.algorithm === 'md5' || this.hashType === 'spki')
format = 'hex';
else
format = 'base64';
Expand All @@ -39,8 +41,12 @@ Fingerprint.prototype.toString = function (format) {

switch (format) {
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,
this.hash.toString('base64')));
default:
Expand All @@ -50,14 +56,20 @@ Fingerprint.prototype.toString = function (format) {

Fingerprint.prototype.matches = function (other) {
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');
} else {
utils.assertCompatible(other, Certificate, [1, 0],
'certificate');
}

var theirHash = other.hash(this.algorithm);
var theirHash = other.hash(this.algorithm, this.hashType);
var theirHash2 = crypto.createHash(this.algorithm).
update(theirHash).digest('base64');

Expand All @@ -68,6 +80,11 @@ Fingerprint.prototype.matches = function (other) {
return (this.hash2 === theirHash2);
};

/*JSSTYLED*/
var base64RE = /^[A-Za-z0-9+\/=]+$/;
/*JSSTYLED*/
var hexRE = /^[a-fA-F0-9]+$/;

Fingerprint.parse = function (fp, options) {
assert.string(fp, 'fingerprint');

Expand All @@ -81,13 +98,18 @@ Fingerprint.parse = function (fp, options) {
options = {};
if (options.enAlgs !== undefined)
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]))
throw (new FingerprintFormatError(fp));
try {
Expand All @@ -107,15 +129,42 @@ Fingerprint.parse = function (fp, options) {
return (p);
});
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));
try {
hash = Buffer.from(parts, 'hex');
} catch (e) {
throw (new FingerprintFormatError(fp));
}
} 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';
}

if (alg === undefined)
Expand All @@ -133,7 +182,8 @@ Fingerprint.parse = function (fp, options) {
return (new Fingerprint({
algorithm: alg,
hash: hash,
type: options.type || 'key'
type: options.type || 'key',
hashType: hashType
}));
};

Expand All @@ -159,8 +209,9 @@ Fingerprint.isFingerprint = function (obj, ver) {
* API versions for Fingerprint:
* [1,0] -- initial ver
* [1,1] -- first tagged ver
* [1,2] -- hashType and spki support
*/
Fingerprint.prototype._sshpkApiVersion = [1, 1];
Fingerprint.prototype._sshpkApiVersion = [1, 2];

Fingerprint._oldVersionDetect = function (obj) {
assert.func(obj.toString);
Expand Down
9 changes: 8 additions & 1 deletion lib/formats/pkcs8.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// Copyright 2015 Joyent, Inc.
// Copyright 2018 Joyent, Inc.

module.exports = {
read: read,
readPkcs8: readPkcs8,
write: write,
writePkcs8: writePkcs8,
pkcs8ToBuffer: pkcs8ToBuffer,

readECDSACurve: readECDSACurve,
writeECDSACurve: writeECDSACurve
Expand Down Expand Up @@ -412,6 +413,12 @@ function readPkcs8X25519Private(der) {
return (new PrivateKey(key));
}

function pkcs8ToBuffer(key) {
var der = new asn1.BerWriter();
writePkcs8(der, key);
return (der.buffer);
}

function writePkcs8(der, key) {
der.startSequence();

Expand Down
Loading

0 comments on commit 44aec4a

Please sign in to comment.