Signs of Triviality

Opinions, mostly my own, on the importance of being and other things.
[homepage] [index] [jschauma@netmeister.org] [@jschauma] [RSS]

Converting ssh(1) RSA public keys to PKCS8 format

December 29, 2013

As I discussed before, ssh(1) RSA keys can be used easily for regular asymmetric encryption using openssl(1)'s rsautl(1) command. In order for that to work, however, the public key -- often called id_rsa.pub, for example -- needs to be converted from the format used by ssh(1) into one that openssl(1) understands.

The format used by ssh(1) to store public RSA keys is described in RFC4716, and a sample key might look like this:

---- BEGIN SSH2 PUBLIC KEY ----
Comment: "2048-bit RSA, converted by jschauma@localhost"
AAAAB3NzaC1yc2EAAAADAQABAAABAQDQyJDO+B7nAKe+8xEejl8OhMGfYPciyd1/jDstm9
TY61gNnVrwxmuR7BaID3lclaXGBRrPBq+EPpPceCoOH/n2qkbf/o8olv2Kea7kwKfN+gb1
RSjbhGpIM9bTjySn1qC3hXB65veNvqVCJuskfArUQaeziGevv9b8l2lbpJIKN8UKHpLm81
3YhQoeL6usn+4X7TZ9aRAsziSfD9+J1RffXRMXajg/KTJHyf9AZtyB/3CSRfWofiYQ3Kqx
lUsMxR611uy3x4x7wlPv6ldrKHEg59wIXitlscgAaCn9HnlfVFOQDT0LN+FfhTlvc1TQMd
FgWUGe0teNh3bP7i17h4eV
---- END SSH2 PUBLIC KEY ----

Now this doesn't actually look like what you're used to when looking at your ~/.ssh/authorized_keys or even your ~/.ssh/id_rsa.pub file. The reason for that is that, in order to be able to store multiple keys in a single file, it is s more convenient for ssh(1) to have one key per line in a file. To accomplish this, a normal ssh(1) rsa public key uses a variation of the RFC4716 format by concatenating the lines, adding the comment to the end (and allowing for additional options at the beginning). The same key as above may look more familiar if represented like this:

$ cat .ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQyJDO+B7nAKe+8xEejl8OhMGfYPciyd1/jDstm9TY
61gNnVrwxmuR7BaID3lclaXGBRrPBq+EPpPceCoOH/n2qkbf/o8olv2Kea7kwKfN+gb1RSjbhGpIM9bT
jySn1qC3hXB65veNvqVCJuskfArUQaeziGevv9b8l2lbpJIKN8UKHpLm813YhQoeL6usn+4X7TZ9aRAs
ziSfD9+J1RffXRMXajg/KTJHyf9AZtyB/3CSRfWofiYQ3KqxlUsMxR611uy3x4x7wlPv6ldrKHEg59wI
XitlscgAaCn9HnlfVFOQDT0LN+FfhTlvc1TQMdFgWUGe0teNh3bP7i17h4eV jschauma@localhost
$ 

Now openssl(1) can't use either of these two formats, which is why we need to convert them to a format it does understand, such as PKCS8 (ie RFC5208). OpenSSH greater than 5.6 provides the functionality to convert from one format to another via the ssh-keygen(1) utility:

$ ssh-keygen -e -m PKCS8 -f ~/.ssh/id_rsa.pub
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0MiQzvge5wCnvvMRHo5f
DoTBn2D3Isndf4w7LZvU2OtYDZ1a8MZrkewWiA95XJWlxgUazwavhD6T3HgqDh/5
9qpG3/6PKJb9inmu5MCnzfoG9UUo24RqSDPW048kp9agt4Vweub3jb6lQibrJHwK
1EGns4hnr7/W/JdpW6SSCjfFCh6S5vNd2IUKHi+rrJ/uF+02fWkQLM4knw/fidUX
310TF2o4PykyR8n/QGbcgf9wkkX1qH4mENyqsZVLDMUetdbst8eMe8JT7+pXayhx
IOfcCF4rZbHIAGgp/R55X1RTkA09CzfhX4U5b3NU0DHRYFlBntLXjYd2z+4te4eH
lQIDAQAB
-----END PUBLIC KEY-----

With this capability, it's trivial to wrap the required commands to allow for easy asymmetric encryption using ssh keys. However, it turns out that on OS X Mavericks, the OS provided version of ssh-keygen(1) lost the ability to handle the PKCS8 format:

$ ssh -V
OpenSSH_6.2p2, OSSLShim 0.9.8r 8 Dec 2011
$ ssh-keygen -e -m PKCS8 -f ~/.ssh/id_rsa.pub
-----BEGIN PUBLIC KEY-----
-----END PUBLIC KEY-----
PEM_write_RSA_PUBKEY failed
$

So, in an effort to fix jass and remove its dependency on OpenSSH's tools, I set about writing some code that could perform the conversion itself. In the process, I learned a bit more Go, a lot about the file formats, and re-acquainted myself with my old friend ASN.1 (more of a frenemy, really).

