PatrickRoumanoff / js-keygen

ssh-keygen in the browser

Home Page:https://js-keygen.surge.sh/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

trying to convert between key formats

getify opened this issue · comments

Just found your site and this code, really appreciate you putting this out there.

I'm trying to read a public ssh key format file in the browser, and convert it to the PEM public key format. It feels like the pieces are there in your code to do that, but I can't quite figure it out.

Ideally, this would be done without the web crypto API methods, but I'm not sure if that's possible, so if the API has to be used, that's OK too.

Any pointers you could give would be super appreciated!

Hey Kyle,

Thanks for reaching out, I am a big fan of YDKJS.

There is an accompanying blog post at

that points to this article on loading and decoding openSSH public key format:

which in turns point to the RFC describing the format in details

They use python, but the format descriptions was enough for me to get started.

updating the code style to ES6 I found out that there is a method called decodePublicKey in ssh-util.js which reads an open ssh public key to its core elements which should allow to write a PEM encoded file. And there is no WebCryptoApi calls.

I saw that as well, but I couldn't figure out what to do with those raw elements to get it back into PEM form?

I have an example live at https://js-keygen.surge.sh/convert.html is that what you are after?

This is great, thanks so much! Super helpful! :)

Sorry to be a pest... but I've tried the code you presented, and I found a few issues.

  1. I think this function is incorrect:

    function pemPublicKey(key) {
       return `---- BEGIN RSA PUBLIC KEY ----\n${wrap(key, 65)}---- END RSA PUBLIC KEY ----`;
    }

    I believe the PKCS1 PEM Public header format is -----BEGIN RSA PUBLIC----- (note the 5 -'s and no space). Also, it should wrap at the 64th column, not 65th. So I believe the function should be:

    function pemPublicKey(key) {
       return `-----BEGIN RSA PUBLIC KEY-----\n${wrap(key, 64)}-----END RSA PUBLIC KEY-----`;
    }

    When I make those changes, your code produces an identical file to the ssh-keygen CLI command. So that's good! :)

  2. Even though I'm able create this PKCS1 PEM format for the public key, it does not seem that I can get the webcrypto API to read that format. :(

    At least, I can't figure out what parameters to use with importKey(..) to make it work. This fails:

    function stripPemFormatting(str) {
       return str
       .replace(/^-----BEGIN (?:RSA )?(?:PRIVATE|PUBLIC) KEY-----$/m,"")
       .replace(/^-----END (?:RSA )?(?:PRIVATE|PUBLIC) KEY-----$/m,"")
       .replace(/[\n\r]/g,"");
    }
    
    function fromPem(keydataB64Pem) {
       var keydataB64 = stripPemFormatting(keydataB64Pem);
       var keydataS = window.atob(keydataB64);
       var keydata = Uint8Array.from(keydataS,x=>x.charCodeAt(0));
       return keydata;
    }
    
    var pkcs1PEM = publicSshToPem(`ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDKqc0Nv7zDbtouPytsDca2sxGRwcVl4TVr9beLiOOP3YrIcQww+hQSuc4tpVyE7WD9nRMbfwGMmWVxd0e88G1uFmaMulctvOpm6E1F2uOLhVT4CwHJhAfX7q8MFEiQ2+kGA/SsAjIVRydqlGrgTjaa0uolQVeXIZIGaCR4Si5hNwGU7ihqwfXc0fiF8tLpno2hcJpnAPAuo82NlVStmWhzDCgCzULWskStYP1/tCwVlr3fNXXNMoUWw1BJ5jG8V/1wvTMrnvPU4k+Yx87IJlr3lNY1FNVJTfsZZ57DUh8xGjCU3V2OoSk4W8g8P/o/3n3emS9nrWgRYIxwrD/2cbRbcOma70lQOWMSWcFQBWIEbb3+Dp1GwYoVg1BEJcV35s2yNCi8SuWoh+bjfK8ZrqlrRyvMeCV84Pq0W8ZjqnNUuFfvJt9ruw6410tVZW+hCZKPaxKTryNNdL2brM3b1IIl30mnZ2i7vCIbm865T/L3Qf7yCLYDA7ut7i7WaPgvuCu8MKZYnQVo4w9lLpND/1ygdEf/kAFEAQlAeNzRngNpXxlWixyUkDRT/4a2Yq1zTSy5YrRp7sO683/L1xKESiutlQ10zraPp2eY/UnRFV1bxRnxFeB2WUTYCLXOC7JZWd3MBVURk7Oedc5HUPx9Y4PkHMuSbVtTjyuRYTQNRZ/w8Q== gitvatar@gmail.com`);
    
    var keyTextBuffer = fromPem(pkcs1PEM);
    
    crypto.subtle.importKey(
      "spki",
       keyTextBuffer,
       {
          name: "RSASSA-PKCS1-v1_5",
          hash: { name: "SHA-256" },
       },
       true,
      ["verify"]
    );

All I get there is a DOMException with no other details. My guess is, either the webcrypto API can't read PKCS1 PEM's... -or- I'm missing some proper params in the algorithm object. Any thoughts?

Alternatively, if it's possible, maybe it'd be better to convert straight to the pkcs8 (or "spki", maybe?) public key format, since webcrypto seems not to support pkcs1 for either public or private keys.

Update: I found this: https://github.com/dominictarr/ssh-key-to-pem/blob/master/index.js

That converts from openssh key format straight to pkcs8. However, it's for node and uses several node packages that I'm not sure whether this code can be adapted for pure browser use or not. But it at least shows it's possible to go to the format we know webcrypto can read.

  1. Header and footer are very often discarded as separator by tooling. I now that openssl does read those headers, but the number of dash is irrelevant and the line length optional (it was meant to be send by email or seen on 80x24 terminals)
  2. Ah! if I had known you wanted to use the format to import into webcrypto... and use pkcs8 that would be a different story. The target format I used is pkcs1 :)

