v1.2.0: add support for ssh private keys (EC and RSA)
Esse commit está contido em:
pai
6b6fd5e01d
commit
29802c1af8
29
README.md
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) |
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
|
||||
1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQR9WZPeBSvixkhjQOh9yCXXlEx5CN9M
|
||||
yh94CJJ1rigf8693gc90HmahIR5oMGHwlqMoS7kKrRw+4KpxqsF7LGvxAAAAqJZtgRuWbY
|
||||
EbAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBH1Zk94FK+LGSGNA
|
||||
6H3IJdeUTHkI30zKH3gIknWuKB/zr3eBz3QeZqEhHmgwYfCWoyhLuQqtHD7gqnGqwXssa/
|
||||
EAAAAgBzKpRmMyXZ4jnSt3ARz0ul6R79AXAr5gQqDAmoFeEKwAAAAOYWpAYm93aWUubG9j
|
||||
YWwBAg==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
|
@ -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-----
|
|
@ -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-----
|
|
@ -0,0 +1 @@
|
|||
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBH1Zk94FK+LGSGNA6H3IJdeUTHkI30zKH3gIknWuKB/zr3eBz3QeZqEhHmgwYfCWoyhLuQqtHD7gqnGqwXssa/E= aj@bowie.local
|
|
@ -0,0 +1 @@
|
|||
ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBMM1wvONZSobkf6CtT9n5E06y+ofhXmpTVSWRTOpMiZi+bsKViBi1eptF4LMufUT4M5DDU3V/kT9iM8124uWSWwS/ernmTsENNOI2TlzXkKkJshXh5Z4tFIVIkEZAeW/6w== aj@bowie.local
|
|
@ -0,0 +1 @@
|
|||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDTNGUXvKQQ2sbdXdILzeI7u60ziBDHqCbrYxUkdro0LA8SqimkQexHKd9SBVvrxNm3l1LsCRmeIgrxlhwuptsygK80rRX5Jit2DCRP62tyMBIEIqLxAksMEW1z6M3XIdIj2jWkL6ei6Qver03JWt4TfxZG94axSPxPaHYf6Psb+lZnMfYrWXtVD9hJA6vQjKNqmpUo1I2dWq6e6F3Oepejfu9Cz1yMnEsNJG3COubJSXUkEdsJYSrW3wfUnMPBrlnjs8uP4NBTik+5SHmpb+pU3+2DjgC4tsnTIkiusaLIp/qJmS+X18pFzRCMy2jfX1QeHMPsXnBSvdfyIdZfIVs5 aj@bowie.local
|
|
@ -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 + '-----'
|
||||
;
|
||||
};
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
Carregando…
Referência em uma nova issue