Recall that an RSA public key consists of a modulus n (the product of two distinct, random prime numbers p and q), and an exponent e such that Φ(n) and e are coprime. (e is, as we will see, commonly 216 + 1 = 65,537.) It is thus really only a question of how these two numbers are encoded.

In the ssh(1) format, we find the Base64 encoded string to contain tuples of:

  • four bytes representing the length of the next data field
  • the data field

In practice, for an RSA public key, we get:

  • four bytes [0 0 0 7]
  • the string "ssh-rsa" (ie 7 bytes)
  • four bytes
  • the exponent e
  • four bytes
  • the modulus n

Let's look at our key from above:

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQyJDO+B7nAKe+8xEejl8OhMGfYPciyd1/jDstm9TY
61gNnVrwxmuR7BaID3lclaXGBRrPBq+EPpPceCoOH/n2qkbf/o8olv2Kea7kwKfN+gb1RSjbhGpIM9bT
jySn1qC3hXB65veNvqVCJuskfArUQaeziGevv9b8l2lbpJIKN8UKHpLm813YhQoeL6usn+4X7TZ9aRAs
ziSfD9+J1RffXRMXajg/KTJHyf9AZtyB/3CSRfWofiYQ3KqxlUsMxR611uy3x4x7wlPv6ldrKHEg59wI
XitlscgAaCn9HnlfVFOQDT0LN+FfhTlvc1TQMdFgWUGe0teNh3bP7i17h4eV jschauma@localhost

...and base64 decoded:

$ awk '{print $2}' ~/.ssh/id_rsa.pub | base64 -D | hexdump
0000000 00 00 00 07 73 73 68 2d 72 73 61 00 00 00 03 01
0000010 00 01 00 00 01 01 00 d0 c8 90 ce f8 1e e7 00 a7
0000020 be f3 11 1e 8e 5f 0e 84 c1 9f 60 f7 22 c9 dd 7f
0000030 8c 3b 2d 9b d4 d8 eb 58 0d 9d 5a f0 c6 6b 91 ec
0000040 16 88 0f 79 5c 95 a5 c6 05 1a cf 06 af 84 3e 93
0000050 dc 78 2a 0e 1f f9 f6 aa 46 df fe 8f 28 96 fd 8a
0000060 79 ae e4 c0 a7 cd fa 06 f5 45 28 db 84 6a 48 33
0000070 d6 d3 8f 24 a7 d6 a0 b7 85 70 7a e6 f7 8d be a5
0000080 42 26 eb 24 7c 0a d4 41 a7 b3 88 67 af bf d6 fc
0000090 97 69 5b a4 92 0a 37 c5 0a 1e 92 e6 f3 5d d8 85
00000a0 0a 1e 2f ab ac 9f ee 17 ed 36 7d 69 10 2c ce 24
00000b0 9f 0f df 89 d5 17 df 5d 13 17 6a 38 3f 29 32 47
00000c0 c9 ff 40 66 dc 81 ff 70 92 45 f5 a8 7e 26 10 dc
00000d0 aa b1 95 4b 0c c5 1e b5 d6 ec b7 c7 8c 7b c2 53
00000e0 ef ea 57 6b 28 71 20 e7 dc 08 5e 2b 65 b1 c8 00
00000f0 68 29 fd 1e 79 5f 54 53 90 0d 3d 0b 37 e1 5f 85
0000100 39 6f 73 54 d0 31 d1 60 59 41 9e d2 d7 8d 87 76
0000110 cf ee 2d 7b 87 87 95
0000117
$

The first four bytes (00 00 00 07) tell us that the next chunk of data is 7 bytes long, which is 73 73 68 2d 72 73 61, or the string "ssh-rsa" (hex 73 = ascii 115 = 's'; hex 73 = ascii 115 = 's'; hex 68 = ascii 104 = 'h'; ...). The next chunk of data is only 3 bytes long (00 00 00 03), consisting of 01 00 01, or the number 65537, which is our exponent e (as noted above, a common value). Finally, the last chunk of data is 257 bytes in length (00 00 01 01). This (00 d0 c8 ... 87 87 95) is our modulus n; since it is a signed value, the leading 00 is discarded when we construct our number from the byte sequence, yielding (in this case), the following number:

263564700450999879272941676651472466042785246611038114231755492421479742573
889581045455652597334048608087001347847416041777711121697734739255748166159
457507589403313646577223042332979589948076811113406239054835600990216102958
381274497583115516140895655558077874241831445942327885416055182232063456259
483128150349796414172069979647305995945438918284378526171372383713790613138
757982408909115368695259726617904818389020814916764943105724259900090351227
974381617931930412217431003402503409463346944345346048962585892643565658739
915634174038541121394515768473394371326195716283269959572457924281925840036
26122437085071253

...which, to me, anyway, looks reasonably hard to factor.

