Skip to content

Commit 44aec4a

Browse files
author
Alex Wilson
committed
#59 want support for SPKI fingerprint format
Reviewed by: Isaac Davis <[email protected]> Reviewed by: Cody Peter Mello <[email protected]>
1 parent 385ff11 commit 44aec4a

File tree

8 files changed

+220
-54
lines changed

8 files changed

+220
-54
lines changed

README.md

+23-9
Original file line numberDiff line numberDiff line change
@@ -174,14 +174,17 @@ Parameters
174174

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

177-
### `Key#fingerprint([algorithm = 'sha256'])`
177+
### `Key#fingerprint([algorithm = 'sha256'[, hashType = 'ssh']])`
178178

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

181181
Parameters
182182

183183
- `algorithm` -- String name of hash algorithm to use, valid options are `md5`,
184184
`sha1`, `sha256`, `sha384`, `sha512`
185+
- `hashType` -- String name of fingerprint hash type to use, valid options are
186+
`ssh` (the type of fingerprint used by OpenSSH, e.g. in
187+
`ssh-keygen`), `spki` (used by HPKP, some OpenSSL applications)
185188

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

@@ -333,17 +336,23 @@ Parameters
333336

334337
## Fingerprints
335338

336-
### `parseFingerprint(fingerprint[, algorithms])`
339+
### `parseFingerprint(fingerprint[, options])`
337340

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

341344
Parameters
342345

343346
- `fingerprint` -- String, the fingerprint value, in any supported format
344-
- `algorithms` -- Optional list of strings, names of hash algorithms to limit
345-
support to. If `fingerprint` uses a hash algorithm not on
346-
this list, throws `InvalidAlgorithmError`.
347+
- `options` -- Optional Object, with properties:
348+
- `algorithms` -- Array of strings, names of hash algorithms to limit
349+
support to. If `fingerprint` uses a hash algorithm not on
350+
this list, throws `InvalidAlgorithmError`.
351+
- `hashType` -- String, the type of hash the fingerprint uses, either `ssh`
352+
or `spki` (normally auto-detected based on the format, but
353+
can be overridden)
354+
- `type` -- String, the entity this fingerprint identifies, either `key` or
355+
`certificate`
347356

348357
### `Fingerprint.isFingerprint(obj)`
349358

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

367-
### `Fingerprint#matches(key)`
376+
### `Fingerprint#matches(keyOrCertificate)`
368377

369-
Verifies whether or not this `Fingerprint` matches a given `Key`. This function
370-
uses double-hashing to avoid leaking timing information. Returns a boolean.
378+
Verifies whether or not this `Fingerprint` matches a given `Key` or
379+
`Certificate`. This function uses double-hashing to avoid leaking timing
380+
information. Returns a boolean.
381+
382+
Note that a `Key`-type Fingerprint will always return `false` if asked to match
383+
a `Certificate` and vice versa.
371384

372385
Parameters
373386

374-
- `key` -- a `Key` object, the key to match this fingerprint against
387+
- `keyOrCertificate` -- a `Key` object or `Certificate` object, the entity to
388+
match this fingerprint against
375389

376390
## Signatures
377391

bin/sshpk-conv

+53-17
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env node
22
// -*- mode: js -*-
33
// vim: set filetype=javascript :
4-
// Copyright 2015 Joyent, Inc. All rights reserved.
4+
// Copyright 2018 Joyent, Inc. All rights reserved.
55

