-
Notifications
You must be signed in to change notification settings - Fork 112
/
signer.go
359 lines (299 loc) · 10.5 KB
/
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
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
package signature
import (
"crypto/sha512"
"encoding"
"errors"
"fmt"
"io"
"os"
"strings"
"sync"
"github.com/oasisprotocol/curve25519-voi/primitives/ed25519"
)
const (
chainContextMaxSize = 64
chainContextSeparator = " for chain "
)
var (
// ErrNotExist is the error returned when a private key does not exist.
ErrNotExist = os.ErrNotExist
// ErrMalformedPrivateKey is the error returned when a private key is
// malformed.
ErrMalformedPrivateKey = errors.New("signature: malformed private key")
// ErrRoleMismatch is the error returned when the signer factory role
// is misconfigured.
ErrRoleMismatch = errors.New("signature: signer factory role mismatch")
// ErrRoleAction is the error returned when the signer role mismatches
// the signing operations allowed by the role.
ErrRoleAction = errors.New("signature: signer role action mismatch")
// ErrInvalidRole is the error returned when the signer role is invalid.
ErrInvalidRole = errors.New("signature: invalid signer role")
errMalformedContext = errors.New("signature: malformed context")
errUnregisteredContext = errors.New("signature: unregistered context")
errNoChainContext = errors.New("signature: chain domain separation context not set")
errNoSuffixConfigured = errors.New("signature: domain separation context suffix not configured")
errNoDynamicSuffix = errors.New("signature: dynamic context suffix not set")
registeredContexts sync.Map
allowUnregisteredContexts bool
chainContextLock sync.RWMutex
chainContext Context
// SignerRoles is the list of all supported signer roles.
SignerRoles = []SignerRole{
SignerEntity,
SignerNode,
SignerP2P,
SignerConsensus,
}
_ encoding.TextMarshaler = (*SignerRole)(nil)
_ encoding.TextUnmarshaler = (*SignerRole)(nil)
)
type contextOptions struct {
chainSeparation bool
dynamicSuffix string
dynamicSuffixMaxLen int
}
// ContextOption is a context configuration option.
type ContextOption func(*contextOptions)
// WithChainSeparation is a context option that enforces additional domain
// separation based on the ChainID.
func WithChainSeparation() ContextOption {
return func(o *contextOptions) {
o.chainSeparation = true
}
}
// WithDynamicSuffix is a context option that configures the context to use
// a dynamic suffix.
func WithDynamicSuffix(str string, len int) ContextOption {
return func(o *contextOptions) {
o.dynamicSuffix = str
o.dynamicSuffixMaxLen = len
}
}
// Context is a domain separation context.
type Context string
// WithSuffix appends a suffix to the context.
func (c Context) WithSuffix(str string) (Context, error) {
// Ensure that the context is registered for use and has dynamic suffix
// configured.
rawOpts, isRegistered := registeredContexts.Load(c)
if !isRegistered {
return c, errUnregisteredContext
}
opts := rawOpts.(*contextOptions)
if opts.dynamicSuffix == "" {
return c, errNoSuffixConfigured
}
if len(str) > opts.dynamicSuffixMaxLen {
return c, fmt.Errorf("suffix too long: %w (max suffix length: %v)", errMalformedContext, opts.dynamicSuffixMaxLen)
}
newCtx := c + Context(opts.dynamicSuffix+str)
// No dynamic suffix for the new context.
newOpts := contextOptions{
chainSeparation: opts.chainSeparation,
}
// Register the context so it can be looked up (same suffix can be used multiple times).
_, _ = registeredContexts.LoadOrStore(newCtx, &newOpts)
return newCtx, nil
}
// NewContext creates and registers a new context. This routine will panic
// if the context is malformed or is already registered.
func NewContext(rawContext string, opts ...ContextOption) Context {
var opt contextOptions
for _, v := range opts {
v(&opt)
}
// Even if we are not using the clearly superior RFC 8032 constructs
// enforce something that is compatible.
//
// Note: We disallow context lengths of 0, since our ContextSign call
// is intended to enforce strict domain separation.
l := len(rawContext)
if l == 0 {
panic(errMalformedContext)
}
if opt.chainSeparation {
l += len(chainContextSeparator) + chainContextMaxSize
}
l += len(opt.dynamicSuffix) + opt.dynamicSuffixMaxLen
if l > ed25519.ContextMaxSize {
panic(errMalformedContext)
}
// Disallow contexts including the chain context separator as a simple
// way to avoid conflicts with chain-separated contexts.
if strings.Contains(rawContext, chainContextSeparator) {
panic("signature: context must not include '" + chainContextSeparator + "': '" + rawContext + "'")
}
ctx := Context(rawContext)
if _, isRegistered := registeredContexts.Load(ctx); isRegistered {
panic("signature: context already registered: '" + ctx + "'")
}
registeredContexts.Store(ctx, &opt)
return ctx
}
// UnsafeResetChainContext resets the chain context.
//
// This function should NOT be used during normal operation as changing
// the chain context while an application is running is unsafe. The main
// use case for having this function is unit tests.
func UnsafeResetChainContext() {
chainContextLock.Lock()
defer chainContextLock.Unlock()
chainContext = Context("")
}
// UnsafeAllowUnregisteredContexts bypasses the context registration check.
//
// This function is only for the benefit of implementing a remote signer.
func UnsafeAllowUnregisteredContexts() {
allowUnregisteredContexts = true
}
// IsUnsafeUnregisteredContextsAllowed returns true iff context registration
// checks are bypassed.
func IsUnsafeUnregisteredContextsAllowed() bool {
return allowUnregisteredContexts
}
// SetChainContext configures the chain domain separation context that is
// used with any contexts constructed using the WithChainSeparation option.
func SetChainContext(rawContext string) {
if l := len(rawContext); l == 0 || l > chainContextMaxSize {
panic(errMalformedContext)
}
chainContextLock.Lock()
defer chainContextLock.Unlock()
if chainContext != "" && rawContext != string(chainContext) {
panic("signature: chain domain separation context already set: '" + chainContext + "'")
}
chainContext = Context(rawContext)
}
// SignerRole is the role of the Signer (Entity, Node, etc).
type SignerRole int
const (
SignerUnknown SignerRole = 0
SignerEntity SignerRole = 1
SignerNode SignerRole = 2
SignerP2P SignerRole = 3
SignerConsensus SignerRole = 4
SignerEntityName = "entity"
SignerNodeName = "node"
SignerP2PName = "p2p"
SignerConsensusName = "consensus"
)
// String returns the string representation of a SignerRole.
func (role SignerRole) String() string {
switch role {
case SignerEntity:
return SignerEntityName
case SignerNode:
return SignerNodeName
case SignerP2P:
return SignerP2PName
case SignerConsensus:
return SignerConsensusName
default:
return "[unknown signer role]"
}
}
// MarshalText encodes a SignerRole into text form.
func (role SignerRole) MarshalText() ([]byte, error) {
return []byte(role.String()), nil
}
// UnmarshalText decodes a text slice into a SignerRole.
func (role *SignerRole) UnmarshalText(text []byte) error {
switch string(text) {
case SignerEntityName:
*role = SignerEntity
case SignerNodeName:
*role = SignerNode
case SignerP2PName:
*role = SignerP2P
case SignerConsensusName:
*role = SignerConsensus
default:
return fmt.Errorf("%w: %s", ErrInvalidRole, string(text))
}
return nil
}
// SignerFactoryCtor is an SignerFactory constructor.
type SignerFactoryCtor func(interface{}, ...SignerRole) (SignerFactory, error)
// SignerFactory is the opaque factory interface for Signers.
type SignerFactory interface {
// EnsureRole ensures that the SignerFactory is configured for the given
// role.
EnsureRole(role SignerRole) error
// Generate will generate and persist an new private key corresponding to
// the provided role, and return a Signer ready for use. Certain
// implementations require an entropy source to be provided.
Generate(role SignerRole, rng io.Reader) (Signer, error)
// Load will load the private key corresonding to the provided role, and
// return a Signer ready for use.
Load(role SignerRole) (Signer, error)
}
// Signer is an opaque interface for private keys that is capable of producing
// signatures, in the spirit of `crypto.Signer`.
type Signer interface {
// Public returns the PublicKey corresponding to the signer.
Public() PublicKey
// ContextSign generates a signature with the private key over the context and
// message.
ContextSign(context Context, message []byte) ([]byte, error)
// String returns the string representation of a Signer, which MUST not
// include any sensitive information.
String() string
// Reset tears down the Signer and obliterates any sensitive state if any.
Reset()
}
// UnsafeSigner is a Signer that also supports access to the raw private key,
// primarily for testing.
type UnsafeSigner interface {
Signer
// UnsafeBytes returns the byte representation of the private key.
UnsafeBytes() []byte
}
// PrepareSignerContext prepares a context for use during signing by a Signer.
func PrepareSignerContext(context Context) ([]byte, error) {
rawOpts, isRegistered := registeredContexts.Load(context)
if !isRegistered {
// The remote signer implementation uses the raw context, and
// registration is dealt with client side. Just check that the
// length is sensible, even though the client should be sending
// something sane.
if allowUnregisteredContexts {
if cLen := len(context); cLen == 0 || cLen > ed25519.ContextMaxSize {
return nil, errMalformedContext
}
return []byte(context), nil
}
// Ensure that the context is registered for use.
return nil, errUnregisteredContext
}
opts := rawOpts.(*contextOptions)
// Include chain domain separation context if configured.
if opts.chainSeparation {
chainContextLock.RLock()
defer chainContextLock.RUnlock()
if chainContext == "" {
return nil, errNoChainContext
}
context = context + chainContextSeparator + chainContext
}
if opts.dynamicSuffix != "" {
return nil, errNoDynamicSuffix
}
return []byte(context), nil
}
// PrepareSignerMessage prepares a context and message for signing by a Signer.
func PrepareSignerMessage(context Context, message []byte) ([]byte, error) {
rawContext, err := PrepareSignerContext(context)
if err != nil {
return nil, err
}
// This is stupid, and we should be using RFC 8032's Ed25519ph instead
// but when an attempt was made to switch to it (See: #2103), people
// complained that certain HSM offerings doesn't support it.
//
// Blame YubiHSM and Ledger, not me.
h := sha512.New512_256()
_, _ = h.Write(rawContext)
_, _ = h.Write(message)
sum := h.Sum(nil)
return sum[:], nil
}