AFAIK pkcs8 is for private key, not public key... are you sure you want to convert the public key to pkcs8? that doesn't seem to fit what I understand.

Sorry for being so ignorant on this stuff. I'm stumbling around and don't even fully know what I'm doing. Appreciate your assistance!

Hey, we are all learners... and I actually enjoy fiddling around with PKI and generating ASN.1 plus I find your git based avatar project interesting - so if I can help all the better.

My ultimate goal is to let a user pick, in their browser, a public key (in ssh, pkcs1, or pkcs8 format) and a private key (in pkcs1 or pkcs8 format), and basically normalize those both to pkcs8 if not already.

Since I know pkcs8 works in webcrypto, jsrsasign (my chosen fallback library for browsers without webcrypto), and node (node-rsa module), it seems like the best universal normalized text format to interchange keys between browser and server.

Additionally, if the user doesn't already have such keys, I can generate a pair for them, again in pkcs8 format.

My app lets someone pick their public key/private key from files and reads them in JS (or generates them!), creates a signature with private key and then verifies it with the public key, then transmits the public key and signature to server (ultimately forgetting any knowledge of their private key). I only verify the first time that they can correctly create a signature that verifies, so we know that will continue to be the case as long as they keep their private key. :)

I'm trying to make it so users don't have to think about what format of keys they have (since it's so confusing), so that's why it's attractive to just normalize everything to one text format that works everywhere I need it to.


BTW, I have a remaining issue to solve, even after this one, to get to that grand ideal scenario. jsrsasign can read pkcs1 private keys, but webcrypto cannot (I don't think). So I have to figure out how to convert pkcs1 private to pkcs8 private. This is trivial on the server with node-rsa, but I need to be able to do it in the browser.

Worst case: in that specific scenario, I have to use jsrsasign alongside webcrypto, instead of only using one or the other. Hope I don't have to do that, but I'm still just stumbling along for now.

If I can figure out a way to package/port rsa-keys lib + ssh-key-to-pem lib to both work in the browser, I think that combination would be the "universal" browser-side RSA key converter I'm referring to. Feels tantalizingly close. :)

From what I understand, you can always derive the public key from the private key, so if you are going to ask for the private key, no need to ask for the public key.

On the other hand the private key should never leave the owner's computer and travel over the network, it needs to stay private, or it will loose the properties we want for authentication.

Converting between formats should not require any crypto package, because it's only bit manipulation and reorder, there should be no cryptographic algorithms involved beside ASN.1 encode/decode and base64 or base64url encode/decode

I'll have a look at how to encode for PKCS8, but my guess is that it will involve the private key.

you can always derive the public key from the private key

This is a great point... I knew that from a technical perspective, but hadn't considered it from the UX perspective. I was too fixated on the incremental tech side. It's probably much better to just ask them for only the private key. And the plus side is that it eliminates all the frustrations around converting between the various public key formats.

That still leaves the problem that I need to be able to import PKCS1 private keys, which jsrsasign understands but webcrypto doesn't. I would still need a way to convert a PKCS1 private key to a PKCS8 form.

So reading up on PKCS8, there are two contents available: PrivateKeyInfo or EncryptedPrivateKeyInfo, the last one will require a crypto library, but the first one doesn't.

But if the use case is about importing PKCS1 keys to webcrypto, we can convert to the jwk format instead, and use webcrypto to export to PKCS8 if required.

Would that suit?

I am still dubious about having people upload their private key.

PrivateKeyInfo or EncryptedPrivateKeyInfo

I'm not supporting encrypted private keys. If they have an encrypted private key, they'll have to decrypt it locally themselves, and paste in the unencrypted contents into the page.

But if the use case is about importing PKCS1 keys to webcrypto, we can convert to the jwk format instead, and use webcrypto to export to PKCS8 if required.

Specifically, the use-case is importing PKCS1 keys into the browser (only) to use, but then used either with webcrypto or with jsrsasign (if the browser doesn't have webcrypto). As such, that means the conversion itself ideally can't rely only on webcrypto or jsrsasign; conversion should work in either type of browser. That's why I've shyed away from jwk.

I am still dubious about having people upload their private key.

They don't upload their private key, they only read it into the page, which only uses the private key to create a digital signature. Only the public key and signature are uploaded. Private key is thrown away.

looking at the RFCs it looks fairly easy to wrap a PKCS1 into a PKCS8 because PKCS8 can be used as a container for PKCS1. That makes me think that if webcrypto can import PKCS8 it should be able to import PKCS1 as well. I'll have a go at it.

Have a look at https://js-keygen.surge.sh/1to8.html I did the simplest and prefixed the pkcs1 with a pkcs8 header. I might need to adjust the length of the pkcs1 entry, but openssl is happy with it...

Haven't had a chance to look at that yet.

But I've run into another roadblock. :( Apparently the webcrypto API does NOT allow you to extract/derive a public key from the private key. Awesome.

So, now I'm back to the drawing board, or... I have to use another crypto lib. Dammit why do they make this stuff so hard?

Actually, good news, I found a hack around this public-from-private limitation, in both jsrsasign and webcrypto. And the fact that this hack works makes me confused and frustrated that these APIs would have gone out of their way to not support this directly.

For webcrypto, the trick is to export the private key to jwk, then copy over only the relevant public key stuff (n and e, etc) into a new jwk-looking object, and import that back in. Just have to change the key_ops from "sign" to "verify". BAM, works. Why on earth would webcrypto have specifically disabled that direct use case but then let you do it so "easily" with such a hack? SMH.

For jsrsasign, the hack is even crazier. Just take the private key object, set isPrivate to false and isPublic to true, and pass it back into KEYUTIL.getPEM(..). BAM, public key. SMH x 2.

It's astonishing to me that neither API supports this what would seem like an incredibly common use case, but that this hack works so easily. Anyway... that problem solved.


I'll look soon at your PKCS1 to PKCS8 conversion and see if these APIs accept it.