66
var dashdash = require('dashdash');
77
var sshpk = require('../lib/index');
@@ -47,6 +47,21 @@ var options = [
4747
type: 'bool',
4848
help: 'Print key metadata instead of converting'
4949
},
50+
{
51+
names: ['fingerprint', 'F'],
52+
type: 'bool',
53+
help: 'Output key fingerprint'
54+
},
55+
{
56+
names: ['hash', 'H'],
57+
type: 'string',
58+
help: 'Hash function to use for key fingeprint with -F'
59+
},
60+
{
61+
names: ['spki', 's'],
62+
type: 'bool',
63+
help: 'With -F, generates an SPKI fingerprint instead of SSH'
64+
},
5065
{
5166
names: ['comment', 'c'],
5267
type: 'string',
@@ -75,13 +90,17 @@ if (require.main === module) {
7590
var help = parser.help({}).trimRight();
7691
console.error('sshpk-conv: converts between SSH key formats\n');
7792
console.error(help);
78-
console.error('\navailable formats:');
93+
console.error('\navailable key formats:');
7994
console.error(' - pem, pkcs1 eg id_rsa');
8095
console.error(' - ssh eg id_rsa.pub');
8196
console.error(' - pkcs8 format you want for openssl');
8297
console.error(' - openssh like output of ssh-keygen -o');
8398
console.error(' - rfc4253 raw OpenSSH wire format');
8499
console.error(' - dnssec dnssec-keygen format');
100+
console.error('\navailable fingerprint formats:');
101+
console.error(' - hex colon-separated hex for SSH');
102+
console.error(' straight hex for SPKI');
103+
console.error(' - base64 SHA256:* format from OpenSSH');
85104
process.exit(1);
86105
}
87106

@@ -172,18 +191,7 @@ if (require.main === module) {
172191
if (opts.comment)
173192
key.comment = opts.comment;
174193

175-
if (!opts.identify) {
176-
fmt = undefined;
177-
if (opts.outformat)
178-
fmt = opts.outformat;
179-
outFile.write(key.toBuffer(fmt));
180-
if (fmt === 'ssh' ||
181-
(!opts.private && fmt === undefined))
182-
outFile.write('\n');
183-
outFile.once('drain', function () {
184-
process.exit(0);
185-
});
186-
} else {
194+
if (opts.identify) {
187195
var kind = 'public';
188196
if (sshpk.PrivateKey.isPrivateKey(key))
189197
kind = 'private';
@@ -193,10 +201,38 @@ if (require.main === module) {
193201
console.log('ECDSA curve: %s', key.curve);
194202
if (key.comment)
195203
console.log('Comment: %s', key.comment);
196-
console.log('Fingerprint:');
197-
console.log(' ' + key.fingerprint().toString());
198-
console.log(' ' + key.fingerprint('md5').toString());
204+
console.log('SHA256 fingerprint: ' +
205+
key.fingerprint('sha256').toString());
206+
console.log('MD5 fingerprint: ' +
207+
key.fingerprint('md5').toString());
208+
console.log('SPKI-SHA256 fingerprint: ' +
209+
key.fingerprint('sha256', 'spki').toString());
199210
process.exit(0);
211+
return;
200212
}
213+
214+
if (opts.fingerprint) {
215+
var hash = opts.hash;
216+
var type = opts.spki ? 'spki' : 'ssh';
217+
var format = opts.outformat;
218+
var fp = key.fingerprint(hash, type).toString(format);
219+
outFile.write(fp);
220+
outFile.write('\n');
221+
outFile.once('drain', function () {
222+
process.exit(0);
223+
});
224+
return;
225+
}
226+
227+
fmt = undefined;
228+
if (opts.outformat)
229+
fmt = opts.outformat;
230+
outFile.write(key.toBuffer(fmt));
231+
if (fmt === 'ssh' ||
232+
(!opts.private && fmt === undefined))
233+
outFile.write('\n');
234+
outFile.once('drain', function () {
235+
process.exit(0);
236+
});
201237
});
202238
}

lib/fingerprint.js

+62-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2015 Joyent, Inc.
1+
// Copyright 2018 Joyent, Inc.
22

33
module.exports = Fingerprint;
44

@@ -8,6 +8,7 @@ var algs = require('./algs');
88
var crypto = require('crypto');
99
var errs = require('./errors');
1010
var Key = require('./key');
11+
var PrivateKey = require('./private-key');
1112
var Certificate = require('./certificate');
1213
var utils = require('./utils');
1314

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

2728
this.hash = opts.hash;
2829
this.type = opts.type;
30+
this.hashType = opts.hashType;
2931
}
3032

