Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/check-license.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ jobs:
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0

- name: Check for files without license header
run: "! grep -rL --include='*.go' -e'Copyright (c) Edgeless Systems GmbH' -e'DO NOT EDIT' | grep ''"
run: "! grep -rL --include='*.go' --exclude-dir='3rdparty' -e'Copyright (c) Edgeless Systems GmbH' -e'DO NOT EDIT' | grep ''"
2 changes: 2 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ linters:
- (*go.uber.org/zap.Logger).Sync
exclusions:
generated: lax
paths:
- "3rdparty/"
rules:
# Simplified does not necessarily mean more readable
- linters: ["staticcheck"]
Expand Down
364 changes: 364 additions & 0 deletions 3rdparty/hashicorp/shamir/LICENSE

Large diffs are not rendered by default.

246 changes: 246 additions & 0 deletions 3rdparty/hashicorp/shamir/shamir.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package shamir

import (
"crypto/rand"
"crypto/subtle"
"fmt"
mathrand "math/rand"
"time"
)

const (
// ShareOverhead is the byte size overhead of each share
// when using Split on a secret. This is caused by appending
// a one byte tag to the share.
ShareOverhead = 1
)

// polynomial represents a polynomial of arbitrary degree
type polynomial struct {
coefficients []uint8
}

// makePolynomial constructs a random polynomial of the given
// degree but with the provided intercept value.
func makePolynomial(intercept, degree uint8) (polynomial, error) {
// Create a wrapper
p := polynomial{
coefficients: make([]byte, degree+1),
}

// Ensure the intercept is set
p.coefficients[0] = intercept

// Assign random co-efficients to the polynomial
if _, err := rand.Read(p.coefficients[1:]); err != nil {
return p, err
}

return p, nil
}

// evaluate returns the value of the polynomial for the given x
func (p *polynomial) evaluate(x uint8) uint8 {
// Special case the origin
if x == 0 {
return p.coefficients[0]
}

// Compute the polynomial value using Horner's method.
degree := len(p.coefficients) - 1
out := p.coefficients[degree]
for i := degree - 1; i >= 0; i-- {
coeff := p.coefficients[i]
out = add(mult(out, x), coeff)
}
return out
}

// interpolatePolynomial takes N sample points and returns
// the value at a given x using a lagrange interpolation.
func interpolatePolynomial(x_samples, y_samples []uint8, x uint8) uint8 {
limit := len(x_samples)
var result, basis uint8
for i := 0; i < limit; i++ {
basis = 1
for j := 0; j < limit; j++ {
if i == j {
continue
}
num := add(x, x_samples[j])
denom := add(x_samples[i], x_samples[j])
term := div(num, denom)
basis = mult(basis, term)
}
group := mult(y_samples[i], basis)
result = add(result, group)
}
return result
}

// div divides two numbers in GF(2^8)
func div(a, b uint8) uint8 {
if b == 0 {
// leaks some timing information but we don't care anyways as this
// should never happen, hence the panic
panic("divide by zero")
}

ret := int(mult(a, inverse(b)))

// Ensure we return zero if a is zero but aren't subject to timing attacks
ret = subtle.ConstantTimeSelect(subtle.ConstantTimeByteEq(a, 0), 0, ret)
return uint8(ret)
}

// inverse calculates the inverse of a number in GF(2^8)
func inverse(a uint8) uint8 {
b := mult(a, a)
c := mult(a, b)
b = mult(c, c)
b = mult(b, b)
c = mult(b, c)
b = mult(b, b)
b = mult(b, b)
b = mult(b, c)
b = mult(b, b)
b = mult(a, b)

return mult(b, b)
}

// mult multiplies two numbers in GF(2^8)
func mult(a, b uint8) (out uint8) {
var r uint8 = 0
var i uint8 = 8

for i > 0 {
i--
r = (-(b >> i & 1) & a) ^ (-(r >> 7) & 0x1B) ^ (r + r)
}

return r
}

