Skip to content

Commit 67add91

Browse files
Enable recovery using a subset of recovery keys (#879)
Signed-off-by: Daniel Weiße <[email protected]>
1 parent 4c8fa03 commit 67add91

File tree

26 files changed

+1282
-86
lines changed

26 files changed

+1282
-86
lines changed

.github/workflows/check-license.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@ jobs:
2121
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
2222

2323
- name: Check for files without license header
24-
run: "! grep -rL --include='*.go' -e'Copyright (c) Edgeless Systems GmbH' -e'DO NOT EDIT' | grep ''"
24+
run: "! grep -rL --include='*.go' --exclude-dir='3rdparty' -e'Copyright (c) Edgeless Systems GmbH' -e'DO NOT EDIT' | grep ''"

.golangci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ linters:
2828
- (*go.uber.org/zap.Logger).Sync
2929
exclusions:
3030
generated: lax
31+
paths:
32+
- "3rdparty/"
3133
rules:
3234
# Simplified does not necessarily mean more readable
3335
- linters: ["staticcheck"]

3rdparty/hashicorp/shamir/LICENSE

Lines changed: 364 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package shamir
5+
6+
import (
7+
"crypto/rand"
8+
"crypto/subtle"
9+
"fmt"
10+
mathrand "math/rand"
11+
"time"
12+
)
13+
14+
const (
15+
// ShareOverhead is the byte size overhead of each share
16+
// when using Split on a secret. This is caused by appending
17+
// a one byte tag to the share.
18+
ShareOverhead = 1
19+
)
20+
21+
// polynomial represents a polynomial of arbitrary degree
22+
type polynomial struct {
23+
coefficients []uint8
24+
}
25+
26+
// makePolynomial constructs a random polynomial of the given
27+
// degree but with the provided intercept value.
28+
func makePolynomial(intercept, degree uint8) (polynomial, error) {
29+
// Create a wrapper
30+
p := polynomial{
31+
coefficients: make([]byte, degree+1),
32+
}
33+
34+
// Ensure the intercept is set
35+
p.coefficients[0] = intercept
36+
37+
// Assign random co-efficients to the polynomial
38+
if _, err := rand.Read(p.coefficients[1:]); err != nil {
39+
return p, err
40+
}
41+
42+
return p, nil
43+
}
44+
45+
// evaluate returns the value of the polynomial for the given x
46+
func (p *polynomial) evaluate(x uint8) uint8 {
47+
// Special case the origin
48+
if x == 0 {
49+
return p.coefficients[0]
50+
}
51+
52+
// Compute the polynomial value using Horner's method.
53+
degree := len(p.coefficients) - 1
54+
out := p.coefficients[degree]
55+
for i := degree - 1; i >= 0; i-- {
56+
coeff := p.coefficients[i]
57+
out = add(mult(out, x), coeff)
58+
}
59+
return out
60+
}
61+
62+
// interpolatePolynomial takes N sample points and returns
63+
// the value at a given x using a lagrange interpolation.
64+
func interpolatePolynomial(x_samples, y_samples []uint8, x uint8) uint8 {
65+
limit := len(x_samples)
66+
var result, basis uint8
67+
for i := 0; i < limit; i++ {
68+
basis = 1
69+
for j := 0; j < limit; j++ {
70+
if i == j {
71+
continue
72+
}
73+
num := add(x, x_samples[j])
74+
denom := add(x_samples[i], x_samples[j])
75+
term := div(num, denom)
76+
basis = mult(basis, term)
77+
}
78+
group := mult(y_samples[i], basis)
79+
result = add(result, group)
80+
}
81+
return result
82+
}
83+
84+
// div divides two numbers in GF(2^8)
85+
func div(a, b uint8) uint8 {
86+
if b == 0 {
87+
// leaks some timing information but we don't care anyways as this
88+
// should never happen, hence the panic
89+
panic("divide by zero")
90+
}
91+
92+
ret := int(mult(a, inverse(b)))
93+
94+
// Ensure we return zero if a is zero but aren't subject to timing attacks
95+
ret = subtle.ConstantTimeSelect(subtle.ConstantTimeByteEq(a, 0), 0, ret)
96+
return uint8(ret)
97+
}
98+
99+
// inverse calculates the inverse of a number in GF(2^8)
100+
func inverse(a uint8) uint8 {
101+
b := mult(a, a)
102+
c := mult(a, b)
103+
b = mult(c, c)
104+
b = mult(b, b)
105+
c = mult(b, c)
106+
b = mult(b, b)
107+
b = mult(b, b)
108+
b = mult(b, c)
109+
b = mult(b, b)
110+
b = mult(a, b)
111+
112+
return mult(b, b)
113+
}
114+
115+
// mult multiplies two numbers in GF(2^8)
116+
func mult(a, b uint8) (out uint8) {
117+
var r uint8 = 0
118+
var i uint8 = 8
119+
120+
for i > 0 {
121+
i--
122+
r = (-(b >> i & 1) & a) ^ (-(r >> 7) & 0x1B) ^ (r + r)
123+
}
124+
125+
return r
126+
}
127+
128+
// add combines two numbers in GF(2^8)
129+
// This can also be used for subtraction since it is symmetric.
130+
func add(a, b uint8) uint8 {
131+
return a ^ b
132+
}
133+
134+
// Split takes an arbitrarily long secret and generates a `parts`
135+
// number of shares, `threshold` of which are required to reconstruct
136+
// the secret. The parts and threshold must be at least 2, and less
137+
// than 256. The returned shares are each one byte longer than the secret
138+
// as they attach a tag used to reconstruct the secret.
139+
func Split(secret []byte, parts, threshold int) ([][]byte, error) {
140+
// Sanity check the input
141+
if parts < threshold {
142+
return nil, fmt.Errorf("parts cannot be less than threshold")
143+
}
144+
if parts > 255 {
145+
return nil, fmt.Errorf("parts cannot exceed 255")
146+
}
147+
if threshold < 2 {
148+
return nil, fmt.Errorf("threshold must be at least 2")
149+
}
150+
if threshold > 255 {
151+
return nil, fmt.Errorf("threshold cannot exceed 255")
152+
}
153+
if len(secret) == 0 {
154+
return nil, fmt.Errorf("cannot split an empty secret")
155+
}
156+
157+
// Generate random list of x coordinates
158+
mathrand.Seed(time.Now().UnixNano())
159+
xCoordinates := mathrand.Perm(255)
160+
161+
// Allocate the output array, initialize the final byte
162+
// of the output with the offset. The representation of each
163+
// output is {y1, y2, .., yN, x}.
164+
out := make([][]byte, parts)
165+
for idx := range out {
166+
out[idx] = make([]byte, len(secret)+1)
167+
out[idx][len(secret)] = uint8(xCoordinates[idx]) + 1
168+
}
169+
170+
// Construct a random polynomial for each byte of the secret.
171+
// Because we are using a field of size 256, we can only represent
172+
// a single byte as the intercept of the polynomial, so we must
173+
// use a new polynomial for each byte.
174+
for idx, val := range secret {
175+
p, err := makePolynomial(val, uint8(threshold-1))
176+
if err != nil {
177+
return nil, fmt.Errorf("failed to generate polynomial: %w", err)
178+
}
179+
180+
// Generate a `parts` number of (x,y) pairs
181+
// We cheat by encoding the x value once as the final index,
182+
// so that it only needs to be stored once.
183+
for i := 0; i < parts; i++ {
184+
x := uint8(xCoordinates[i]) + 1
185+
y := p.evaluate(x)
186+
out[i][idx] = y
187+
}
188+
}
189+
190+
// Return the encoded secrets
191+
return out, nil
192+
}
193+
194+
// Combine is used to reverse a Split and reconstruct a secret
195+
// once a `threshold` number of parts are available.
196+
func Combine(parts [][]byte) ([]byte, error) {
197+
// Verify enough parts provided
198+
if len(parts) < 2 {
199+
return nil, fmt.Errorf("less than two parts cannot be used to reconstruct the secret")
200+
}
201+
202+
// Verify the parts are all the same length
203+
firstPartLen := len(parts[0])
204+
if firstPartLen < 2 {
205+
return nil, fmt.Errorf("parts must be at least two bytes")
206+
}
207+
for i := 1; i < len(parts); i++ {
208+
if len(parts[i]) != firstPartLen {
209+
return nil, fmt.Errorf("all parts must be the same length")
210+
}
211+
}
212+
213+
// Create a buffer to store the reconstructed secret
214+
secret := make([]byte, firstPartLen-1)
215+
216+
// Buffer to store the samples
217+
x_samples := make([]uint8, len(parts))
218+
y_samples := make([]uint8, len(parts))
219+
220+
// Set the x value for each sample and ensure no x_sample values are the same,
221+
// otherwise div() can be unhappy
222+
checkMap := map[byte]bool{}
223+
for i, part := range parts {
224+
samp := part[firstPartLen-1]
225+
if exists := checkMap[samp]; exists {
226+
return nil, fmt.Errorf("duplicate part detected")
227+
}
228+
checkMap[samp] = true
229+
x_samples[i] = samp
230+
}
231+
232+
// Reconstruct each byte
233+
for idx := range secret {
234+
// Set the y value for each sample
235+
for i, part := range parts {
236+
y_samples[i] = part[idx]
237+
}
238+
239+
// Interpolate the polynomial and compute the value at 0
240+
val := interpolatePolynomial(x_samples, y_samples, 0)
241+
242+
// Evaluate the 0th value to get the intercept
243+
secret[idx] = val
244+
}
245+
return secret, nil
246+
}

cmd/coordinator/run.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,7 @@ func run(log *zap.Logger, validator quote.Validator, issuer quote.Issuer, sealDi
8484
backend = "default"
8585
}
8686
store, keyDistributor := setUpStore(backend, sealer, sealDir, validator, issuer, log)
87-
distributedStore, _ := store.(*dstore.Store)
88-
89-
rec := recovery.New(distributedStore, log)
87+
rec := recovery.New(store, log)
9088

9189
// creating core
9290
log.Info("Creating the Core object")
@@ -98,7 +96,7 @@ func run(log *zap.Logger, validator quote.Validator, issuer quote.Issuer, sealDi
9896
}
9997

10098
// Add quote generator to store so instances regenerate their quotes depending on loaded state
101-
if distributedStore != nil {
99+
if distributedStore, ok := store.(*dstore.Store); ok {
102100
distributedStore.SetQuoteGenerator(co)
103101
}
104102
clientAPI, err := clientapi.New(store, rec, co, keyDistributor, log)

coordinator/clientapi/clientapi.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ func (a *ClientAPI) SetManifest(ctx context.Context, rawManifest []byte) (recove
344344
}
345345

346346
// Set encryption key & generate recovery data
347-
encryptionKey, err := a.recovery.GenerateEncryptionKey(mnf.RecoveryKeys)
347+
encryptionKey, err := a.recovery.GenerateEncryptionKey(mnf.RecoveryKeys, mnf.Config.RecoveryThreshold)
348348
if err != nil {
349349
a.log.Error("Could not set up encryption key for sealing the state", zap.Error(err))
350350
return nil, fmt.Errorf("generating recovery encryption key: %w", err)
@@ -558,6 +558,10 @@ func (a *ClientAPI) UpdateManifest(ctx context.Context, rawUpdateManifest []byte
558558
return nil, 0, errors.New("recovery keys cannot be updated")
559559
}
560560
}
561+
if currentManifest.Config.RecoveryThreshold != updateManifest.Config.RecoveryThreshold {
562+
a.log.Error("UpdateManifest: Invalid manifest: Recovery threshold cannot be updated")
563+
return nil, 0, errors.New("recovery threshold cannot be updated")
564+
}
561565

562566
// Get all users that are allowed to update the manifest
563567
// and create a new pending update

coordinator/clientapi/clientapi_test.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1093,6 +1093,33 @@ func TestUpdateManifest(t *testing.T) {
10931093
}(),
10941094
wantErr: true,
10951095
},
1096+
"invalid manifest: changed recovery threshold": {
1097+
updateManifest: func() manifest.Manifest {
1098+
manifest := testUpdateManifest()
1099+
manifest.Config.RecoveryThreshold = 2
1100+
return manifest
1101+
}(),
1102+
prepareAPI: func(require *require.Assertions, api *ClientAPI) {
1103+
// set up manifest with 3 recovery keys
1104+
manifest := testManifest()
1105+
manifest.RecoveryKeys["key2"] = manifest.RecoveryKeys["key"]
1106+
manifest.RecoveryKeys["key3"] = manifest.RecoveryKeys["key"]
1107+
manifest.Config.RecoveryThreshold = 3
1108+
manifestJSON, err := json.Marshal(manifest)
1109+
require.NoError(err)
1110+
_, err = api.SetManifest(ctx, manifestJSON)
1111+
require.NoError(err)
1112+
},
1113+
core: &fakeCore{
1114+
state: state.AcceptingManifest,
1115+
},
1116+
updater: func() *user.User {
1117+
u := user.NewUser("admin", mustParseCert(t, test.AdminCert))
1118+
u.Assign(user.NewPermission(user.PermissionUpdateManifest, []string{}))
1119+
return u
1120+
}(),
1121+
wantErr: true,
1122+
},
10961123
"symmetric secret file": {
10971124
updateManifest: func() manifest.Manifest {
10981125
mnf := testManifest()
@@ -1522,7 +1549,7 @@ type stubRecovery struct {
15221549
decryptRecoverySecretErr error
15231550
}
15241551

1525-
func (s *stubRecovery) GenerateEncryptionKey(_ map[string]string) ([]byte, error) {
1552+
func (s *stubRecovery) GenerateEncryptionKey(_ map[string]string, _ uint) ([]byte, error) {
15261553
return s.generateEncryptionKeyRes, s.generateEncryptionKeyErr
15271554
}
15281555

coordinator/clientapi/legacy_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,7 @@ func setupAPI(t *testing.T, core *fakeCore) (*ClientAPI, wrapper.Wrapper) {
647647

648648
return &ClientAPI{
649649
core: core,
650-
recovery: recovery.New(nil, log),
650+
recovery: recovery.New(store, log),
651651
txHandle: store,
652652
log: log,
653653
updateLog: updateLog,

0 commit comments

Comments
 (0)