From e3bd35470ef25106044ec32809622d93d3751d94 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 20 Nov 2018 10:43:47 -0700 Subject: [PATCH] v1.1.0: support ssh public key out --- README.md | 17 ++++++++++++ fixtures/pub-ec-p256.ssh.pub | 1 + fixtures/pub-ec-p384.ssh.pub | 1 + lib/eckles.js | 54 +++++++++++++++++++++++++++--------- package.json | 2 +- test.sh | 10 +++++++ 6 files changed, 71 insertions(+), 14 deletions(-) create mode 100644 fixtures/pub-ec-p256.ssh.pub create mode 100644 fixtures/pub-ec-p384.ssh.pub diff --git a/README.md b/README.md index 53bfe21..0e9c059 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ eckles.import({ pem: pem }).then(function (jwk) { * [x] SEC1/X9.62, PKCS#8, SPKI/PKIX * [x] P-256 (prime256v1, secp256r1), P-384 (secp384r1) +* [x] SSH (RFC4716), (RFC 4716/SSH2) ```js var eckles = require('eckles'); @@ -76,6 +77,22 @@ eckles.export({ jwk: jwk, format: 'pkcs8' }).then(function (pem) { }); ``` +`format: 'ssh'`: + +Although SSH uses SEC1 for private keys, it uses ts own special non-ASN1 format +(affectionately known as rfc4716) for public keys. I got curious and then decided +to add this format as well. + +To get the same format as you +would get with `ssh-keygen`, pass `ssh` as the format option: + +```js +eckles.export({ jwk: jwk, format: 'ssh' }).then(function (pub) { + // Special SSH2 Public Key format (RFC 4716) + console.log(pub); +}); +``` + `public: 'true'`: If a private key is used as input, a private key will be output. diff --git a/fixtures/pub-ec-p256.ssh.pub b/fixtures/pub-ec-p256.ssh.pub new file mode 100644 index 0000000..1d87fdd --- /dev/null +++ b/fixtures/pub-ec-p256.ssh.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCE9Uli8bGnD4hOWdeo5KKQJ/P/vOazI4MgqJK54w37emP2JwOAOdMmXuwpxbKng3KZz27mz+nKWIlXJ3rzSGMo= P-256@localhost diff --git a/fixtures/pub-ec-p384.ssh.pub b/fixtures/pub-ec-p384.ssh.pub new file mode 100644 index 0000000..b07ea6b --- /dev/null +++ b/fixtures/pub-ec-p384.ssh.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBNsxFNGygmu3oyiyCfKDxpy4aoccor+P8N/CtmtEjwunbEnff4JTSfJXKr9LH3+Rm1Q+I57vN0urHJ+v03gI9xGTkRBmzrOnc6FaWroJ/l0DDMgvTuFS2wwxRgWUyZTLGw== P-384@localhost diff --git a/lib/eckles.js b/lib/eckles.js index fd60fc1..d07aa9a 100644 --- a/lib/eckles.js +++ b/lib/eckles.js @@ -150,11 +150,11 @@ EC.parseSec1 = function parseEcOnlyPrivkey(u8, jwk) { kty: jwk.kty , crv: jwk.crv , d: PEM._toUrlSafeBase64(d) - //, dh: d + //, dh: toHex(d) , x: PEM._toUrlSafeBase64(x) - //, xh: x + //, xh: toHex(x) , y: PEM._toUrlSafeBase64(y) - //, yh: y + //, yh: toHex(y) }; }; @@ -187,11 +187,11 @@ EC.parsePkcs8 = function parseEcPkcs8(u8, jwk) { kty: jwk.kty , crv: jwk.crv , d: PEM._toUrlSafeBase64(d) - //, dh: d + //, dh: toHex(d) , x: PEM._toUrlSafeBase64(x) - //, xh: x + //, xh: toHex(x) , y: PEM._toUrlSafeBase64(y) - //, yh: y + //, yh: toHex(y) }; }; @@ -219,9 +219,9 @@ EC.parseSpki = function parsePem(u8, jwk) { kty: jwk.kty , crv: jwk.crv , x: PEM._toUrlSafeBase64(x) - //, xh: x + //, xh: toHex(x) , y: PEM._toUrlSafeBase64(y) - //, yh: y + //, yh: toHex(y) }; }; EC.parsePkix = EC.parseSpki; @@ -304,17 +304,20 @@ EC.pack = function (opts) { } var jwk = JSON.parse(JSON.stringify(opts.jwk)); var format = opts.format; - if (opts.public || -1 !== [ 'spki', 'pkix' ].indexOf(format)) { + if (opts.public || -1 !== [ 'spki', 'pkix', 'ssh', 'rfc4716' ].indexOf(format)) { jwk.d = null; } if ('EC' !== jwk.kty) { throw new Error("options.jwk.kty must be 'EC' for EC keys"); } if (!jwk.d) { - if (!format || 'pkix' === format) { + if (!format || -1 !== [ 'spki', 'pkix' ].indexOf(format)) { format = 'spki'; - } else if ('spki' !== format) { - throw new Error("options.format must be 'spki' for public EC keys"); + } else if (-1 !== [ 'ssh', 'rfc4716' ].indexOf(format)) { + format = 'ssh'; + } else { + throw new Error("options.format must be 'spki' or 'ssh' for public EC keys, not (" + + typeof format + ") " + format); } } else { if (!format || 'sec1' === format) { @@ -334,8 +337,12 @@ EC.pack = function (opts) { return PEM.packBlock({ type: "EC PRIVATE KEY", bytes: EC.packSec1(jwk) }); } else if ('pkcs8' === format) { return PEM.packBlock({ type: "EC PRIVATE KEY", bytes: EC.packPkcs8(jwk) }); - } else { + } else if (-1 !== [ 'spki', 'pkix' ].indexOf(format)) { return PEM.packBlock({ type: "PUBLIC KEY", bytes: EC.packSpki(jwk) }); + } else if (-1 !== [ 'ssh', 'rfc4716' ].indexOf(format)) { + return EC.packSsh(jwk); + } else { + throw new Error("Sanity Error: reached unreachable code block with format: " + format); } }); }; @@ -386,6 +393,27 @@ EC.packSpki = function (jwk) { ); }; EC.packPkix = EC.packSpki; +EC.packSsh = function (jwk) { + // Custom SSH format + var typ = 'ecdsa-sha2-nistp256'; + var a = '32 35 36'; + var b = '41'; + var comment = jwk.crv + '@localhost'; + if ('P-256' !== jwk.crv) { + typ = 'ecdsa-sha2-nistp384'; + a = '33 38 34'; + b = '61'; + } + var x = toHex(base64ToUint8(urlBase64ToBase64(jwk.x))); + var y = toHex(base64ToUint8(urlBase64ToBase64(jwk.y))); + var ssh = Hex.toUint8( + ('00 00 00 13 65 63 64 73 61 2d 73 68 61 32 2d 6e 69 73 74 70' + + a + '00 00 00 08 6e 69 73 74 70' + a + '00 00 00' + b + + '04' + x + y).replace(/\s+/g, '').toLowerCase() + ); + + return typ + ' ' + toBase64(ssh) + ' ' + comment; +}; // // A dumbed-down, minimal ASN.1 packer diff --git a/package.json b/package.json index b539a26..e8ae525 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eckles", - "version": "1.0.1", + "version": "1.1.0", "description": "PEM-to-JWK and JWK-to-PEM for ECDSA keys in a lightweight, zero-dependency library focused on perfect universal compatibility.", "homepage": "https://git.coolaj86.com/coolaj86/eckles.js", "main": "index.js", diff --git a/test.sh b/test.sh index 2f2460d..37b4a4b 100644 --- a/test.sh +++ b/test.sh @@ -29,10 +29,15 @@ echo "Testing JWK-to-PEM P-256" echo "" node bin/eckles.js fixtures/privkey-ec-p256.jwk.json sec1 | tee fixtures/privkey-ec-p256.sec1.pem.2 diff fixtures/privkey-ec-p256.sec1.pem fixtures/privkey-ec-p256.sec1.pem.2 +# node bin/eckles.js fixtures/privkey-ec-p256.jwk.json pkcs8 | tee fixtures/privkey-ec-p256.pkcs8.pem.2 diff fixtures/privkey-ec-p256.pkcs8.pem fixtures/privkey-ec-p256.pkcs8.pem.2 +# node bin/eckles.js fixtures/pub-ec-p256.jwk.json spki | tee fixtures/pub-ec-p256.spki.pem.2 diff fixtures/pub-ec-p256.spki.pem fixtures/pub-ec-p256.spki.pem.2 +# ssh-keygen -f fixtures/pub-ec-p256.spki.pem -i -mPKCS8 > fixtures/pub-ec-p256.ssh.pub +node bin/eckles.js fixtures/pub-ec-p256.jwk.json ssh | tee fixtures/pub-ec-p256.ssh.pub.2 +diff fixtures/pub-ec-p256.ssh.pub fixtures/pub-ec-p256.ssh.pub.2 echo "" echo "" @@ -40,10 +45,15 @@ echo "Testing JWK-to-PEM P-384" echo "" node bin/eckles.js fixtures/privkey-ec-p384.jwk.json sec1 | tee fixtures/privkey-ec-p384.sec1.pem.2 diff fixtures/privkey-ec-p384.sec1.pem fixtures/privkey-ec-p384.sec1.pem.2 +# node bin/eckles.js fixtures/privkey-ec-p384.jwk.json pkcs8 | tee fixtures/privkey-ec-p384.pkcs8.pem.2 diff fixtures/privkey-ec-p384.pkcs8.pem fixtures/privkey-ec-p384.pkcs8.pem.2 +# node bin/eckles.js fixtures/pub-ec-p384.jwk.json spki | tee fixtures/pub-ec-p384.spki.pem.2 diff fixtures/pub-ec-p384.spki.pem fixtures/pub-ec-p384.spki.pem.2 +# ssh-keygen -f fixtures/pub-ec-p384.spki.pem -i -mPKCS8 > fixtures/pub-ec-p384.ssh.pub +node bin/eckles.js fixtures/pub-ec-p384.jwk.json ssh | tee fixtures/pub-ec-p384.ssh.pub.2 +diff fixtures/pub-ec-p384.ssh.pub fixtures/pub-ec-p384.ssh.pub.2 rm fixtures/*.2