// add combines two numbers in GF(2^8)
// This can also be used for subtraction since it is symmetric.
func add(a, b uint8) uint8 {
return a ^ b
}

// Split takes an arbitrarily long secret and generates a `parts`
// number of shares, `threshold` of which are required to reconstruct
// the secret. The parts and threshold must be at least 2, and less
// than 256. The returned shares are each one byte longer than the secret
// as they attach a tag used to reconstruct the secret.
func Split(secret []byte, parts, threshold int) ([][]byte, error) {
// Sanity check the input
if parts < threshold {
return nil, fmt.Errorf("parts cannot be less than threshold")
}
if parts > 255 {
return nil, fmt.Errorf("parts cannot exceed 255")
}
if threshold < 2 {
return nil, fmt.Errorf("threshold must be at least 2")
}
if threshold > 255 {
return nil, fmt.Errorf("threshold cannot exceed 255")
}
if len(secret) == 0 {
return nil, fmt.Errorf("cannot split an empty secret")
}

// Generate random list of x coordinates
mathrand.Seed(time.Now().UnixNano())
xCoordinates := mathrand.Perm(255)

// Allocate the output array, initialize the final byte
// of the output with the offset. The representation of each
// output is {y1, y2, .., yN, x}.
out := make([][]byte, parts)
for idx := range out {
out[idx] = make([]byte, len(secret)+1)
out[idx][len(secret)] = uint8(xCoordinates[idx]) + 1
}

// Construct a random polynomial for each byte of the secret.
// Because we are using a field of size 256, we can only represent
// a single byte as the intercept of the polynomial, so we must
// use a new polynomial for each byte.
for idx, val := range secret {
p, err := makePolynomial(val, uint8(threshold-1))
if err != nil {
return nil, fmt.Errorf("failed to generate polynomial: %w", err)
}

// Generate a `parts` number of (x,y) pairs
// We cheat by encoding the x value once as the final index,
// so that it only needs to be stored once.
for i := 0; i < parts; i++ {
x := uint8(xCoordinates[i]) + 1
y := p.evaluate(x)
out[i][idx] = y
}
}

// Return the encoded secrets
return out, nil
}

// Combine is used to reverse a Split and reconstruct a secret
// once a `threshold` number of parts are available.
func Combine(parts [][]byte) ([]byte, error) {
// Verify enough parts provided
if len(parts) < 2 {
return nil, fmt.Errorf("less than two parts cannot be used to reconstruct the secret")
}

// Verify the parts are all the same length
firstPartLen := len(parts[0])
if firstPartLen < 2 {
return nil, fmt.Errorf("parts must be at least two bytes")
}
for i := 1; i < len(parts); i++ {
if len(parts[i]) != firstPartLen {
return nil, fmt.Errorf("all parts must be the same length")
}
}

// Create a buffer to store the reconstructed secret
secret := make([]byte, firstPartLen-1)

// Buffer to store the samples
x_samples := make([]uint8, len(parts))
y_samples := make([]uint8, len(parts))

// Set the x value for each sample and ensure no x_sample values are the same,
// otherwise div() can be unhappy
checkMap := map[byte]bool{}
for i, part := range parts {
samp := part[firstPartLen-1]
if exists := checkMap[samp]; exists {
return nil, fmt.Errorf("duplicate part detected")
}
checkMap[samp] = true
x_samples[i] = samp
}

// Reconstruct each byte
for idx := range secret {
// Set the y value for each sample
for i, part := range parts {
y_samples[i] = part[idx]
}

// Interpolate the polynomial and compute the value at 0
val := interpolatePolynomial(x_samples, y_samples, 0)

// Evaluate the 0th value to get the intercept
secret[idx] = val
}
return secret, nil
}
6 changes: 2 additions & 4 deletions cmd/coordinator/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,7 @@ func run(log *zap.Logger, validator quote.Validator, issuer quote.Issuer, sealDi
backend = "default"
}
store, keyDistributor := setUpStore(backend, sealer, sealDir, validator, issuer, log)
distributedStore, _ := store.(*dstore.Store)

