-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathvault_signer.go
337 lines (289 loc) · 9.04 KB
/
vault_signer.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
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
package vaultsigner
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"io"
"path"
"strings"
"github.com/hashicorp/vault/api"
"github.com/mitchellh/mapstructure"
)
type keyType int
const (
keyTypeRsa keyType = iota
keyTypeEd25519
keyTypeEcdsa
)
type VaultSigner struct {
vaultClient *api.Client
publicKey crypto.PublicKey
// key configuration
namespace string
mountPath string
keyName string
context []byte
// key type specific configuration
hashAlgorithm HashAlgorithm
signatureAlgorithm SignatureAlgorithm
// key properties
derived bool
keyType keyType
}
type SignerConfig struct {
// Namespace for the key. This can be provided in the key config, the vault client,
// or both where they will be combined
Namespace string
// Mountpath is the mount path for transit secrets engine that holds the key
MountPath string
// Keyname is the name of the key in the transit secrets engine
KeyName string
// Context is the context for a derived key and can only be provided when working
// with a derived key
Context []byte
// HashAlgorithm is the hash algorithm used in the signing operation. It is only supported
// for RSA and ECDSA keys. If unset for supported keys, the value will default to sha2-256.
// If the sign request hashes the signing data in the request, this value will be ignored.
HashAlgorithm HashAlgorithm
// SignatureAlgorithm is the signature algorithm used in the signing operation. It is only
// support for RSA keys. If unset for supported keys, the value will default to PKCS#1v15.
SignatureAlgorithm SignatureAlgorithm
}
type HashAlgorithm string
const (
HashAlgorithmSha1 HashAlgorithm = "sha1"
HashAlgorithmSha224 HashAlgorithm = "sha2-224"
HashAlgorithmSha256 HashAlgorithm = "sha2-256"
HashAlgorithmSha384 HashAlgorithm = "sha2-384"
HashAlgorithmSha512 HashAlgorithm = "sha2-512"
)
type SignatureAlgorithm string
const (
SignatureAlgorithmRSAPSS SignatureAlgorithm = "pss"
SignatureAlgorithmRSAPKCS1v15 SignatureAlgorithm = "pkcs1v15"
)
// NewVaultSigner creates a signer the leverages HashiCorp Vault's transit engine to sign
// using Go's built in crypto.Signer interface.
func NewVaultSigner(vaultClient *api.Client, signerConfig *SignerConfig) (*VaultSigner, error) {
if vaultClient == nil {
return nil, errors.New("vault client is required")
}
if signerConfig == nil {
return nil, errors.New("signer config is required")
}
if signerConfig.MountPath == "" {
return nil, errors.New("key mount path is required")
}
if signerConfig.KeyName == "" {
return nil, errors.New("key name is required")
}
signer := &VaultSigner{
vaultClient: vaultClient,
namespace: signerConfig.Namespace,
mountPath: signerConfig.MountPath,
keyName: signerConfig.KeyName,
context: signerConfig.Context,
signatureAlgorithm: signerConfig.SignatureAlgorithm,
hashAlgorithm: signerConfig.HashAlgorithm,
}
if err := signer.retrieveKey(); err != nil {
return nil, err
}
return signer, nil
}
// CloneWithContext copies the signer with a new context. This function will also retrieve
// the derived public key.
func (s *VaultSigner) CloneWithContext(context []byte) (*VaultSigner, error) {
if !s.derived {
return nil, errors.New("context can only be used with derived keys")
}
signer := &VaultSigner{
vaultClient: s.vaultClient,
namespace: s.namespace,
mountPath: s.mountPath,
keyName: s.keyName,
context: context,
signatureAlgorithm: s.signatureAlgorithm,
hashAlgorithm: s.hashAlgorithm,
}
if err := signer.retrieveKey(); err != nil {
return nil, err
}
return signer, nil
}
// Sign is part of the crypto.Signer interface and signs a given digest with the configured key
// in Vault's transit secrets engine
func (s *VaultSigner) Sign(_ io.Reader, digest []byte, signerOpts crypto.SignerOpts) ([]byte, error) {
if signerOpts == nil {
signerOpts = crypto.Hash(0)
}
requestData := map[string]interface{}{
"input": base64.StdEncoding.EncodeToString(digest),
}
if s.derived {
requestData["context"] = base64.StdEncoding.EncodeToString(s.context)
}
if s.keyType == keyTypeRsa {
requestData["signature_algorithm"] = SignatureAlgorithmRSAPKCS1v15
if s.signatureAlgorithm != "" {
requestData["signature_algorithm"] = s.signatureAlgorithm
}
if _, ok := signerOpts.(*rsa.PSSOptions); ok && requestData["signature_algorithm"] == SignatureAlgorithmRSAPKCS1v15 {
return nil, errors.New("PSS options were given when signature algorithm is set to PKCS1v15")
}
}
// The crypto.Signer interface specifies that if the message is hashed, the
// HashFunc in the SignerOpts will be specified
//
// See https://pkg.go.dev/crypto#Signer
switch {
case signerOpts.HashFunc() != crypto.Hash(0):
requestData["prehashed"] = true
switch signerOpts.HashFunc() {
case crypto.SHA1:
requestData["hash_algorithm"] = HashAlgorithmSha1
case crypto.SHA224:
requestData["hash_algorithm"] = HashAlgorithmSha224
case crypto.SHA256:
requestData["hash_algorithm"] = HashAlgorithmSha256
case crypto.SHA384:
requestData["hash_algorithm"] = HashAlgorithmSha384
case crypto.SHA512:
requestData["hash_algorithm"] = HashAlgorithmSha512
default:
return nil, fmt.Errorf("unsupported hash algorithm: %s", signerOpts.HashFunc().String())
}
case s.hashAlgorithm != "":
requestData["hash_algorithm"] = s.hashAlgorithm
}
rsp, err := s.vaultClient.Logical().Write(s.buildKeyPath("sign"), requestData)
if err != nil {
return nil, err
}
if rsp == nil {
return nil, errors.New("no secret returned")
}
sig, ok := rsp.Data["signature"]
if !ok {
return nil, errors.New("no signature returned")
}
splitSig := strings.Split(sig.(string), ":")
if len(splitSig) != 3 {
return nil, errors.New("malformed signature value")
}
sigBytes, err := base64.StdEncoding.DecodeString(splitSig[2])
if err != nil {
return nil, fmt.Errorf("error decoding signature: %s", err)
}
return sigBytes, nil
}
// Public returns the public key for the key stored in transit's secrets engine
func (s *VaultSigner) Public() crypto.PublicKey {
return s.publicKey
}
func (s *VaultSigner) retrieveKey() error {
keyPath := s.buildKeyPath("keys")
// context is ignored if the key is not derived so it is always sent
var context string
if len(s.context) > 0 {
context = base64.StdEncoding.EncodeToString(s.context)
}
rsp, err := s.vaultClient.Logical().ReadWithData(keyPath, map[string][]string{
"context": {
context,
},
})
if err != nil {
return err
}
keyInfo := struct {
Derived bool `mapstructure:"derived"`
SupportsSigning bool `mapstructure:"supports_signing"`
KeyType string `mapstructure:"type"`
Keys map[int]interface{} `mapstructure:"keys"`
LatestVersion int `mapstructure:"latest_version"`
}{}
if err := mapstructure.WeakDecode(rsp.Data, &keyInfo); err != nil {
return err
}
if !keyInfo.SupportsSigning {
return errors.New("key does not support signing")
}
if keyInfo.Derived {
if len(s.context) == 0 {
return errors.New("context must be provided for derived keys")
}
} else {
if len(s.context) > 0 {
return errors.New("context provided but key is not derived")
}
}
s.derived = keyInfo.Derived
switch keyInfo.KeyType {
case "rsa-2048", "rsa-3072", "rsa-4096":
s.keyType = keyTypeRsa
case "ecdsa-p256", "ecdsa-p384", "ecdsa-p521":
s.keyType = keyTypeEcdsa
case "ed25519":
s.keyType = keyTypeEd25519
default:
return errors.New("unsupported key type")
}
if s.keyType != keyTypeRsa && s.signatureAlgorithm != "" {
return errors.New("signature algorithm can only be set for RSA keys")
}
publicKeyInfo := struct {
PublicKey string `mapstructure:"public_key"`
}{}
if err := mapstructure.WeakDecode(keyInfo.Keys[keyInfo.LatestVersion], &publicKeyInfo); err != nil {
return err
}
publicKey, err := s.createPublicKey(publicKeyInfo.PublicKey)
if err != nil {
return err
}
s.publicKey = publicKey
return nil
}
func (s *VaultSigner) buildKeyPath(operation string) string {
return path.Join(s.namespace, s.mountPath, operation, s.keyName)
}
func (s *VaultSigner) createPublicKey(keyData string) (crypto.PublicKey, error) {
switch s.keyType {
case keyTypeRsa:
block, _ := pem.Decode([]byte(keyData))
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err
}
key, ok := pub.(*rsa.PublicKey)
if !ok {
return nil, errors.New("unable to cast to RSA public key")
}
return key, nil
case keyTypeEcdsa:
block, _ := pem.Decode([]byte(keyData))
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err
}
key, ok := pub.(*ecdsa.PublicKey)
if !ok {
return nil, errors.New("unable to cast to ECDSA public key")
}
return key, nil
case keyTypeEd25519:
key, err := base64.StdEncoding.DecodeString(keyData)
if err != nil {
return nil, err
}
return ed25519.PublicKey(key), nil
}
return nil, errors.New("unknown public key type")
}