Browse Source

v1.2.0: add support for ssh private keys (EC and RSA)

master v1.2.0
AJ ONeal 5 years ago
parent
commit
29802c1af8
  1. 29
      README.md
  2. 9
      fixtures/privkey-ec-p256.openssh.pem
  3. 10
      fixtures/privkey-ec-p384.openssh.pem
  4. 27
      fixtures/privkey-rsa-2048.openssh.pem
  5. 1
      fixtures/pub-ec-p256.ssh.pub
  6. 1
      fixtures/pub-ec-p384.ssh.pub
  7. 1
      fixtures/pub-rsa-2048.ssh.pub
  8. 28
      lib/pem.js
  9. 108
      lib/ssh-parser.js
  10. 2
      package.json

29
README.md

@ -12,6 +12,9 @@ Features
* [x] SSH Public Keys
* fingerprint
* [x] SSH EC Private Keys
* [ ] SSH RSA Private Keys
* `dp` and `dq` values are unavailable
* [x] RSA Public Keys
* [x] EC Public Keys
* P-256 (prime256v1, secp256r1)
@ -29,9 +32,14 @@ Try one of these:
### Need SSH Private Keys?
SSH private keys are just normal PEM files,
Many SSH private keys are just normal PEM files,
so you can use Eckles or Rasha, as mentioned above.
As for the [OpenSSH-specific Private Keys](https://coolaj86.com/articles/the-openssh-private-key-format/),
EC is **fully supported**, but RSA has only partial support.
For more information see the "SSH Private Keys" section at the end of this file.
# CLI
You can install `ssh-to-jwk` and use it from command line:
@ -73,6 +81,25 @@ sshtojwk.fingerprint({ pub: pub }).then(function (fingerprint) {
});
```
# SSH Private Keys
As mentioned above, EC private keys are fully supported,
and RSA private keys are partially supported.
It's unlikely that we'll support full SSH-to-JWK conversion for private RSA keys
because OpenSSH omits the `dp` and `dq` values.
Although they are "optional" (they can be computed from the available values),
to compute them in JavaScript would require a large and expensive BigInt library -
and including (or writing) such a library would require contradicting the
"lightweight" and/or "zero dependency" goals for this library.
That said, for someone willing to include a BigInt library in their code
it should be trivial to perform the operations to derive `dp` and `dq`.
If that's you please open an issue because I am interested in creating
a `ssh-to-jwk-bigint` library... I just don't have a use case for it right now.
# Legal
[ssh-to-jwk.js](https://git.coolaj86.com/coolaj86/ssh-to-jwk.js) |

9
fixtures/privkey-ec-p256.openssh.pem

@ -0,0 +1,9 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQR9WZPeBSvixkhjQOh9yCXXlEx5CN9M
yh94CJJ1rigf8693gc90HmahIR5oMGHwlqMoS7kKrRw+4KpxqsF7LGvxAAAAqJZtgRuWbY
EbAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBH1Zk94FK+LGSGNA
6H3IJdeUTHkI30zKH3gIknWuKB/zr3eBz3QeZqEhHmgwYfCWoyhLuQqtHD7gqnGqwXssa/
EAAAAgBzKpRmMyXZ4jnSt3ARz0ul6R79AXAr5gQqDAmoFeEKwAAAAOYWpAYm93aWUubG9j
YWwBAg==
-----END OPENSSH PRIVATE KEY-----

10
fixtures/privkey-ec-p384.openssh.pem

@ -0,0 +1,10 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS
1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQTDNcLzjWUqG5H+grU/Z+RNOsvqH4V5
qU1UlkUzqTImYvm7ClYgYtXqbReCzLn1E+DOQw1N1f5E/YjPNduLlklsEv3q55k7BDTTiN
k5c15CpCbIV4eWeLRSFSJBGQHlv+sAAADYZYs6wGWLOsAAAAATZWNkc2Etc2hhMi1uaXN0
cDM4NAAAAAhuaXN0cDM4NAAAAGEEwzXC841lKhuR/oK1P2fkTTrL6h+FealNVJZFM6kyJm
L5uwpWIGLV6m0Xgsy59RPgzkMNTdX+RP2IzzXbi5ZJbBL96ueZOwQ004jZOXNeQqQmyFeH
lni0UhUiQRkB5b/rAAAAMQDbyZ7XRFtCCvdmdYJPkPuMzQBO5VJ1g/9eeFjI2ZLyIhtPh3
tvrki2EjEi8X4iLroAAAAOYWpAYm93aWUubG9jYWwB
-----END OPENSSH PRIVATE KEY-----

27
fixtures/privkey-rsa-2048.openssh.pem

@ -0,0 +1,27 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAQEA0zRlF7ykENrG3V3SC83iO7utM4gQx6gm62MVJHa6NCwPEqoppEHs
RynfUgVb68TZt5dS7AkZniIK8ZYcLqbbMoCvNK0V+SYrdgwkT+trcjASBCKi8QJLDBFtc+
jN1yHSI9o1pC+noukL3q9NyVreE38WRveGsUj8T2h2H+j7G/pWZzH2K1l7VQ/YSQOr0Iyj
apqVKNSNnVqunuhdznqXo37vQs9cjJxLDSRtwjrmyUl1JBHbCWEq1t8H1JzDwa5Z47PLj+
DQU4pPuUh5qW/qVN/tg44AuLbJ0yJIrrGiyKf6iZkvl9fKRc0QjMto319UHhzD7F5wUr3X
8iHWXyFbOQAAA8gqv28eKr9vHgAAAAdzc2gtcnNhAAABAQDTNGUXvKQQ2sbdXdILzeI7u6
0ziBDHqCbrYxUkdro0LA8SqimkQexHKd9SBVvrxNm3l1LsCRmeIgrxlhwuptsygK80rRX5
Jit2DCRP62tyMBIEIqLxAksMEW1z6M3XIdIj2jWkL6ei6Qver03JWt4TfxZG94axSPxPaH
Yf6Psb+lZnMfYrWXtVD9hJA6vQjKNqmpUo1I2dWq6e6F3Oepejfu9Cz1yMnEsNJG3COubJ
SXUkEdsJYSrW3wfUnMPBrlnjs8uP4NBTik+5SHmpb+pU3+2DjgC4tsnTIkiusaLIp/qJmS
+X18pFzRCMy2jfX1QeHMPsXnBSvdfyIdZfIVs5AAAAAwEAAQAAAQAUz+LuVd5s8sIJ6kba
du1GKZZFr7DHm+BJ7beVokVzAqxxkGcOEpjv4kZpVLHcJ8e0earoK3VkycH+UGZyimqrLV
cWf7/cj1BVD5k8btxloisEUU1xJmKyy7zXYSd3fZOxiL0kcrW4LfLHfMrTfqrHjQxq7dVN
/v0t7gNF3bVw6ipIqrO3Z+eDJYIhXZVtSPUmTke8XmAeYELX+IgmWuQTSxaQ8FlICEt86o
K4UNFQ9+i9K54X+lRRhIuqFAel1rXAGXpcMSsTWVTyRpVojunF/r9GzBOohYfgnQ0r/qf+
lxNgcUlqAxbWp+dR7BexfEn/Xi3M2peg5a1Op/jkPwupAAAAgG0XaH1ZeYlx88Pa5VMnq6
nZzTnSPS450JawMlFZH8TNQktfhPRd4+xeJB85uW1j57EudVZWOsV9NljwEx75FI81L7AG
3coPrAmE8OWkxUsxtg+gNMja19wmh5x7tDfBo+mv4XxMydRQ51EXn1BMo4EcAZf27GJLkN
yZH4bCBCjDAAAAgQD6rSj9fGzfHVz0eMJ7FHpA/FX5ZZ90ERBweMVCLCvTjkc9r2AFTT9J
Lp2g5vvF4pYO6T+pJEXW98AEteQPNj2MM8WZOmTa1x7FPY9m7VjRjSH4L2dALMF94NMoa/
4mO7glFtKquGT3TtwxqOmic/jtBhipfgZG906dtYeLOg2KSwAAAIEA17CkB+asl/lX0vie
ylkIseZm74mQutso+bhHGNLx/VEbtn2EV3hiLfslsd4yxnMPTFdZKUTw+sPksYO8/0/3H2
T18q8XTOm7rs9N5ahWHMmfx+shBpub2e8Z23tk60Hk5O1lzSBH5iktC9mE86BwpohZ1uvp
JffialjvTR5C/gsAAAAOYWpAYm93aWUubG9jYWwBAgMEBQ==
-----END OPENSSH PRIVATE KEY-----

1
fixtures/pub-ec-p256.ssh.pub

@ -0,0 +1 @@
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBH1Zk94FK+LGSGNA6H3IJdeUTHkI30zKH3gIknWuKB/zr3eBz3QeZqEhHmgwYfCWoyhLuQqtHD7gqnGqwXssa/E= aj@bowie.local

1
fixtures/pub-ec-p384.ssh.pub

@ -0,0 +1 @@
ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBMM1wvONZSobkf6CtT9n5E06y+ofhXmpTVSWRTOpMiZi+bsKViBi1eptF4LMufUT4M5DDU3V/kT9iM8124uWSWwS/ernmTsENNOI2TlzXkKkJshXh5Z4tFIVIkEZAeW/6w== aj@bowie.local

1
fixtures/pub-rsa-2048.ssh.pub

@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDTNGUXvKQQ2sbdXdILzeI7u60ziBDHqCbrYxUkdro0LA8SqimkQexHKd9SBVvrxNm3l1LsCRmeIgrxlhwuptsygK80rRX5Jit2DCRP62tyMBIEIqLxAksMEW1z6M3XIdIj2jWkL6ei6Qver03JWt4TfxZG94axSPxPaHYf6Psb+lZnMfYrWXtVD9hJA6vQjKNqmpUo1I2dWq6e6F3Oepejfu9Cz1yMnEsNJG3COubJSXUkEdsJYSrW3wfUnMPBrlnjs8uP4NBTik+5SHmpb+pU3+2DjgC4tsnTIkiusaLIp/qJmS+X18pFzRCMy2jfX1QeHMPsXnBSvdfyIdZfIVs5 aj@bowie.local

28
lib/pem.js

@ -0,0 +1,28 @@
'use strict';
var PEM = module.exports;
var Enc = require('./encoding.js');
PEM.parseBlock = function pemToDer(pem) {
var lines = pem.trim().split(/\n/);
var end = lines.length - 1;
var head = lines[0].match(/-----BEGIN (.*)-----/);
var foot = lines[end].match(/-----END (.*)-----/);
if (head) {
lines = lines.slice(1, end);
head = head[1];
if (head !== foot[1]) {
throw new Error("headers and footers do not match");
}
}
return { type: head, bytes: Enc.base64ToBuf(lines.join('')) };
};
PEM.packBlock = function (opts) {
return '-----BEGIN ' + opts.type + '-----\n'
+ Enc.bufToBase64(opts.bytes).match(/.{1,64}/g).join('\n') + '\n'
+ '-----END ' + opts.type + '-----'
;
};

108
lib/ssh-parser.js

@ -2,11 +2,20 @@
var SSH = module.exports;
var Enc = require('./encoding.js');
var PEM = require('./pem.js');
SSH.parse = function (opts) {
var pub = opts.pub || opts;
var ssh = SSH.parseBlock(pub);
ssh = SSH.parseElements(ssh);
if ('OPENSSH PRIVATE KEY' === ssh.type) {
ssh = SSH.parsePrivateElements(ssh);
if (opts.public) {
ssh.elements = ssh.elements.slice(0, 3);
}
} else {
ssh.elements = SSH.parseElements(ssh.bytes);
}
//delete ssh.bytes;
return SSH.parsePublicKey(ssh);
};
@ -27,6 +36,9 @@ SSH.fingerprint = function (opts) {
};
SSH.parseBlock = function (ssh) {
if (/^-----BEGIN OPENSSH PRIVATE KEY-----/.test(ssh)) {
return PEM.parseBlock(ssh);
}
ssh = ssh.split(/\s+/g);
return {
@ -36,10 +48,69 @@ SSH.parseBlock = function (ssh) {
};
};
SSH.parseElements = function (ssh) {
SSH.parsePrivateElements = function (ssh) {
// https://coolaj86.com/articles/the-openssh-private-key-format/
var buf = ssh.bytes;
var fulllen = buf.byteLength || buf.length;
var offset = (buf.byteOffset || 0);
var dv = new DataView(buf.buffer.slice(offset, offset + fulllen));
var index = 0;
var padlen = 0;
var len;
// The last byte will be either
// * a non-printable pad character
// * a printable comment character
function lastByteIsPad() {
var n = ssh.bytes[(ssh.bytes.bytesLength || ssh.bytes.length) - 1];
return n >= 0x01 && n <= 0x07;
}
while (lastByteIsPad()) {
padlen += 1;
len = (ssh.bytes.bytesLength || ssh.bytes.length);
ssh.bytes = ssh.bytes.slice(0, len - 1);
}
// o p e n s s h - k e y - v 1 NULL
// 6f 70 65 6e 73 73 68 2d 6b 65 79 2d 76 31 00
// 15 characters
// 4-byte len, "none" (encryption)
// 4-byte len, "none" (kdfname)
if ('none' !== Enc.bufToBin(ssh.bytes.slice(15 + 8 + 4, 15 + 8 + 8))) {
throw new Error("Key is either encrypted (not yet supported), corrupt, or not openssh-key-v1");
}
if (padlen >= 8) {
throw new Error("Padding length should be between 0 and 7, not '" + padlen + "'."
+ " Probably not an ssh private key.");
}
// 4-byte len, nil (kdf)
// 4-byte number of keys
index += 15 + 8 + 8 + 4 + 4;
// length of public key
len = dv.getUint32(index, false);
// throw away public key (it's in the private key)
index += 4 + len;
// length of dummy checksum + private key + padding
len = dv.getUint32(index, false) - padlen;
// throw away dummy checksum
index += 4 + 8;
ssh.elements = SSH.parseElements(ssh.bytes.slice(index, index + (len - 8)));
index += Array.prototype.reduce.call(ssh.elements, function (el, sum) {
// 32-bit len + element len
return 4 + (el.byteLength || el.length) + sum;
}, 0);
// comment will exist, even if it's an empty string
ssh.comment = Enc.bufToBin(ssh.elements.pop());
return ssh;
};
SSH.parseElements = function (buf) {
var fulllen = buf.byteLength || buf.length;
// Note: node has weird offsets
var offset = (buf.byteOffset || 0);
var i = 0;
var index = 0;
// using dataview to be browser-compatible (I do want _some_ code reuse)
@ -53,6 +124,7 @@ SSH.parseElements = function (ssh) {
if (i > 15) { throw new Error("15+ elements, probably not a public ssh key"); }
len = dv.getUint32(index, false);
index += 4;
if (0 === len) { continue; }
el = buf.slice(index, index + len);
// remove BigUInt '00' prefix
if (0x00 === el[0]) {
@ -67,8 +139,7 @@ SSH.parseElements = function (ssh) {
}).join('\n'));
}
ssh.elements = els;
return ssh;
return els;
};
SSH.parsePublicKey = function (ssh) {
@ -78,11 +149,26 @@ SSH.parsePublicKey = function (ssh) {
// RSA keys are all the same
if (SSH.types.rsa === typ) {
ssh.jwk = {
kty: 'RSA'
, n: Enc.bufToUrlBase64(els[2])
, e: Enc.bufToUrlBase64(els[1])
};
if (3 === els.length) {
ssh.jwk = {
kty: 'RSA'
, n: Enc.bufToUrlBase64(els[2])
, e: Enc.bufToUrlBase64(els[1])
};
} else {
console.log('len:', els.length);
ssh.jwk = {
kty: 'RSA'
, n: Enc.bufToUrlBase64(els[2])
, e: Enc.bufToUrlBase64(els[1])
, d: Enc.bufToUrlBase64(els[3])
, p: Enc.bufToUrlBase64(els[5])
, q: Enc.bufToUrlBase64(els[6])
//, dp: Enc.bufToUrlBase64(els[x])
//, dq: Enc.bufToUrlBase64(els[x])
, qi: Enc.bufToUrlBase64(els[4])
};
}
return ssh;
}
@ -101,11 +187,15 @@ SSH.parsePublicKey = function (ssh) {
// els[1] is just a repeat of a subset of els[0]
var x = els[2].slice(1, 1 + len);
var y = els[2].slice(1 + len, 1 + len + len);
var d;
// I don't think EC keys use 0x00 padding, but just in case
while (0x00 === x[0]) { x = x.slice(1); }
while (0x00 === y[0]) { y = y.slice(1); }
if (els[3]) {
ssh.jwk.d = Enc.bufToUrlBase64(els[3]);
}
ssh.jwk.x = Enc.bufToUrlBase64(x);
ssh.jwk.y = Enc.bufToUrlBase64(y);

2
package.json

@ -1,6 +1,6 @@
{
"name": "ssh-to-jwk",
"version": "1.1.0",
"version": "1.2.0",
"description": "💯 SSH to JWK in a lightweight, zero-dependency library.",
"homepage": "https://git.coolaj86.com/coolaj86/ssh-to-jwk.js",
"main": "index.js",

Loading…
Cancel
Save