3133
Fingerprint.prototype.toString = function (format) {
3234
if (format === undefined) {
33-
if (this.algorithm === 'md5')
35+
if (this.algorithm === 'md5' || this.hashType === 'spki')
3436
format = 'hex';
3537
else
3638
format = 'base64';
@@ -39,8 +41,12 @@ Fingerprint.prototype.toString = function (format) {
3941

4042
switch (format) {
4143
case 'hex':
44+
if (this.hashType === 'spki')
45+
return (this.hash.toString('hex'));
4246
return (addColons(this.hash.toString('hex')));
4347
case 'base64':
48+
if (this.hashType === 'spki')
49+
return (this.hash.toString('base64'));
4450
return (sshBase64Format(this.algorithm,
4551
this.hash.toString('base64')));
4652
default:
@@ -50,14 +56,20 @@ Fingerprint.prototype.toString = function (format) {
5056

5157
Fingerprint.prototype.matches = function (other) {
5258
assert.object(other, 'key or certificate');
53-
if (this.type === 'key') {
59+
if (this.type === 'key' && this.hashType !== 'ssh') {
60+
utils.assertCompatible(other, Key, [1, 7], 'key with spki');
61+
if (PrivateKey.isPrivateKey(other)) {
62+
utils.assertCompatible(other, PrivateKey, [1, 6],
63+
'privatekey with spki support');
64+
}
65+
} else if (this.type === 'key') {
5466
utils.assertCompatible(other, Key, [1, 0], 'key');
5567
} else {
5668
utils.assertCompatible(other, Certificate, [1, 0],
5769
'certificate');
5870
}
5971

60-
var theirHash = other.hash(this.algorithm);
72+
var theirHash = other.hash(this.algorithm, this.hashType);
6173
var theirHash2 = crypto.createHash(this.algorithm).
6274
update(theirHash).digest('base64');
6375

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

83+
/*JSSTYLED*/
84+
var base64RE = /^[A-Za-z0-9+\/=]+$/;
85+
/*JSSTYLED*/
86+
var hexRE = /^[a-fA-F0-9]+$/;
87+
7188
Fingerprint.parse = function (fp, options) {
7289
assert.string(fp, 'fingerprint');
7390

@@ -81,13 +98,18 @@ Fingerprint.parse = function (fp, options) {
8198
options = {};
8299
if (options.enAlgs !== undefined)
83100
enAlgs = options.enAlgs;
101+
if (options.algorithms !== undefined)
102+
enAlgs = options.algorithms;
84103
assert.optionalArrayOfString(enAlgs, 'algorithms');
85104

105+
var hashType = 'ssh';
106+
if (options.hashType !== undefined)
107+
hashType = options.hashType;
108+
assert.string(hashType, 'options.hashType');
109+
86110
var parts = fp.split(':');
87111
if (parts.length == 2) {
88112
alg = parts[0].toLowerCase();
89-
/*JSSTYLED*/
90-
var base64RE = /^[A-Za-z0-9+\/=]+$/;
91113
if (!base64RE.test(parts[1]))
92114
throw (new FingerprintFormatError(fp));
93115
try {
@@ -107,15 +129,42 @@ Fingerprint.parse = function (fp, options) {
107129
return (p);
108130
});
109131
parts = parts.join('');
110-
/*JSSTYLED*/
111-
var md5RE = /^[a-fA-F0-9]+$/;
112-
if (!md5RE.test(parts) || parts.length % 2 !== 0)
132+
if (!hexRE.test(parts) || parts.length % 2 !== 0)
113133
throw (new FingerprintFormatError(fp));
114134
try {
115135
hash = Buffer.from(parts, 'hex');
116136
} catch (e) {
117137
throw (new FingerprintFormatError(fp));
118138
}
139+
} else {
140+
if (hexRE.test(fp)) {
141+
hash = Buffer.from(fp, 'hex');
142+
} else if (base64RE.test(fp)) {
143+
hash = Buffer.from(fp, 'base64');
144+
} else {
145+
throw (new FingerprintFormatError(fp));
146+
}
147+
148+
switch (hash.length) {
149+
case 32:
150+
alg = 'sha256';
151+
break;
152+
case 16:
153+
alg = 'md5';
154+
break;
155+
case 20:
156+
alg = 'sha1';
157+
break;
158+
case 64:
159+
alg = 'sha512';
160+
break;
161+
default:
162+
throw (new FingerprintFormatError(fp));
163+
}
164+
165+
/* Plain hex/base64: guess it's probably SPKI unless told. */
166+
if (options.hashType === undefined)
167+
hashType = 'spki';
119168
}
120169

121170
if (alg === undefined)
@@ -133,7 +182,8 @@ Fingerprint.parse = function (fp, options) {
133182
return (new Fingerprint({
134183
algorithm: alg,
135184
hash: hash,
136-
type: options.type || 'key'
185+
type: options.type || 'key',
186+
hashType: hashType
137187
}));
138188
};
139189

@@ -159,8 +209,9 @@ Fingerprint.isFingerprint = function (obj, ver) {
159209
* API versions for Fingerprint:
160210
* [1,0] -- initial ver
161211
* [1,1] -- first tagged ver
212+
* [1,2] -- hashType and spki support
162213
*/
163-
Fingerprint.prototype._sshpkApiVersion = [1, 1];
214+
Fingerprint.prototype._sshpkApiVersion = [1, 2];
164215

165216
Fingerprint._oldVersionDetect = function (obj) {
166217
assert.func(obj.toString);

lib/formats/pkcs8.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
// Copyright 2015 Joyent, Inc.
1+
// Copyright 2018 Joyent, Inc.
22

33
module.exports = {
44
read: read,
55
readPkcs8: readPkcs8,
66
write: write,
77
writePkcs8: writePkcs8,
8+
pkcs8ToBuffer: pkcs8ToBuffer,
89

910
readECDSACurve: readECDSACurve,
1011
writeECDSACurve: writeECDSACurve
@@ -412,6 +413,12 @@ function readPkcs8X25519Private(der) {
412413
return (new PrivateKey(key));
413414
}
414415

416+
function pkcs8ToBuffer(key) {
417+
var der = new asn1.BerWriter();
418+
writePkcs8(der, key);
419+
return (der.buffer);
420+
}
421+
415422
function writePkcs8(der, key) {
416423
der.startSequence();
417424

0 commit comments

Comments
 (0)