rec := recovery.New(distributedStore, log)
rec := recovery.New(store, log)

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

// Add quote generator to store so instances regenerate their quotes depending on loaded state
if distributedStore != nil {
if distributedStore, ok := store.(*dstore.Store); ok {
distributedStore.SetQuoteGenerator(co)
}
clientAPI, err := clientapi.New(store, rec, co, keyDistributor, log)
Expand Down
6 changes: 5 additions & 1 deletion coordinator/clientapi/clientapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ func (a *ClientAPI) SetManifest(ctx context.Context, rawManifest []byte) (recove
}

// Set encryption key & generate recovery data
encryptionKey, err := a.recovery.GenerateEncryptionKey(mnf.RecoveryKeys)
encryptionKey, err := a.recovery.GenerateEncryptionKey(mnf.RecoveryKeys, mnf.Config.RecoveryThreshold)
if err != nil {
a.log.Error("Could not set up encryption key for sealing the state", zap.Error(err))
return nil, fmt.Errorf("generating recovery encryption key: %w", err)
Expand Down Expand Up @@ -558,6 +558,10 @@ func (a *ClientAPI) UpdateManifest(ctx context.Context, rawUpdateManifest []byte
return nil, 0, errors.New("recovery keys cannot be updated")
}
}
if currentManifest.Config.RecoveryThreshold != updateManifest.Config.RecoveryThreshold {
a.log.Error("UpdateManifest: Invalid manifest: Recovery threshold cannot be updated")
return nil, 0, errors.New("recovery threshold cannot be updated")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to be sure: we will be able to allow updating this together with "Allow updating recovery public keys on manifest update", right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.
Its currently blocked since changing the threshold would require re-generating the recovery secrets, which is effectively the same as changing the keys

}

// Get all users that are allowed to update the manifest
// and create a new pending update
Expand Down
29 changes: 28 additions & 1 deletion coordinator/clientapi/clientapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1093,6 +1093,33 @@ func TestUpdateManifest(t *testing.T) {
}(),
wantErr: true,
},
"invalid manifest: changed recovery threshold": {
updateManifest: func() manifest.Manifest {
manifest := testUpdateManifest()
manifest.Config.RecoveryThreshold = 2
return manifest
}(),
prepareAPI: func(require *require.Assertions, api *ClientAPI) {
// set up manifest with 3 recovery keys
manifest := testManifest()
manifest.RecoveryKeys["key2"] = manifest.RecoveryKeys["key"]
manifest.RecoveryKeys["key3"] = manifest.RecoveryKeys["key"]
manifest.Config.RecoveryThreshold = 3
manifestJSON, err := json.Marshal(manifest)
require.NoError(err)
_, err = api.SetManifest(ctx, manifestJSON)
require.NoError(err)
},
core: &fakeCore{
state: state.AcceptingManifest,
},
updater: func() *user.User {
u := user.NewUser("admin", mustParseCert(t, test.AdminCert))
u.Assign(user.NewPermission(user.PermissionUpdateManifest, []string{}))
return u
}(),
wantErr: true,
},
"symmetric secret file": {
updateManifest: func() manifest.Manifest {
mnf := testManifest()
Expand Down Expand Up @@ -1522,7 +1549,7 @@ type stubRecovery struct {
decryptRecoverySecretErr error
}

func (s *stubRecovery) GenerateEncryptionKey(_ map[string]string) ([]byte, error) {
func (s *stubRecovery) GenerateEncryptionKey(_ map[string]string, _ uint) ([]byte, error) {
return s.generateEncryptionKeyRes, s.generateEncryptionKeyErr
}

Expand Down
2 changes: 1 addition & 1 deletion coordinator/clientapi/legacy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,7 @@ func setupAPI(t *testing.T, core *fakeCore) (*ClientAPI, wrapper.Wrapper) {

return &ClientAPI{
core: core,
recovery: recovery.New(nil, log),
recovery: recovery.New(store, log),
txHandle: store,
log: log,
updateLog: updateLog,
Expand Down
Loading