/
kmsca.go
255 lines (231 loc) · 8.16 KB
/
kmsca.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
/*
Copyright 2020 Skyscanner Limited.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package kmsca
import (
"context"
"crypto"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"errors"
"fmt"
"time"
"crypto/sha1" //nolint:gosec // Used for consistent hash
"math/big"
"github.com/Skyscanner/kms-issuer/v4/pkg/interfaces"
"github.com/Skyscanner/kms-issuer/v4/pkg/signer"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/kms"
kmstypes "github.com/aws/aws-sdk-go-v2/service/kms/types"
)
const (
// DefaultCertDuration is the default CA certificate validity duration
DefaultCertDuration = time.Hour * 24 * 365 * 3 // 3 year
// DefaultCertRenewalRatio is default ratio of time before the certificate
// is expected to be renewed
DefaultCertRenewalRatio = 2.0 / 3
)
// KMSCA KMS Certificate Authority provides the API operation methods for implementation
// a certificate authority on top of AWS KMS.
type KMSCA struct {
Client interfaces.KMSClient
}
// NewKMSCA creates a new instance of the KMSCA client with a session.
// If additional configuration is needed for the client instance use the optional
// aws.Config parameter to add your extra config.
func NewKMSCA(cfg *aws.Config) *KMSCA {
return &KMSCA{
Client: kms.NewFromConfig(*cfg),
}
}
// CreateKey creates an asymetric KMS key used to sign certificates and a KMS Alias pointing at the key.
// The method only creates the key if the alias hasn't yet been created.
// Returns the KeyID string
func (ca *KMSCA) CreateKey(ctx context.Context, input *CreateKeyInput) (string, error) {
// Check if the key already exists
response, err := ca.Client.DescribeKey(ctx, &kms.DescribeKeyInput{
KeyId: aws.String(input.AliasName),
})
if err == nil {
// return existing key if one already exists
return aws.ToString(response.KeyMetadata.KeyId), nil
}
// if the error isn't a NotFoundException then raise it
var nsk *kmstypes.NotFoundException
if !errors.As(err, &nsk) {
return "", err
}
// Create the KMS key
keyInput := &kms.CreateKeyInput{
KeyUsage: kmstypes.KeyUsageTypeSignVerify,
KeySpec: kmstypes.KeySpec(kmstypes.CustomerMasterKeySpecRsa2048),
}
if len(input.CustomerMasterKeySpec) > 0 {
keyInput.KeySpec = kmstypes.KeySpec(input.CustomerMasterKeySpec)
}
if len(input.Description) > 0 {
keyInput.Description = aws.String(input.Description)
}
if len(input.Policy) > 0 {
keyInput.Policy = aws.String(input.Policy)
}
if len(input.Tags) > 0 {
for k, v := range input.Tags {
keyInput.Tags = append(keyInput.Tags, kmstypes.Tag{TagKey: aws.String(k), TagValue: aws.String(v)})
}
}
key, err := ca.Client.CreateKey(ctx, keyInput)
if err != nil {
return "", err
}
// Create the KMS alias
_, err = ca.Client.CreateAlias(ctx, &kms.CreateAliasInput{
TargetKeyId: key.KeyMetadata.KeyId,
AliasName: aws.String(input.AliasName),
})
if err != nil {
return "", err
}
return aws.ToString(key.KeyMetadata.KeyId), nil
}
// DeleteKey delete a KMS key alias and the underlying target KMS Key.
func (ca *KMSCA) DeleteKey(ctx context.Context, input *DeleteKeyInput) error {
// Check if the key already exists
response, err := ca.Client.DescribeKey(ctx, &kms.DescribeKeyInput{
KeyId: aws.String(input.AliasName),
})
if err != nil {
return err
}
// Delete the KMS key
deleteInput := &kms.ScheduleKeyDeletionInput{
KeyId: response.KeyMetadata.KeyId,
}
if input.PendingWindowInDays > 0 {
deleteInput.PendingWindowInDays = aws.Int32(int32(input.PendingWindowInDays))
}
_, err = ca.Client.ScheduleKeyDeletion(ctx, deleteInput)
if err != nil {
return err
}
// Delete the KMS alias
_, err = ca.Client.DeleteAlias(ctx, &kms.DeleteAliasInput{
AliasName: aws.String(input.AliasName),
})
if err != nil {
return err
}
return nil
}
// GenerateCertificateAuthorityCertificate returns the Certificate Authority Certificate
func (ca *KMSCA) GenerateCertificateAuthorityCertificate(input *GenerateCertificateAuthorityCertificateInput) *x509.Certificate {
// Compute the start/end validity.
// The rounding factor is used to ensure all the certificates issued within the same period are identical.
notBefore := time.Now().Truncate(input.Rounding)
notAfter := notBefore.Add(input.Duration)
// Compute CA certificate
cert := &x509.Certificate{
Subject: input.Subject,
NotBefore: notBefore,
NotAfter: notAfter,
IsCA: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
}
// Compute the serial number
serialNumberKey := fmt.Sprintf("%s %v", input, cert)
sha := sha1.Sum([]byte(serialNumberKey)) //nolint:gosec // Used for consistent hash
cert.SerialNumber = new(big.Int).SetBytes(sha[:])
return cert
}
// GenerateAndSignCertificateAuthorityCertificate returns the signed Certificate Authority Certificate
func (ca *KMSCA) GenerateAndSignCertificateAuthorityCertificate(ctx context.Context, input *GenerateCertificateAuthorityCertificateInput) (*x509.Certificate, error) {
cert := ca.GenerateCertificateAuthorityCertificate(input)
newSigner, err := signer.New(ctx, ca.Client, input.KeyID)
if err != nil {
return nil, err
}
pub := newSigner.Public()
if pub == nil {
return nil, errors.New("could not retrieve the public key associated with the KMS private key")
}
signedBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, pub, newSigner)
if err != nil {
return nil, err
}
return x509.ParseCertificate(signedBytes)
}
// SignCertificate Signs a certificate request using KMS.
func (ca *KMSCA) SignCertificate(ctx context.Context, input *IssueCertificateInput) (*x509.Certificate, error) {
newSigner, err := signer.New(ctx, ca.Client, input.KeyID)
if err != nil {
return nil, err
}
signedBytes, err := x509.CreateCertificate(rand.Reader, input.Cert, input.Parent, input.PublicKey, newSigner)
if err != nil {
return nil, err
}
return x509.ParseCertificate(signedBytes)
}
// CreateKeyInput input for the CreateKey method
type CreateKeyInput struct {
// AliasName Specifies the alias name for the kms key. This value must begin with alias/ followed by a
// name, such as alias/ExampleAlias.
AliasName string
// Description for the key
Description string
// CustomerMasterKeySpec determines the signing algorithms that the CMK supports.
// Only RSA_2048 is currently supported.
CustomerMasterKeySpec string
// The key policy to attach to the CMK
Policy string
// Tags is a list of tags for the key
Tags map[string]string
}
// DeleteKeyInput input for the CreateKey method
type DeleteKeyInput struct {
// AliasName Specifies the alias name for the kms key. This value must begin with alias/ followed by a
// name, such as alias/ExampleAlias.
AliasName string
// PendingWindowInDays. This value is optional. If you include a value, it must be between 7 and
// 30, inclusive. If you do not include a value, it defaults to 30.
PendingWindowInDays int
}
type Key struct {
// KeyID is the KMS Key Id
KeyID string
}
type GenerateCertificateAuthorityCertificateInput struct {
// KeyID is the KMS Key Id
KeyID string
// Subject of the CA certificate
Subject pkix.Name
// Duration is certificate validity duration
Duration time.Duration
// Rounding is used to round down the certificate NotBefore time.
// For example, by setting the rounding period to 1h, all the certificates generated between the start
// and in the end of an hour will be identical
Rounding time.Duration
}
type IssueCertificateInput struct {
// KeyID is the KMS Key Id
KeyID string
// CSR Certificate Request
Cert *x509.Certificate
// PublicKey
PublicKey crypto.PublicKey
// Parent Signing Certificate
Parent *x509.Certificate
// Public
}