File: signer.go

package info (click to toggle)
golang-step-crypto 0.24.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 2,056 kB
  • sloc: sh: 53; makefile: 28
file content (185 lines) | stat: -rw-r--r-- 4,844 bytes parent folder | download
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
//go:build !noazurekms
// +build !noazurekms

package azurekms

import (
	"crypto"
	"crypto/ecdsa"
	"crypto/rsa"
	"encoding/base64"
	"io"
	"math/big"
	"time"

	"github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
	"github.com/Azure/go-autorest/autorest/azure"
	"github.com/pkg/errors"
	"golang.org/x/crypto/cryptobyte"
	"golang.org/x/crypto/cryptobyte/asn1"
)

// Signer implements a crypto.Signer using the AWS KMS.
type Signer struct {
	client       KeyVaultClient
	vaultBaseURL string
	name         string
	version      string
	publicKey    crypto.PublicKey
}

// NewSigner creates a new signer using a key in the AWS KMS.
func NewSigner(client KeyVaultClient, signingKey string, defaults DefaultOptions) (crypto.Signer, error) {
	vault, name, version, _, err := parseKeyName(signingKey, defaults)
	if err != nil {
		return nil, err
	}

	// Make sure that the key exists.
	signer := &Signer{
		client:       client,
		vaultBaseURL: vaultBaseURL(vault),
		name:         name,
		version:      version,
	}
	if err := signer.preloadKey(); err != nil {
		return nil, err
	}

	return signer, nil
}

func (s *Signer) preloadKey() error {
	ctx, cancel := defaultContext()
	defer cancel()

	resp, err := s.client.GetKey(ctx, s.vaultBaseURL, s.name, s.version)
	if err != nil {
		return errors.Wrap(err, "keyVault GetKey failed")
	}

	s.publicKey, err = convertKey(resp.Key)
	return err
}

// Public returns the public key of this signer or an error.
func (s *Signer) Public() crypto.PublicKey {
	return s.publicKey
}

// Sign signs digest with the private key stored in the AWS KMS.
func (s *Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
	alg, err := getSigningAlgorithm(s.Public(), opts)
	if err != nil {
		return nil, err
	}

	b64 := base64.RawURLEncoding.EncodeToString(digest)

	// Sign with retry if the key is not ready
	resp, err := s.signWithRetry(alg, b64, 3)
	if err != nil {
		return nil, errors.Wrap(err, "keyVault Sign failed")
	}

	sig, err := base64.RawURLEncoding.DecodeString(*resp.Result)
	if err != nil {
		return nil, errors.Wrap(err, "error decoding keyVault Sign result")
	}

	var octetSize int
	switch alg {
	case keyvault.ES256:
		octetSize = 32 // 256-bit, concat(R,S) = 64 bytes
	case keyvault.ES384:
		octetSize = 48 // 384-bit, concat(R,S) = 96 bytes
	case keyvault.ES512:
		octetSize = 66 // 528-bit, concat(R,S) = 132 bytes
	default:
		return sig, nil
	}

	// Convert to asn1
	if len(sig) != octetSize*2 {
		return nil, errors.Errorf("keyVault Sign failed: unexpected signature length")
	}
	var b cryptobyte.Builder
	b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) {
		b.AddASN1BigInt(new(big.Int).SetBytes(sig[:octetSize])) // R
		b.AddASN1BigInt(new(big.Int).SetBytes(sig[octetSize:])) // S
	})
	return b.Bytes()
}

func (s *Signer) signWithRetry(alg keyvault.JSONWebKeySignatureAlgorithm, b64 string, retryAttempts int) (keyvault.KeyOperationResult, error) {
retry:
	ctx, cancel := defaultContext()
	defer cancel()

	resp, err := s.client.Sign(ctx, s.vaultBaseURL, s.name, s.version, keyvault.KeySignParameters{
		Algorithm: alg,
		Value:     &b64,
	})
	if err != nil && retryAttempts > 0 {
		var requestError *azure.RequestError
		if errors.As(err, &requestError) {
			if se := requestError.ServiceError; se != nil && se.InnerError != nil {
				code, ok := se.InnerError["code"].(string)
				if ok && code == "KeyNotYetValid" {
					time.Sleep(time.Second / time.Duration(retryAttempts))
					retryAttempts--
					goto retry
				}
			}
		}
	}
	return resp, err
}

func getSigningAlgorithm(key crypto.PublicKey, opts crypto.SignerOpts) (keyvault.JSONWebKeySignatureAlgorithm, error) {
	switch key.(type) {
	case *rsa.PublicKey:
		hashFunc := opts.HashFunc()
		pss, isPSS := opts.(*rsa.PSSOptions)
		// Random salt lengths are not supported
		if isPSS &&
			pss.SaltLength != rsa.PSSSaltLengthAuto &&
			pss.SaltLength != rsa.PSSSaltLengthEqualsHash &&
			pss.SaltLength != hashFunc.Size() {
			return "", errors.Errorf("unsupported RSA-PSS salt length %d", pss.SaltLength)
		}

		switch h := hashFunc; h {
		case crypto.SHA256:
			if isPSS {
				return keyvault.PS256, nil
			}
			return keyvault.RS256, nil
		case crypto.SHA384:
			if isPSS {
				return keyvault.PS384, nil
			}
			return keyvault.RS384, nil
		case crypto.SHA512:
			if isPSS {
				return keyvault.PS512, nil
			}
			return keyvault.RS512, nil
		default:
			return "", errors.Errorf("unsupported hash function %v", h)
		}
	case *ecdsa.PublicKey:
		switch h := opts.HashFunc(); h {
		case crypto.SHA256:
			return keyvault.ES256, nil
		case crypto.SHA384:
			return keyvault.ES384, nil
		case crypto.SHA512:
			return keyvault.ES512, nil
		default:
			return "", errors.Errorf("unsupported hash function %v", h)
		}
	default:
		return "", errors.Errorf("unsupported key type %T", key)
	}
}