-
Notifications
You must be signed in to change notification settings - Fork 38
Enable recovery using a subset of recovery keys #879
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e8c043f
c418763
595f043
159ca5a
aee2a22
aa436d8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| 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 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
|
|
@@ -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") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. |
||
| } | ||
|
|
||
| // Get all users that are allowed to update the manifest | ||
| // and create a new pending update | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.