/
signature.go
276 lines (244 loc) · 8.12 KB
/
signature.go
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
// {{{ Copyright (c) Paul R. Tagliamonte <paultag@gmail.com> 2022
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE. }}}
package sshsig
import (
"bytes"
"crypto"
"fmt"
"io"
"golang.org/x/crypto/ssh"
)
// HashAlgo represents the hash function used to sign the data.
type HashAlgo string
// Hash will return the crypto.Hash object that relates to the specified
// hashing algorithm.
func (ha HashAlgo) Hash() (crypto.Hash, error) {
switch ha {
case HashAlgoSHA256:
return crypto.SHA256, nil
case HashAlgoSHA512:
return crypto.SHA512, nil
default:
return crypto.Hash(0), ErrUnknownHashAlgorithm
}
}
// Check will ensure that the Hash Algorithm is understood by sshsig.
func (ha HashAlgo) Check() error {
switch ha {
case HashAlgoSHA256:
return nil
case HashAlgoSHA512:
return nil
default:
return ErrUnknownHashAlgorithm
}
}
var (
// ErrUnknownHashAlgorithm will be returned if the HashAlgo is not
// understood by this implementation.
ErrUnknownHashAlgorithm = fmt.Errorf("sshsig: Unknown Hash Algorithm")
// ErrBadMagic will be returned if the Magic preamble is not correct
// for an SSHSIG packet.
ErrBadMagic = fmt.Errorf("sshsig: Bad Magic Preamble")
// ErrUnknownVersion will be returned if the Version is different than
// the library supported (currently, only version 1 is supported).
ErrUnknownVersion = fmt.Errorf("sshsig: Unknown Version")
// HashAlgoSHA512 is the SHA512 algorithm.
HashAlgoSHA512 HashAlgo = "sha512"
// HashAlgoSHA256 is the SHA256 algorithm.
HashAlgoSHA256 HashAlgo = "sha256"
// MagicPreamble is the first 6 bytes of any Signature.
MagicPreamble = [6]byte{'S', 'S', 'H', 'S', 'I', 'G'}
)
// signature is the line-format version of the OpenSSH signature. This isn't
// exported to users, since we need to do a bit of work to parse the underlying
// data, and the data at this level isn't a very friendly way to interact with
// the signature.
//
// After parsing and validation, this will be moved and exported into the
// Signature type.
type signature struct {
Magic [6]byte
Version uint32
PublicKey []byte
Namespace []byte
Reserved []byte
HashAlgorithm string
Signature []byte
}
// Check will ensure that the Signature is well-formed.
func (sig signature) Check() error {
if bytes.Compare(sig.Magic[:], MagicPreamble[:]) != 0 {
return ErrBadMagic
}
if sig.Version != 1 {
return ErrUnknownVersion
}
if err := HashAlgo(sig.HashAlgorithm).Check(); err != nil {
return err
}
return nil
}
// Signature contains the fields of an OpenSSH SSHSIG Signature, as read
// from the underlying wire data. All of the fields of this struct are
// user-provided and not safe to consider trusted without other verification.
type Signature struct {
// Version is the major version of the SSHSIG protocol. Currently, only
// Version 1 is supported; any other version is not understood
// and will result in an error.
Version uint32
// PublicKey is the underlying SSH Public Key used to create the signature.
// This is user provided, and not to be treated as validated or otherwise
// trustworthy until otherwise verified.
PublicKey ssh.PublicKey
// Namespace is a unique signature domain used to avoid copy-and-pasting
// signatures between uses. Ensure your key signing scheme has a unique
// namespace.
Namespace []byte
// HashAlgorithm is the algorithm used to sign the data.
HashAlgorithm HashAlgo
// Signature is an ssh Signature over the hash of the data.
Signature *ssh.Signature
}
// Marshal will encode the Signature into the SSHSIG Wire format.
func (sig Signature) Marshal() []byte {
return ssh.Marshal(signature{
Magic: MagicPreamble,
Version: sig.Version,
PublicKey: sig.PublicKey.Marshal(),
Namespace: sig.Namespace,
Reserved: nil,
HashAlgorithm: string(sig.HashAlgorithm),
Signature: ssh.Marshal(sig.Signature),
})
}
// ParseSignature will unpack the wire-protocol version of an SSHSIG format
// Signature into a Signature struct.
func ParseSignature(b []byte) (*Signature, error) {
sig := &signature{}
if err := ssh.Unmarshal(b, sig); err != nil {
return nil, err
}
if err := sig.Check(); err != nil {
return nil, err
}
pubKey, err := ssh.ParsePublicKey(sig.PublicKey)
if err != nil {
return nil, err
}
sign := &ssh.Signature{}
if err := ssh.Unmarshal(sig.Signature, sign); err != nil {
return nil, err
}
return &Signature{
Version: sig.Version,
PublicKey: pubKey,
Namespace: sig.Namespace,
HashAlgorithm: HashAlgo(sig.HashAlgorithm),
Signature: sign,
}, nil
}
// signedData is the SSH line format used to create the data that is
// signed by the OpenSSH key. This isn't exported since I don't think this
// is a sensible user-facing API component.
type signedData struct {
Namespace []byte
HashAlgorithm string
Hash []byte
}
// Marshal will serialize the hash and associated metadata into a "Signed Data"
// struct, for signing or verification.
func (sd signedData) Marshal() []byte {
return ssh.Marshal(struct {
Magic [6]byte
Namespace []byte
Reserved []byte
HashAlgorithm string
Hash []byte
}{
MagicPreamble,
sd.Namespace,
nil,
sd.HashAlgorithm,
sd.Hash,
})
}
// Verify will check that the OpenSSH SSHSIG Signature is valid given the
// data hash, hash algorith, and namespace.
//
// This function expects that the data from the Signature is passed
// explicitly -- even though the Signature type also includes the Namespace,
// HashAlgo, and PublicKey, passing them in here ensures that the values
// of the Signature are confirmed totally during validation.
//
// This will only verify an SSHSIG v1 signature.
func Verify(
pub ssh.PublicKey,
namespace []byte,
hashAlgo HashAlgo,
hash []byte,
sig *Signature,
) error {
// Check the Public Key early to provide a better error. This isn't
// actually a security feature, since the provided PublicKey is used,
// even after we believe they match by checking their SHA256 hash.
if ssh.FingerprintSHA256(pub) != ssh.FingerprintSHA256(sig.PublicKey) {
return fmt.Errorf("sshsig: provided Public Key didn't sign this data.")
}
// If the HashAlgorithm provided doesn't match the Signature's
// HashAlgorithm, we shouldn't go forward.
if hashAlgo != sig.HashAlgorithm {
return fmt.Errorf("sshsig: provided HashAlgorithm doesn't match Signature")
}
message := signedData{
Namespace: namespace,
HashAlgorithm: string(hashAlgo),
Hash: hash,
}.Marshal()
return pub.Verify(message, sig.Signature)
}
// Sign will create an OpenSSH SSHSIG format Signature in the provdied namespace,
// with the provided Hash Algorith, and data hash. This will create an SSHSIG
// v1.
func Sign(
rand io.Reader,
priv ssh.Signer,
namespace []byte,
hashAlgo HashAlgo,
hash []byte,
) ([]byte, error) {
message := signedData{
Namespace: namespace,
HashAlgorithm: string(hashAlgo),
Hash: hash,
}.Marshal()
sig, err := priv.Sign(rand, message)
if err != nil {
return nil, err
}
return Signature{
Version: 1,
PublicKey: priv.PublicKey(),
Namespace: namespace,
HashAlgorithm: hashAlgo,
Signature: sig,
}.Marshal(), nil
}
// vim: foldmethod=marker