...
1# SSH File Signatures
2
3SSH keys can be used to sign files!
4Unfortunately this is a pretty recent change to the openssh tooling, so it is not
5supported by golang.org/x/crypto/ssh yet.
6
7This document explains how it works at a high level.
8
9## Keys
10
11SSH keys are usually split into public and private files, named `id_rsa.pub` and
12`id_rsa`, respectively.
13These files are encoded and formatted a little differently than other signing keys.
14
15### Public Keys
16
17These are typically in the "known hosts" format.
18This looks something like:
19
20```
21ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDw0ZWP4zZLELSJVenQTQsrFJVBnoP64KTg/UWRU6qOb8HEOdtHJDOyTmo9dvN/yJoTFtWAfQEjaTsMVJzTD0gOk6ncTsp0BUtgXawSCfEUiv7v+2VgSVbUfAv/NL+HEGSCdcORnansIyrZaHwAjR3ei3O+pRWvgjRj3pOH1rWGrxaC5IbsELYzS/HvwAG/uwcxgBv4POvaq6eCEHVbqRjIYjjoYsC+c24sgSQxOyXvDS7j2z9TPHPvepDhVr9y6xnnqhLqZEWmidRrbb35aYkVLJxmGTFy/JW1cewyU2Jb3+sKQOiOwL7DAB39tRyec2ed+EHh6QLW4pcMnoXsWuPyi+G595HiUYmIlqXJ5JPo0Cv/rOJrmWSFceWiDjC/SeODp/AcK0EsN/p3wOp6ac7EzAz9Npri0vwSQX4MUYlya/olKiKCx5GIhTZtXioREPd8v4osx2VrVyDxKX99PVVbxw1FXSe4u+PuOawJzUA4vW41mxUY9zoAsb/fvoNPtrrT9HfC+7Pg6ryBdz+445M8Atc8YjjLeYXkTXWD6KMielRzBFFoIwIgi0bMotq3iQ9IwjQSXPMDQLb+UPg8xqsgRsX3wvyZzdBhxO4Bdomv7JYmySysaGgliHktU8qRse1lpDIXMovPtowywcKL4U3seDKrq7saVO0qdsLavy1o0w== lorenc.d@gmail.com
22```
23
24These can be parsed with [ParseKnownHosts](https://pkg.go.dev/golang.org/x/crypto/ssh#ParseKnownHosts)
25, NOT `ParsePublicKey`.
26
27In addition to the key material itself, this can contain the algorithm (`ssh-rsa` here) and a comment
28(lorenc.d@gmail.com) here.
29
30### Private Keys
31
32These are stored in an "armored" PEM format, resembling PGP or x509 keys:
33
34```
35-----BEGIN SSH PRIVATE KEY-----
36<base64 encoded key here>
37-----END SSH PRIVATE KEY-----
38```
39
40These can be parsed correctly with [ParsePrivateKey](https://pkg.go.dev/golang.org/x/crypto/ssh#ParsePrivateKey).
41
42## Wire Format
43
44The wire format is relatively standard.
45
46* Bytes are laid out in order.
47* Fixed-length fields are laid out at the proper offset with the specified length.
48* Strings are stored with the size as a prefix.
49
50## Signature
51
52These can be generated and validated from the command line with the `ssh-keygen -Y` set of commands:
53`sign`, `verify`, and `check-novalidate`.
54
55To work with them in Go is a little tricker.
56The signature is stored using a struct packed using the `openssh` wire format.
57The data that is used in the signing function is also packed in another struct before it is signed.
58
59### Signature Format
60
61Signatures are formatted on disk in a PEM-encoded format.
62The header is `-----BEGIN SSH SIGNATURE-----`, and the end is `-----END SSH SIGNATURE-----`.
63The signature contents are base64-encoded.
64
65The signature contents are wrapped with extra metadata, then encoded as a struct using the
66`openssh` wire format.
67That struct is defined [here](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig#L34).
68
69In Go:
70
71```
72type WrappedSig struct {
73 MagicHeader [6]byte
74 Version uint32
75 PublicKey string
76 Namespace string
77 Reserved string
78 HashAlgorithm string
79 Signature string
80}
81```
82
83The `PublicKey` and `Signature` fields are also stored as openssh-wire-formatted structs.
84The `MagicHeader` is `SSHSIG`.
85The `Version` is 1.
86The `Namespace` is `file` (for this use-case).
87`Reserved` must be empty.
88
89Go can already parse the `PublicKey` and `Signature` fields,
90and the `Signature` struct contains a `Blob` with the signature data.
91
92### Signed Message
93
94In addition to these wrappers, the message to be signed is wrapped with some metadata before
95it is passed to the signing function.
96
97That wrapper is defined [here](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig#L81).
98
99And in Go:
100
101```
102type MessageWrapper struct {
103 Namespace string
104 Reserved string
105 HashAlgorithm string
106 Hash string
107}
108```.
109
110So, the data must first be hashed, then packed in this struct and encoded in the
111openssh wire format.
112Then, this resulting data is signed using the desired signature function.
113
114The `Namespace` field must be `file` (for this usecase).
115The `Reserved` field must be empty.
116
117The output of this signature function (and the hash) becomes the `Signature.Blob`
118value, which gets wire-encoded, wrapped, wire-encoded and finally pem-encoded.
View as plain text