File: signer.go

package info (click to toggle)
golang-github-smallstep-crypto 0.63.0-1
  • links: PTS, VCS
  • area: main
  • in suites: experimental
  • size: 3,800 kB
  • sloc: sh: 66; makefile: 50
file content (177 lines) | stat: -rw-r--r-- 4,798 bytes parent folder | download | duplicates (3)
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
//go:build !noazurekms
// +build !noazurekms

package azurekms

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

	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
	"github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys"
	"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
	name      string
	version   string
	publicKey crypto.PublicKey
}

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

	client, err := lazyClient.Get(vaultURL)
	if err != nil {
		return nil, err
	}

	// Make sure that the key exists.
	signer := &Signer{
		client:  client,
		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.name, s.version, nil)
	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 Azure Key Vault.
func (s *Signer) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
	alg, err := getSigningAlgorithm(s.Public(), opts)
	if err != nil {
		return nil, err
	}

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

	var octetSize int
	switch alg {
	case azkeys.JSONWebKeySignatureAlgorithmES256:
		octetSize = 32 // 256-bit, concat(R,S) = 64 bytes
	case azkeys.JSONWebKeySignatureAlgorithmES384:
		octetSize = 48 // 384-bit, concat(R,S) = 96 bytes
	case azkeys.JSONWebKeySignatureAlgorithmES512:
		octetSize = 66 // 528-bit, concat(R,S) = 132 bytes
	default:
		return resp.Result, nil
	}

	// Convert to asn1
	if len(resp.Result) != 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(resp.Result[:octetSize])) // R
		b.AddASN1BigInt(new(big.Int).SetBytes(resp.Result[octetSize:])) // S
	})
	return b.Bytes()
}

func (s *Signer) signWithRetry(alg azkeys.JSONWebKeySignatureAlgorithm, digest []byte, retryAttempts int) (azkeys.SignResponse, error) {
retry:
	ctx, cancel := defaultContext()
	defer cancel()

	resp, err := s.client.Sign(ctx, s.name, s.version, azkeys.SignParameters{
		Algorithm: &alg,
		Value:     digest,
	}, nil)
	if err != nil && retryAttempts > 0 {
		var responseError *azcore.ResponseError
		if errors.As(err, &responseError) {
			if responseError.StatusCode == 429 {
				time.Sleep(time.Second / time.Duration(retryAttempts))
				retryAttempts--
				goto retry
			}
		}
	}
	return resp, err
}

func getSigningAlgorithm(key crypto.PublicKey, opts crypto.SignerOpts) (azkeys.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 azkeys.JSONWebKeySignatureAlgorithmPS256, nil
			}
			return azkeys.JSONWebKeySignatureAlgorithmRS256, nil
		case crypto.SHA384:
			if isPSS {
				return azkeys.JSONWebKeySignatureAlgorithmPS384, nil
			}
			return azkeys.JSONWebKeySignatureAlgorithmRS384, nil
		case crypto.SHA512:
			if isPSS {
				return azkeys.JSONWebKeySignatureAlgorithmPS512, nil
			}
			return azkeys.JSONWebKeySignatureAlgorithmRS512, nil
		default:
			return "", errors.Errorf("unsupported hash function %v", h)
		}
	case *ecdsa.PublicKey:
		switch h := opts.HashFunc(); h {
		case crypto.SHA256:
			return azkeys.JSONWebKeySignatureAlgorithmES256, nil
		case crypto.SHA384:
			return azkeys.JSONWebKeySignatureAlgorithmES384, nil
		case crypto.SHA512:
			return azkeys.JSONWebKeySignatureAlgorithmES512, nil
		default:
			return "", errors.Errorf("unsupported hash function %v", h)
		}
	default:
		return "", errors.Errorf("unsupported key type %T", key)
	}
}