Having successfully extracted our public key exponent (65537) and modulus (263564700...), we can now encode it in the format specified by the PKCS8 format as an ASN.1 sequence of a "public key algorithm" and a bitstring of the actual key bytes. The "public key algorithm" uses the ASN.1 OID 1.2.840.113549.1.1.1 assigned to RSA. The final sequence is then base64 encoded and printed in 64-char long lines.

You can do all of this in many different languages; I chose to write it in Go. You can download the sample program from here, or read through it below. To check that it works, you can compare the output to that of ssh-keygen(1):

$ cat ~/.ssh/id_rsa.pub | go run ssh2pkcs8.go >/tmp/1
$ ssh-keygen -e -m PKCS8 -f ~/.ssh/id_rsa.pub > /tmp/2
$ diff -bu /tmp/[12]
$


Sample Go code

     1	package main
      	
     2	import (
     3		"bytes"
     4		"crypto/rsa"
     5		"encoding/asn1"
     6		"encoding/base64"
     7		"encoding/binary"
     8		"encoding/hex"
     9		"fmt"
    10		"io/ioutil"
    11		"log"
    12		"math/big"
    13		"os"
    14		"strings"
    15	)
      	
    16	const MAX_COLUMNS = 64
      	
    17	func main() {
    18		if len(os.Args) > 1 {
    19			log.Fatal("Unexpected arguments.  This program can only read input from stdin.")
    20		}
      	
    21		input, err := ioutil.ReadAll(os.Stdin)
    22		if err != nil {
    23			log.Fatal(err)
    24		}
      	
    25		key := string(input)
      	
    26		i:= strings.Index(key, "ssh-rsa AAAAB3NzaC1")
    27		if i < 0 {
    28			log.Fatal("Input does not look like a valid SSH RSA key.")
    29		}
      	
    30		fields := strings.Split(key[i:], " ")
    31		decoded, err := base64.StdEncoding.DecodeString(fields[1]);
    32		if err != nil {
    33			log.Fatal("Unable to decode key: %v", err)
    34		}
      	
    35		n := 0
    36		var pubkey rsa.PublicKey
    37		for len(decoded) > 0 {
    38			var dlen uint32
    39			bbuf := bytes.NewReader(decoded[:4])
    40			err := binary.Read(bbuf, binary.BigEndian, &dlen)
    41			if err != nil {
    42				log.Fatal(err)
    43			}
      	
    44			data := decoded[4:int(dlen)+4]
    45			decoded = decoded[4+int(dlen):]
      	
    46			if (n == 0) {
    47				if ktype := fmt.Sprintf("%s", data); ktype != "ssh-rsa" {
    48					log.Fatal("Unsupported key type (%v).", ktype)
    49				}
    50			} else if (n == 1) {
    51				i := new(big.Int)
    52				i.SetString(fmt.Sprintf("0x%v", hex.EncodeToString(data)), 0)
    53				pubkey.E = int(i.Int64())
    54			} else if (n == 2) {
    55				i := new(big.Int)
    56				/* The value in this field is signed, so the first
    57				 * byte should be 0, so we strip it. */
    58				i.SetString(fmt.Sprintf("0x%v", hex.EncodeToString(data[1:])), 0)
    59				pubkey.N = i
    60				break
    61			}
    62			n += 1
    63		}
      	
    64		enc, err := asn1.Marshal(pubkey)
    65		if err != nil {
    66			log.Fatal("Unable to marshal pubkey (%v): %v", pubkey, err)
    67		}
    68		bitstring := asn1.BitString{enc, len(enc) * 8}
      	
    69		type AlgorithmIdentifier struct {
    70			Algorithm  asn1.ObjectIdentifier
    71			Parameters asn1.RawValue
    72		}
      	
    73		var null = asn1.RawValue{ Tag: 5 }
    74		var pkid = AlgorithmIdentifier{ asn1.ObjectIdentifier{1,2,840,113549,1,1,1}, null }
      	
    75		type keyseq struct {
    76			Algorithm AlgorithmIdentifier
    77			BitString asn1.BitString
    78		}
    79		ks := keyseq{ pkid, bitstring}
      	
    80		enc, err = asn1.Marshal(ks)
    81		if err != nil {
    82			log.Fatal("Unable to marshal pubkey (%v): %v", pubkey, err)
    83		}
      	
    84		fmt.Printf("-----BEGIN PUBLIC KEY-----\n")
    85		out := base64.StdEncoding.EncodeToString(enc)
    86		for len(out) > MAX_COLUMNS {
    87			fmt.Printf("%v\n", out[:MAX_COLUMNS])
    88			out = out[MAX_COLUMNS:]
    89		}
    90		fmt.Printf("%v\n", out)
    91		fmt.Printf("-----END PUBLIC KEY-----\n")
    92	}

December 29, 2013


Related links

On this blog:

Elsewhere:


[The Nest of Trust] [Index] [Mehr Üs als Äs]