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