Skip to content
Draft
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.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
83 changes: 63 additions & 20 deletions coordinator/clientapi/clientapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"encoding/pem"
"errors"
"fmt"
"maps"
"slices"
"strings"

Expand Down Expand Up @@ -352,26 +353,10 @@ func (a *ClientAPI) recover(ctx context.Context, encryptionKey, encryptionKeySig
if err != nil {
return -1, fmt.Errorf("loading manifest from store: %w", err)
}
if len(mnf.RecoveryKeys) != len(a.recoverySignatureCache) {
return -1, fmt.Errorf("recovery keys in manifest do not match the keys used for recovery: expected %d, got %d", len(mnf.RecoveryKeys), len(a.recoverySignatureCache))
}
for keyName, keyPEM := range mnf.RecoveryKeys {
pubKey, err := crypto.ParseRSAPublicKeyFromPEM(keyPEM)
if err != nil {
return -1, fmt.Errorf("parsing recovery public key %q: %w", keyName, err)
}

found := false
for key, signature := range a.recoverySignatureCache {
if err := util.VerifyPKCS1v15(pubKey, []byte(key), signature); err == nil {
found = true
delete(a.recoverySignatureCache, key)
break
}
}
if !found {
return -1, fmt.Errorf("no matching recovery key found for recovery public key %q", keyName)
}
// Make sure the provided recovery keys match the configuration given in the manifest
if err := validateRecoveryKeys(mnf, a.recoverySignatureCache); err != nil {
return -1, err
}

// cache SGX quote over the root certificate
Expand Down Expand Up @@ -464,7 +449,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 @@ -678,6 +663,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")
}

// Get all users that are allowed to update the manifest
// and create a new pending update
Expand Down Expand Up @@ -1119,3 +1108,57 @@ func encodeMonotonicCounterID(marbleType string, marbleUUID uuid.UUID, name stri
b64 := base64.StdEncoding.EncodeToString
return fmt.Sprintf("%s:%s:%s", b64([]byte(marbleType)), b64(marbleUUID[:]), b64([]byte(name)))
}

func validateRecoveryKeys(mnf manifest.Manifest, recoverySignatureCache map[string][]byte) error {
if mnf.Config.RecoveryThreshold == 0 || mnf.Config.RecoveryThreshold == uint(len(mnf.RecoveryKeys)) {
if len(mnf.RecoveryKeys) != len(recoverySignatureCache) {
return fmt.Errorf("recovery keys in manifest do not match the keys used for recovery: expected %d, got %d", len(mnf.RecoveryKeys), len(recoverySignatureCache))
}
for keyName, keyPEM := range mnf.RecoveryKeys {
pubKey, err := crypto.ParseRSAPublicKeyFromPEM(keyPEM)
if err != nil {
return fmt.Errorf("parsing recovery public key %q: %w", keyName, err)
}

found := false
for key, signature := range recoverySignatureCache {
if err := util.VerifyPKCS1v15(pubKey, []byte(key), signature); err == nil {
found = true
delete(recoverySignatureCache, key)
break
}
}
if !found {
return fmt.Errorf("no matching recovery key found for recovery public key %q", keyName)
}
}

return nil
}

if mnf.Config.RecoveryThreshold != uint(len(recoverySignatureCache)) {
return fmt.Errorf("recovery keys in manifest do not match the keys used for recovery: expected %d, got %d", mnf.Config.RecoveryThreshold, len(recoverySignatureCache))
}
validKeys := make(map[string]string)
maps.Copy(validKeys, mnf.RecoveryKeys)

for key, signature := range recoverySignatureCache {
found := false
for keyName, keyPEM := range validKeys {
pubKey, err := crypto.ParseRSAPublicKeyFromPEM(keyPEM)
if err != nil {
return fmt.Errorf("parsing recovery public key %q: %w", keyName, err)
}
if err := util.VerifyPKCS1v15(pubKey, []byte(key), signature); err == nil {
found = true
delete(validKeys, keyName)
break
}
}
if !found {
return errors.New("no matching recovery public key found for recovery key")
}
}

return nil
}
Loading