From 1e9df488a027aa6c2dee33c85fcd3e78b46cc52e Mon Sep 17 00:00:00 2001 From: Fernando Lobato Meeser Date: Fri, 25 Oct 2024 14:06:50 -0700 Subject: [PATCH] Introduce internal secret sharing implementation PiperOrigin-RevId: 689905835 Change-Id: I18790f905316fc3845f3ccacb0aefa547492fcb2 --- client/BUILD | 2 - client/cloudkms/BUILD | 2 - client/confidentialspace/BUILD | 2 - client/ekmclient/BUILD | 2 - client/internal/secret_sharing/OWNERS | 1 + .../internal/secret_sharing/finitefield/BUILD | 33 ++ .../secret_sharing/finitefield/finitefield.go | 39 ++ .../finitefield/finitefield_test.go | 32 ++ .../secret_sharing/internal/field/BUILD | 30 ++ .../secret_sharing/internal/field/field.go | 61 +++ .../secret_sharing/internal/field/gf32/BUILD | 43 +++ .../internal/field/gf32/gf32.go | 273 +++++++++++++ .../internal/field/gf32/gf32_test.go | 201 ++++++++++ .../secret_sharing/internal/field/gf8/BUILD | 42 ++ .../secret_sharing/internal/field/gf8/gf8.go | 180 +++++++++ .../internal/field/gf8/gf8_test.go | 360 ++++++++++++++++++ .../internal/shamirgeneric/BUILD | 27 ++ .../internal/shamirgeneric/shamir_generic.go | 235 ++++++++++++ .../shamirgeneric/shamir_generic_test.go | 202 ++++++++++ client/internal/secret_sharing/secrets/BUILD | 28 ++ .../secret_sharing/secrets/secrets.go | 43 +++ client/internal/secret_sharing/shamir/BUILD | 48 +++ .../internal/secret_sharing/shamir/shamir.go | 81 ++++ .../secret_sharing/shamir/shamir_test.go | 313 +++++++++++++++ client/jwt/BUILD | 1 - client/securesession/BUILD | 2 - client/shares/BUILD | 2 - client/testutil/BUILD | 1 - client/vpc/BUILD | 2 - cmd/conformance/BUILD | 1 - cmd/securesession/BUILD | 1 - cmd/server/BUILD | 1 - cmd/stet/BUILD | 1 - constants/BUILD | 1 - proto/BUILD | 1 - server/BUILD | 2 - transportshim/BUILD | 2 - 37 files changed, 2272 insertions(+), 26 deletions(-) create mode 100644 client/internal/secret_sharing/OWNERS create mode 100644 client/internal/secret_sharing/finitefield/BUILD create mode 100644 client/internal/secret_sharing/finitefield/finitefield.go create mode 100644 client/internal/secret_sharing/finitefield/finitefield_test.go create mode 100644 client/internal/secret_sharing/internal/field/BUILD create mode 100644 client/internal/secret_sharing/internal/field/field.go create mode 100644 client/internal/secret_sharing/internal/field/gf32/BUILD create mode 100644 client/internal/secret_sharing/internal/field/gf32/gf32.go create mode 100644 client/internal/secret_sharing/internal/field/gf32/gf32_test.go create mode 100644 client/internal/secret_sharing/internal/field/gf8/BUILD create mode 100644 client/internal/secret_sharing/internal/field/gf8/gf8.go create mode 100644 client/internal/secret_sharing/internal/field/gf8/gf8_test.go create mode 100644 client/internal/secret_sharing/internal/shamirgeneric/BUILD create mode 100644 client/internal/secret_sharing/internal/shamirgeneric/shamir_generic.go create mode 100644 client/internal/secret_sharing/internal/shamirgeneric/shamir_generic_test.go create mode 100644 client/internal/secret_sharing/secrets/BUILD create mode 100644 client/internal/secret_sharing/secrets/secrets.go create mode 100644 client/internal/secret_sharing/shamir/BUILD create mode 100644 client/internal/secret_sharing/shamir/shamir.go create mode 100644 client/internal/secret_sharing/shamir/shamir_test.go diff --git a/client/BUILD b/client/BUILD index d78ceb5..bb1aea9 100644 --- a/client/BUILD +++ b/client/BUILD @@ -13,8 +13,6 @@ # limitations under the License. load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") -load("//tools/build_defs/go:go_library.bzl", "go_library") -load("//tools/build_defs/go:go_test.bzl", "go_test") package( default_applicable_licenses = ["//:license"], diff --git a/client/cloudkms/BUILD b/client/cloudkms/BUILD index 72df236..bfe59ea 100644 --- a/client/cloudkms/BUILD +++ b/client/cloudkms/BUILD @@ -13,8 +13,6 @@ # limitations under the License. load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") -load("//tools/build_defs/go:go_library.bzl", "go_library") -load("//tools/build_defs/go:go_test.bzl", "go_test") package( default_visibility = ["//:__subpackages__"], diff --git a/client/confidentialspace/BUILD b/client/confidentialspace/BUILD index d3257ae..0bf7f35 100644 --- a/client/confidentialspace/BUILD +++ b/client/confidentialspace/BUILD @@ -13,8 +13,6 @@ # limitations under the License. load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") -load("//tools/build_defs/go:go_library.bzl", "go_library") -load("//tools/build_defs/go:go_test.bzl", "go_test") package( default_visibility = ["//:__subpackages__"], diff --git a/client/ekmclient/BUILD b/client/ekmclient/BUILD index 010613f..a675a54 100644 --- a/client/ekmclient/BUILD +++ b/client/ekmclient/BUILD @@ -13,8 +13,6 @@ # limitations under the License. load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") -load("//tools/build_defs/go:go_library.bzl", "go_library") -load("//tools/build_defs/go:go_test.bzl", "go_test") package( default_visibility = ["//:__subpackages__"], diff --git a/client/internal/secret_sharing/OWNERS b/client/internal/secret_sharing/OWNERS new file mode 100644 index 0000000..bedc616 --- /dev/null +++ b/client/internal/secret_sharing/OWNERS @@ -0,0 +1 @@ +mdb-group:ise-crypto diff --git a/client/internal/secret_sharing/finitefield/BUILD b/client/internal/secret_sharing/finitefield/BUILD new file mode 100644 index 0000000..30f1547 --- /dev/null +++ b/client/internal/secret_sharing/finitefield/BUILD @@ -0,0 +1,33 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +package( + default_visibility = [ + "//client/internal/secret_sharing:__subpackages__", + ], +) + +go_library( + name = "finitefield", + srcs = ["finitefield.go"], + importpath = "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/finitefield", +) + +go_test( + name = "finitefield_test", + srcs = ["finitefield_test.go"], + deps = [":finitefield"], +) diff --git a/client/internal/secret_sharing/finitefield/finitefield.go b/client/internal/secret_sharing/finitefield/finitefield.go new file mode 100644 index 0000000..8ec0d43 --- /dev/null +++ b/client/internal/secret_sharing/finitefield/finitefield.go @@ -0,0 +1,39 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package finitefield represents the finite fields supported by the secret sharing library. +package finitefield + +import "fmt" + +// ID represents a finite field supported by the secret sharing library. +type ID int + +const ( + // GF32 is a Galois Field with characteristic 2^5. + GF32 ID = 1 + iota + // GF8 is a Galois Field with characteristic 2^8. + GF8 +) + +func (id ID) String() string { + switch id { + case GF8: + return "GF8" + case GF32: + return "GF32" + default: + return fmt.Sprintf("unknown finite field ID: %d", id) + } +} diff --git a/client/internal/secret_sharing/finitefield/finitefield_test.go b/client/internal/secret_sharing/finitefield/finitefield_test.go new file mode 100644 index 0000000..b2bb955 --- /dev/null +++ b/client/internal/secret_sharing/finitefield/finitefield_test.go @@ -0,0 +1,32 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package finitefield_test + +import ( + "testing" + + "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/finitefield" +) + +func TestFieldIDString(t *testing.T) { + want := "GF32" + if got := finitefield.GF32.String(); got != want { + t.Errorf("finitefield.GF32.String() = %q, want %q", got, want) + } + want = "GF8" + if got := finitefield.GF8.String(); got != want { + t.Errorf("finitefield.GF8.String() = %q, want %q", got, want) + } +} diff --git a/client/internal/secret_sharing/internal/field/BUILD b/client/internal/secret_sharing/internal/field/BUILD new file mode 100644 index 0000000..d1b1458 --- /dev/null +++ b/client/internal/secret_sharing/internal/field/BUILD @@ -0,0 +1,30 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +package( + default_visibility = [ + "//client/internal/secret_sharing:__subpackages__", + ], +) + +licenses(["notice"]) + +go_library( + name = "field", + srcs = ["field.go"], + importpath = "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/internal/field", + deps = ["//client/internal/secret_sharing/finitefield"], +) diff --git a/client/internal/secret_sharing/internal/field/field.go b/client/internal/secret_sharing/internal/field/field.go new file mode 100644 index 0000000..c6ad84f --- /dev/null +++ b/client/internal/secret_sharing/internal/field/field.go @@ -0,0 +1,61 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package field defines a generic definition of a finite field. +package field + +import "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/finitefield" + +// Element is an element in a Finite Field +type Element interface { + // Add element `a` and returns a new element. + Add(a Element) Element + // Subtract element `a` and returns a new element. + Subtract(a Element) Element + // Multiply by element `a` and returns a new element. + Multiply(a Element) Element + // Inverse returns an element that's the multiplicative inverse. + // If element has no inverse, an error is returned. + Inverse() (Element, error) + // GT returns true if the element `b` is greater than. + GT(b Element) bool + // Bytes returns the element in a big endian encoded byte representation. + Bytes() []byte + // Flip flips an element by multiplying the element by the group order, + // Flip is only required if the order of elements in substraction affects the result, hence some + // fields might return the same element. + Flip() Element +} + +// GaloisField represents a Finite Field. +type GaloisField interface { + // CreateElement creates a new field element from i. The value of i should be within the range + // of unsigned integers that can be stored in a byte array of length ElementSize(). + CreateElement(i int) (Element, error) + // NewRandomNonZero generates a random element inside the field. + // The random element is assumed to be good enough for cryptographic purposes. + NewRandomNonZero() (Element, error) + // ReadElement reads an element from a big endian encoded byte slice b at an offset i. + ReadElement(b []byte, i int) (Element, error) + // EncodeElements encodes a set of field elements into a byte slice of size secLen. + // The output of this function can be passed to DecodeElements() to recreate the elements. + EncodeElements(parts []Element, secLen int) ([]byte, error) + // DecodeElements creates a set of field elements from a byte slice. + // Expects the output of EncodeElements(). + DecodeElements([]byte) []Element + // ElementSize returns the size of each element in bytes. + ElementSize() int + // FieldID returns a unique identifier for the field. + FieldID() finitefield.ID +} diff --git a/client/internal/secret_sharing/internal/field/gf32/BUILD b/client/internal/secret_sharing/internal/field/gf32/BUILD new file mode 100644 index 0000000..e2a3d05 --- /dev/null +++ b/client/internal/secret_sharing/internal/field/gf32/BUILD @@ -0,0 +1,43 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +package( + default_visibility = [ + "//client/internal/secret_sharing:__subpackages__", + ], +) + +licenses(["notice"]) + +go_library( + name = "gf32", + srcs = ["gf32.go"], + importpath = "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/internal/field/gf32", + deps = [ + "//client/internal/secret_sharing/finitefield", + "//client/internal/secret_sharing/internal/field", + ], +) + +go_test( + name = "gf32_test", + size = "small", + srcs = ["gf32_test.go"], + deps = [ + "//client/internal/secret_sharing/internal/field/gf32", + "@com_github_google_go_cmp//cmp:go_default_library", + ], +) diff --git a/client/internal/secret_sharing/internal/field/gf32/gf32.go b/client/internal/secret_sharing/internal/field/gf32/gf32.go new file mode 100644 index 0000000..f893bd0 --- /dev/null +++ b/client/internal/secret_sharing/internal/field/gf32/gf32.go @@ -0,0 +1,273 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package gf32 implements a finite field of characteristic 2^5. +package gf32 + +import ( + "crypto/rand" + "encoding/binary" + "fmt" + "math/bits" + + "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/finitefield" + "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/internal/field" +) + +const ( + primeGF32 = 2147483659 + factor = 8589934548 // (1 << bitlen(primeGF32) * 2) // primeGF32 + barretLimit = (2147483659 * 2147483659) - 1 // primeGF32 ^ 2 - 1 + + bitPerSubsecret = 31 + elementSizeBytes = 4 +) + +var ( + maxGF32 = newElement(primeGF32 - 1) +) + +// Element is an element in the GF32 +type Element struct { + Value uint32 +} + +var _ field.Element = (*Element)(nil) + +func newElement(v uint32) field.Element { + return &Element{Value: v} +} + +// Add element by 'x' modulo the field order. +func (e *Element) Add(x field.Element) field.Element { + return newElement(mod(uint64(e.Value) + uint64(x.(*Element).Value))) +} + +// Subtract element by 'x' modulo the field order. +func (e *Element) Subtract(x field.Element) field.Element { + return newElement(mod(uint64(e.Value) + primeGF32 - uint64(x.(*Element).Value))) +} + +// Multiply element by 'x' modulo the field order. +func (e *Element) Multiply(x field.Element) field.Element { + return newElement(multiplyMod(e.Value, x.(*Element).Value)) +} + +// Inverse returns the multiplicative inverse for an element in the field. +func (e *Element) Inverse() (field.Element, error) { + ne, err := modInverse(e.Value) + if err != nil { + return nil, err + } + return newElement(ne), nil +} + +// Flip flips a value +func (e *Element) Flip() field.Element { + return e.Multiply(maxGF32) +} + +// GT returns true if the element is greater than 'b'. +func (e *Element) GT(b field.Element) bool { + return e.Value > b.(*Element).Value +} + +// Bytes returns a big endian representation of the element value as a byte slice. +func (e *Element) Bytes() []byte { + o := make([]byte, 4, 4) + binary.BigEndian.PutUint32(o, e.Value) + return o +} + +// barretReduce performs barret reduction which calculates `a mod n` +// replacing divisions by multiplications. This is advantageous because +// DIV instructions are not commonly constant time, while multiplication +// tend to be. +// For a more detailed explanation see "Handbook of Applied Cryptography", +// Chapter 14.3.3 - https://cacr.uwaterloo.ca/hac/about/chap14.pdf +// +// In a standard `a mod n` calculation barret reduction approximates 1/n +// with a value m/(2^k), division by 2^k is just a right shift. +// m/(2^k) = 1/n <-> m = (2^k) / n +func barretReduce(x uint64) uint32 { + // This barret reduction only works for values between 0 and `n^2``, however field elements should + // be in this range. Secrets are read in 31-bit chunks so they can't be greater than `n`. Field + // elements get the `%` operator applied when created, but not during arithmetic operations. Hence + // every element should be in the field and the maximum value can be (n-1)^2. + if x > barretLimit { + panic(fmt.Sprintf("value out of range: %d", x)) + } + // This is equivalent to multiplication by (m/(2^k)) + // Mul64 multiplies `x` by `factor` and returns the result in two unsigned 64-bit values, + // representing a 128-bit unsigned value. we skip multuplying by 1/(2^k), since + // k in this case is always 64, we'll be shifting 64 bytes, which means we can just + // ignore the lower limb. + hi, _ := bits.Mul64(uint64(x), uint64(factor)) + t := x - (uint64(hi) * primeGF32) + if t < primeGF32 { + return uint32(t) + } + return uint32(t - primeGF32) +} + +func mod(a uint64) uint32 { + return barretReduce(a) +} + +func multiplyMod(a, b uint32) uint32 { + return mod(uint64(a) * uint64(b)) +} + +func modInverse(a uint32) (uint32, error) { + if a == 0 { + return 0, fmt.Errorf("modular inverse isn't defined for identity element") + } + var inverse uint32 = 1 + for exponent := primeGF32 - 2; exponent > 0; exponent >>= 1 { + if exponent&1 == 1 { + inverse = multiplyMod(inverse, a) + } + a = multiplyMod(a, a) + } + return inverse, nil +} + +// Field is a finite field with characteristic 2^5 (GF32). +type Field struct{} + +// New creates a new GF32. +func New() field.GaloisField { return &Field{} } + +var _ field.GaloisField = (*Field)(nil) + +func divideRoundUp(a, b int) int { + return (a + b - 1) / b +} + +// FieldID returns an ID for the specific field implemented. +func (e *Field) FieldID() finitefield.ID { + return finitefield.GF32 +} + +// ElementSize returns the size in bytes of elements in the field. +func (e *Field) ElementSize() int { + return elementSizeBytes +} + +// CreateElement creates an element in the field by performing a modulo +// operation over the field order. This function isn't guaranteed to execute +// in constant time. +func (e *Field) CreateElement(o int) (field.Element, error) { + return newElement(uint32(o % primeGF32)), nil +} + +// NewRandomNonZero returns a random non-zero element in the field. +func (e *Field) NewRandomNonZero() (field.Element, error) { + b := make([]byte, 4) + for { + clear(b) + if _, err := rand.Read(b); err != nil { + return newElement(0), fmt.Errorf("rand.Read failed: %v", err) + } + r := binary.BigEndian.Uint32(b) + if r < primeGF32 && r != 0 { + return newElement(r), nil + } + } +} + +// ReadElement reads a field element from a byte slice, in GF32 each element +// is a uint32. The function builds an integer by taking the next 4 bytes at +// the offset and interprets them as a big endian encoded unsigned integer. +func (e *Field) ReadElement(b []byte, i int) (field.Element, error) { + if len(b) < ((i * elementSizeBytes) + elementSizeBytes) { + return nil, fmt.Errorf("b (len = %d), is smaller than offset %d", len(b), i) + } + subshare := uint32(0) + for k := 0; k < elementSizeBytes; k++ { + j := elementSizeBytes*i + k + if j >= len(b) { + break + } + subshare <<= 8 + subshare += uint32(b[j]) + } + return newElement(subshare), nil +} + +// EncodeElements encode field elements into a byte slice. +func (e *Field) EncodeElements(parts []field.Element, secLen int) ([]byte, error) { + secret := make([]byte, secLen, secLen) + bitsDone := 0 + j := len(parts) - 1 + + secretParts := make([]uint32, len(parts), len(parts)) + for i, p := range parts { + secretParts[i] = p.(*Element).Value + } + + for i := len(secret) - 1; i >= 0 && j >= 0; i-- { + if bitPerSubsecret-bitsDone > 8 { + secret[i] = uint8((secretParts[j] >> bitsDone) & 0xFF) + bitsDone += 8 + } else { + nextLowBits := uint8(secretParts[j] >> bitsDone) + j-- + if j >= 0 { + secret[i] = uint8( + secretParts[j] & (0xFF >> (bitPerSubsecret - bitsDone))) + } + bitsDone = (bitsDone + 8) % bitPerSubsecret + secret[i] <<= 8 - bitsDone + secret[i] |= nextLowBits + } + } + return secret, nil +} + +// DecodeElements translates the byte slice into a set of elements in +// GF32. The slice is divided into 31-bit chunks, this allows the use of unsigned +// 64-bit integer multiplication without worrying about overflowing. +func (e *Field) DecodeElements(s []byte) []field.Element { + var bitsDone = 0 + var bitsPerSub = 31 + + n := divideRoundUp(len(s)*8, bitsPerSub) + parts := make([]uint32, n, n) + + currSub := len(parts) - 1 + + for i := len(s) - 1; i >= 0; i-- { + currByte := uint8(s[i]) + if bitsPerSub-bitsDone > 8 { + parts[currSub] |= uint32(currByte) << bitsDone + bitsDone += 8 + continue + } + + currByteRight := currByte & (0xFF >> (8 - (bitsPerSub - bitsDone))) + parts[currSub] |= uint32(currByteRight) << bitsDone + + if !(i == 0 && bitsDone+8 == bitsPerSub) { + bitsDone = (bitsDone + 8) % bitsPerSub + currSub-- + parts[currSub] |= uint32(currByte) >> (8 - bitsDone) + } + } + out := []field.Element{} + for _, p := range parts { + out = append(out, newElement(p)) + } + return out +} diff --git a/client/internal/secret_sharing/internal/field/gf32/gf32_test.go b/client/internal/secret_sharing/internal/field/gf32/gf32_test.go new file mode 100644 index 0000000..06e1d35 --- /dev/null +++ b/client/internal/secret_sharing/internal/field/gf32/gf32_test.go @@ -0,0 +1,201 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gf32_test + +import ( + "crypto/rand" + "encoding/binary" + "testing" + + "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/internal/field/gf32" + "github.com/google/go-cmp/cmp" +) + +func getRandomBytes(t *testing.T, n int) []byte { + t.Helper() + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + t.Fatalf("Failed to read random bytes: %v", err) + } + return b +} + +func TestEncodeDecode(t *testing.T) { + type testCase struct { + tag string + b []byte + } + for _, tc := range []testCase{ + { + tag: "small value no carry over", + b: []byte{0x01, 0x02, 0x03}, + }, + { + tag: "small value no carry over", + b: []byte{0x01, 0x02, 0x03, 0x04}, + }, + { + tag: "5 bytes pushes on bit causing carry", + b: []byte{0x01, 0x02, 0x03, 0x04, 0x05}, + }, + { + tag: "8 bytes pushes on bit causing carry", + b: []byte{0xFF, 0x02, 0x03, 0x04, 0xFF, 0x06, 0x07, 0x08, 0xFF}, + }, + { + tag: "32 bytes pushes on bit causing carry", + b: getRandomBytes(t, 32), + }, + { + tag: "large random value", + b: getRandomBytes(t, 300), + }, + } { + gf := gf32.New() + t.Run(tc.tag, func(t *testing.T) { + e := gf.DecodeElements(tc.b) + got, err := gf.EncodeElements(e, len(tc.b)) + if err != nil { + t.Fatalf("EncodeElements() err = %v, want nil", err) + } + if !cmp.Equal(got, tc.b) { + t.Errorf("ConvertGF32 got %v, want %v, intermediate: %v", got, tc.b, e) + } + }) + } +} + +const fieldPrimeOrder = 2147483659 + +func TestFieldArithmetic(t *testing.T) { + type testCase struct { + tag string + a int + b int + sum uint32 + mult uint32 + sub uint32 + } + gf := gf32.New() + for _, tc := range []testCase{ + { + tag: "small values", + a: 2, + b: 5, + sum: 7, + mult: 10, + sub: 2147483656, + }, + { + tag: "field order", + a: fieldPrimeOrder - 1, + b: 1, + sum: 0, + mult: fieldPrimeOrder - 1, + sub: fieldPrimeOrder - 2, + }, + { + tag: "field order + 1", + a: fieldPrimeOrder - 1, + b: 2, + sum: 1, + mult: fieldPrimeOrder - 2, + sub: fieldPrimeOrder - 3, + }, + { + tag: "max integer values", + a: (1 << 32), + b: (1 << 32), + sum: 2147483615, + mult: 484, + sub: 0, + }, + } { + t.Run(tc.tag, func(t *testing.T) { + ea, err := gf.CreateElement(tc.a) + if err != nil { + t.Fatalf("CreateElement(%d) err = %v, want nil", tc.a, err) + } + eb, err := gf.CreateElement(tc.b) + if err != nil { + t.Fatalf("CreateElement(%d) err = %v, want nil", tc.b, err) + } + rsum := binary.BigEndian.Uint32(ea.Add(eb).Bytes()) + rsub := binary.BigEndian.Uint32(ea.Subtract(eb).Bytes()) + rmult := binary.BigEndian.Uint32(ea.Multiply(eb).Bytes()) + if rsum != tc.sum { + t.Errorf("%d + %d got = %d, want %d", tc.a, tc.b, rsum, tc.sum) + } + if rmult != tc.mult { + t.Errorf("%d * %d got = %d, want %d", tc.a, tc.b, rmult, tc.mult) + } + if rsub != tc.sub { + t.Errorf("%d - %d got = %d, want %d", tc.a, tc.b, rsub, tc.sub) + } + }) + } +} + +func TestModularInverse(t *testing.T) { + type testCase struct { + tag string + e int + inv uint32 + } + gf := gf32.New() + for _, tc := range []testCase{ + { + tag: "identity", + e: 1, + inv: 1, + }, + { + tag: "order - 1", + e: fieldPrimeOrder - 1, + inv: fieldPrimeOrder - 1, + }, + { + tag: "larger than order", + e: 21474836580, + inv: 1932735293, + }, + } { + t.Run(tc.tag, func(t *testing.T) { + e, err := gf.CreateElement(tc.e) + if err != nil { + t.Fatalf("CreateElement(%d) err = %v, want nil", tc.e, err) + } + i, err := e.Inverse() + if err != nil { + t.Fatalf("e.Inverse() err = %v, want nil", err) + } + rinv := binary.BigEndian.Uint32(i.Bytes()) + if rinv != tc.inv { + t.Errorf("%d ^-1 = %d, want %d", tc.e, rinv, tc.inv) + } + }) + } +} + +func TestIdentityInverseFails(t *testing.T) { + e, err := gf32.New().CreateElement(0) + if err != nil { + t.Fatalf("CreateElement(0) err = %v, want nil", err) + } + if _, err = e.Inverse(); err == nil { + t.Fatalf("Inverse() err = nil, want non-nil error") + } +} diff --git a/client/internal/secret_sharing/internal/field/gf8/BUILD b/client/internal/secret_sharing/internal/field/gf8/BUILD new file mode 100644 index 0000000..c282678 --- /dev/null +++ b/client/internal/secret_sharing/internal/field/gf8/BUILD @@ -0,0 +1,42 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +package( + default_visibility = [ + "//client/internal/secret_sharing:__subpackages__", + ], +) + +go_library( + name = "gf8", + srcs = ["gf8.go"], + importpath = "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/internal/field/gf8", + deps = [ + "//client/internal/secret_sharing/finitefield", + "//client/internal/secret_sharing/internal/field", + ], +) + +go_test( + name = "gf8_test", + srcs = ["gf8_test.go"], + deps = [ + ":gf8", + "//client/internal/secret_sharing/finitefield", + "//client/internal/secret_sharing/internal/field", + "@com_github_google_go_cmp//cmp:go_default_library", + ], +) diff --git a/client/internal/secret_sharing/internal/field/gf8/gf8.go b/client/internal/secret_sharing/internal/field/gf8/gf8.go new file mode 100644 index 0000000..a5f0706 --- /dev/null +++ b/client/internal/secret_sharing/internal/field/gf8/gf8.go @@ -0,0 +1,180 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package gf8 implements with a field with characteristic 2^8 (GF(2^8)). +package gf8 + +import ( + "crypto/rand" + "fmt" + + "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/finitefield" + "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/internal/field" +) + +type element byte + +// Add element `a` and returns a new element in GF(2^8). +func (e element) Add(a field.Element) field.Element { + return e ^ a.(element) +} + +// Subtract element `a` and returns a new element in GF(2^8). +func (e element) Subtract(a field.Element) field.Element { + return e.Add(a) +} + +// irreducible polynomial (x^8 + x^4 + x^3 + x + 1) +// (x^8 + x^4 + x^3 + x + 1) = {0x01 0x1B} +// we deal with uint8 so we only need 0x1B +const irreduciblePolynomial = 0x1B + +// Multiply by element `a` and returns a new element. +func (e element) Multiply(a field.Element) field.Element { + // This function tries to defend against side-channel attacks + // (timing, cache), hence avoiding pre-computed tables and branches. + x := byte(e) + y := byte(a.(element)) + + var product uint8 = 0 + + // Similar steps to: + // https://en.wikipedia.org/wiki/Finite_field_arithmetic#Multiplication + // This code avoids branching by negating values (ex:`-foo`) + // negating values produces a mask of either all zeros or ones + // which allows AND operations without branching. + // + for i := 7; i >= 0; i-- { + + // if MSB in current product is set, mod is irreduciblePolynomial, else 0 + mod := (-(product >> 7)) & irreduciblePolynomial + + // multiply coefficient x[i] with every coefficient in y + xiTimesY := -((x >> i) & 1) & y + + // reduce the multiplication by irreduciblePolynomial if MSB in product was + // set and left shift product + product = xiTimesY ^ mod ^ (product << 1) + } + return element(product) +} + +// Inverse returns an element that's the multiplicative inverse. +// If element has no inverse, an error is returned. +func (e element) Inverse() (field.Element, error) { + if e == 0 { + return nil, fmt.Errorf("inverse of zero is not defined") + } + // we calculate the multiplicative inverse (e^-1) by computing: + // e^254, which in GF(2^8) is (e^-1) + // multiplication chain reference: https://crypto.stackexchange.com/a/40140 + + b := e.Multiply(e) // e^2 + c := e.Multiply(b) // e^3 + + b = c.Multiply(c) // e^6 = (e^3)^2 + b = b.Multiply(b) // e^12 = (e^6)^2 + c = b.Multiply(c) // e^15 = (e^12) * (e^3) + b = b.Multiply(b) // e^30 = (e^15)^2 + b = b.Multiply(b) // e^60 = (e^30)^2 + b = b.Multiply(c) // e^63 = (e^60) * (e^3) + b = b.Multiply(b) // e^126 = (e^63)^2 + b = e.Multiply(b) // e^127 = (e^126) * e + return b.Multiply(b), nil // e^254 = (e^127)^2 +} + +// GT returns true if element is greater than 'b'. +func (e element) GT(b field.Element) bool { + return e > b.(element) +} + +// Bytes returns a big endian representation of the element value as a byte array. +func (e element) Bytes() []byte { + return []byte{byte(e)} +} + +// Flip is needed in circumstances where substraction is done over two elements based on which is +// larger, and then a flip is get the equivalent value. In other fields, this requires multiplication +// by the field order. In GF(2^8), addition and substraction are the same operation (xor), hence this isn't necessary. +func (e element) Flip() field.Element { + return e +} + +type gf8Field struct{} + +// New creates a new GF8. +func New() field.GaloisField { return &gf8Field{} } + +var _ field.GaloisField = (*gf8Field)(nil) + +// CreateElement creates a new field element from an integer. +// Returns an error when i is larger than the largest uint8 (255). +func (e *gf8Field) CreateElement(i int) (field.Element, error) { + if i > 255 { + return nil, fmt.Errorf("field element can't be larger than %d bytes", e.ElementSize()) + } + return element(i), nil +} + +// NewRandomNonZero generates a random element inside the field. +// The random element is assumed to be good enough for cryptographic purposes. +func (e *gf8Field) NewRandomNonZero() (field.Element, error) { + b := make([]byte, 1) + for { + clear(b) + if _, err := rand.Read(b); err != nil { + return element(0), fmt.Errorf("rand.Read failed: %v", err) + } + if b[0] != 0 { + return element(b[0]), nil + } + } +} + +// ReadElement reads an element from a big endian encoded byte array `b` at an offset `i`. +func (e *gf8Field) ReadElement(b []byte, i int) (field.Element, error) { + if len(b) < i { + return element(0), fmt.Errorf("b (len = %d), is smaller than offset %d", len(b), i) + } + return element(b[i]), nil +} + +// EncodeElements encodes a set of field elements into a byte array of size `secLen` . +func (e *gf8Field) EncodeElements(parts []field.Element, secLen int) ([]byte, error) { + if secLen != len(parts) { + return nil, fmt.Errorf("can't encode elements (len = %d) into secret len (%d)", len(parts), secLen) + } + elems := make([]byte, secLen, secLen) + for i, e := range parts { + elems[i] = byte(e.(element)) + } + return elems, nil +} + +// DecodeElements decodes a byte array into a set of elements in GF(2^8). +func (e *gf8Field) DecodeElements(in []byte) []field.Element { + elems := make([]field.Element, len(in)) + for i, b := range in { + elems[i] = element(b) + } + return elems +} + +func (e *gf8Field) ElementSize() int { + return 1 +} + +func (e *gf8Field) FieldID() finitefield.ID { + return finitefield.GF8 +} diff --git a/client/internal/secret_sharing/internal/field/gf8/gf8_test.go b/client/internal/secret_sharing/internal/field/gf8/gf8_test.go new file mode 100644 index 0000000..5452d4f --- /dev/null +++ b/client/internal/secret_sharing/internal/field/gf8/gf8_test.go @@ -0,0 +1,360 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gf8_test + +import ( + "crypto/rand" + "fmt" + "testing" + + "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/finitefield" + "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/internal/field" + "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/internal/field/gf8" + "github.com/google/go-cmp/cmp" +) + +func getRandomBytes(t *testing.T, n int) []byte { + t.Helper() + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + t.Fatalf("Failed to read random bytes: %v", err) + } + return b +} + +func TestAddition(t *testing.T) { + f := gf8.New() + for range 10 { + elems := getRandomBytes(t, 2) + + e1, err := f.CreateElement(int(elems[0])) + if err != nil { + t.Fatal(err) + } + e2, err := f.CreateElement(int(elems[1])) + if err != nil { + t.Fatal(err) + } + + got := e1.Add(e2).Bytes()[0] + + if got != elems[0]^elems[1] { + t.Fatalf("a(%d) + b(%b), got = %d, want = %d", elems[0], elems[1], got, elems[0]^elems[1]) + } + } +} + +func TestSubtraction(t *testing.T) { + f := gf8.New() + for range 10 { + elems := getRandomBytes(t, 2) + + e1, err := f.CreateElement(int(elems[0])) + if err != nil { + t.Fatal(err) + } + e2, err := f.CreateElement(int(elems[1])) + if err != nil { + t.Fatal(err) + } + + got := e1.Subtract(e2).Bytes()[0] + + if got != elems[0]^elems[1] { + t.Errorf("a(%d) - b(%b), got = %d, want = %d", elems[0], elems[1], got, elems[0]^elems[1]) + } + } +} + +func TestMultiplication(t *testing.T) { + f := gf8.New() + for _, tc := range []struct { + a byte + b byte + want byte + }{ + // The following test cases are taken from various online examples of AES finite field + // arithmetic, which uses GF(2^8) over the same irreducible polynomial: + // - https://en.wikipedia.org/wiki/Finite_field_arithmetic#Rijndael's_(AES)_finite_field + // - https://uomustansiriyah.edu.iq/media/lectures/5/5_2020_12_28!10_55_23_AM.pdf + { + a: 0x53, + b: 0xCA, + want: 0x01, + }, + { + a: 0x02, + b: 0x87, + want: 0x15, + }, + { + a: 0x03, + b: 0x6E, + want: 0xB2, + }, + // The following test cases where generated manually: + // http://www.ee.unb.ca/cgi-bin/tervo/calc2.pl + { + a: 161, + b: 56, + want: 102, + }, + { + a: 51, + b: 82, + want: 15, + }, + { + a: 15, + b: 30, + want: 170, + }, + { + a: 105, + b: 27, + want: 20, + }, + { + a: 178, + b: 160, + want: 67, + }, + { + a: 244, + b: 118, + want: 55, + }, + { + a: 250, + b: 221, + want: 160, + }, + { + a: 244, + b: 34, + want: 90, + }, + } { + t.Run(fmt.Sprintf("%d * %d", tc.a, tc.b), func(t *testing.T) { + a, err := f.CreateElement(int(tc.a)) + if err != nil { + t.Fatal(err) + } + b, err := f.CreateElement(int(tc.b)) + if err != nil { + t.Fatal(err) + } + got := uint8(a.Multiply(b).Bytes()[0]) + if got != tc.want { + t.Errorf("a(%d) * b(%d), got = %d, want = %d", tc.a, tc.b, got, tc.want) + } + }) + } +} + +func TestInverse(t *testing.T) { + f := gf8.New() + for _, tc := range []struct { + a byte + want byte + }{ + // Test case was taken from AES FF arithmetic example: https://en.wikipedia.org/wiki/Finite_field_arithmetic#Rijndael's_(AES)_finite_field + { + a: 0x53, want: 0xCA, + }, + // The following test cases where generated manually: + // http://www.ee.unb.ca/cgi-bin/tervo/calc2.pl + { + a: 29, want: 64, + }, + { + a: 180, want: 17, + }, + { + a: 249, want: 156, + }, + { + a: 186, want: 118, + }, + { + a: 209, want: 7, + }, + { + a: 233, want: 78, + }, + { + a: 242, want: 56, + }, + { + a: 249, want: 156, + }, + } { + t.Run(fmt.Sprintf("inverse(%d)", tc.a), func(t *testing.T) { + a, err := f.ReadElement([]byte{tc.a}, 0) + if err != nil { + t.Fatal(err) + } + inv, err := a.Inverse() + if err != nil { + t.Fatal(err) + } + got := uint8(inv.Bytes()[0]) + if got != tc.want { + t.Errorf("inverse(%d), got = %d, want = %d", tc.a, got, tc.want) + } + }) + } +} + +func TestIdentityInverseFails(t *testing.T) { + e, err := gf8.New().CreateElement(0) + if err != nil { + t.Fatal(err) + } + if _, err = e.Inverse(); err == nil { + t.Fatalf("Inverse() err = nil, want non-nil error") + } +} + +func TestGreaterThan(t *testing.T) { + f := gf8.New() + for _, tc := range []struct { + a byte + b byte + want bool + }{ + { + a: 0, + b: 1, + want: false, + }, + { + a: 1, + b: 0, + want: true, + }, + { + a: 1, + b: 1, + want: false, + }, + { + a: 0, + b: 0, + want: false, + }, + { + a: 255, + b: 254, + want: true, + }, + } { + t.Run(fmt.Sprintf("%d > %d", tc.a, tc.b), func(t *testing.T) { + a, err := f.CreateElement(int(tc.a)) + if err != nil { + t.Fatal(err) + } + b, err := f.CreateElement(int(tc.b)) + if err != nil { + t.Fatal(err) + } + if got := a.GT(b); got != tc.want { + t.Errorf("GT(%d, %d) = %v, want %v", tc.a, tc.b, got, tc.want) + } + }) + } +} + +func TestCreateElementLargerThanFieldFails(t *testing.T) { + f := gf8.New() + if _, err := f.CreateElement(256); err == nil { + t.Fatalf("CreateElement(256) err = nil, want non-nil error") + } +} + +func TestBytes(t *testing.T) { + f := gf8.New() + for range 10 { + a := getRandomBytes(t, 1) + e, err := f.CreateElement(int(a[0])) + if err != nil { + t.Fatal(err) + } + got := e.Bytes() + if !cmp.Equal(got, a) { + t.Errorf("Bytes() got = %d, want = %d", got, a) + } + } +} + +func TestFlip(t *testing.T) { + f := gf8.New() + a := getRandomBytes(t, 1) + e, err := f.CreateElement(int(a[0])) + if err != nil { + t.Fatal(err) + } + got := e.Flip().Bytes() + if !cmp.Equal(got, a) { + t.Errorf("Flip() got = %d, want = %d", got, a) + } +} + +func TestDecodeElements(t *testing.T) { + f := gf8.New() + r := getRandomBytes(t, 30) + elements := f.DecodeElements(r) + for i, e := range elements { + if e.Bytes()[0] != r[i] { + t.Errorf("DecodeElements()[%d] got = %d, want = %d", i, e.Bytes()[0], r[i]) + } + } +} + +func TestEncodeDecode(t *testing.T) { + f := gf8.New() + n := 10 + elems := make([]field.Element, n) + for i := range n { + var err error + if elems[i], err = f.NewRandomNonZero(); err != nil { + t.Fatal(err) + } + } + encoded, err := f.EncodeElements(elems, n) + if err != nil { + t.Fatal(err) + } + + decoded := f.DecodeElements(encoded) + if !cmp.Equal(elems, decoded) { + t.Fatalf("EncodeElements(DecodeElements()) got = %q, want = %q", decoded, elems) + } +} + +func TestFieldElementSize(t *testing.T) { + got := gf8.New().ElementSize() + if got != 1 { + t.Fatalf("gf8.New().ElementSize() got = %d, want %d", got, 1) + } +} + +func TestFieldID(t *testing.T) { + got := gf8.New().FieldID() + if got != finitefield.GF8 { + t.Fatalf("gf8.New().FieldID() got = %q, want %q", got, finitefield.GF8) + } +} diff --git a/client/internal/secret_sharing/internal/shamirgeneric/BUILD b/client/internal/secret_sharing/internal/shamirgeneric/BUILD new file mode 100644 index 0000000..e269bab --- /dev/null +++ b/client/internal/secret_sharing/internal/shamirgeneric/BUILD @@ -0,0 +1,27 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//client:__subpackages__"], +) + +go_library( + name = "shamirgeneric", + srcs = ["shamir_generic.go"], + importpath = "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/internal/shamirgeneric", + deps = [ + "//client/internal/secret_sharing/internal/field", + "//client/internal/secret_sharing/secrets", + ], +) + +go_test( + name = "shamirgeneric_test", + srcs = ["shamir_generic_test.go"], + deps = [ + ":shamirgeneric", + "//client/internal/secret_sharing/finitefield", + "//client/internal/secret_sharing/internal/field/gf32", + "//client/internal/secret_sharing/secrets", + ], +) diff --git a/client/internal/secret_sharing/internal/shamirgeneric/shamir_generic.go b/client/internal/secret_sharing/internal/shamirgeneric/shamir_generic.go new file mode 100644 index 0000000..4ab826b --- /dev/null +++ b/client/internal/secret_sharing/internal/shamirgeneric/shamir_generic.go @@ -0,0 +1,235 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package shamirgeneric implements shamir secret sharing with a generic group structure. +package shamirgeneric + +import ( + "fmt" + + "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/internal/field" + "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/secrets" +) + +// SplitSecret splits a secret into n shares where t or more shares can be combined to reconstruct +// the original secret using shamir secret sharing. +func SplitSecret(metadata secrets.Metadata, secret []byte, gf field.GaloisField) (secrets.Split, error) { + if err := validateSplitInput(metadata, secret, gf); err != nil { + return secrets.Split{}, err + } + threshold := metadata.Threshold + numShares := metadata.NumShares + // The `secret` can be an arbitrary length byte array, but each element in a field is of + // a finite size, hence the `secret` is split into a set of elements in the field. + subsecrets := gf.DecodeElements(secret) + shares := make([]secrets.Share, numShares) + + // For each subsecret we build a polynomial of degree N, where N is `threshold`. + // Each subsecret is the constant coefficient in the polynomial and every other coefficient + // is selected as a random field element: + // subsecret + R_1 * x^1 + R_2 * X^2 + ... + R_N * X^N + for _, subsecret := range subsecrets { + coefficients := make([]field.Element, threshold, threshold) + coefficients[0] = subsecret + for i := 1; i < threshold; i++ { + var err error + if coefficients[i], err = gf.NewRandomNonZero(); err != nil { + return secrets.Split{}, err + } + } + for i := 0; i < numShares; i++ { + // We create each sub-share by evaluating each polynomial for a subsecret + // at a specific value `X`, this gives us the point (X, Y). + xi, err := gf.CreateElement(i + 1) + if err != nil { + return secrets.Split{}, err + } + subshare, err := evaluatePolynomial(coefficients, xi, gf) + if err != nil { + return secrets.Split{}, err + } + // shares is a set of encoded field elements. Each field element is the evaluation of a + // different polynomial where the constant term of each polynomial represents a subsecrets. + // shares[0] = [ F1(1), F2(1), ..., FN(1) ] + // shares[1] = [ F1(2), F2(2), ..., FN(2) ] + // shares[N - 1] = [ F1(N), F2(N), ..., FN(N) ] + shares[i].Value = append(shares[i].Value, subshare.Bytes()...) + shares[i].X = i + 1 + } + } + return secrets.Split{ + Shares: shares, + Metadata: metadata, + SecretLen: len(secret), + }, nil +} + +// evaluates a polynomial at `x` where `coefficients` take the form: +// f(x) = c[n-1] * x^(n-1) + c[n-2] * x^(n-2) + ... + c[1] * x^1 + c[0] +// Evaluation assumes no coefficient is zero and performs all arithmetic +// over a finite field. +func evaluatePolynomial(coefficients []field.Element, x field.Element, gf field.GaloisField) (field.Element, error) { + sum, err := gf.CreateElement(0) + if err != nil { + return nil, err + } + for i := len(coefficients) - 1; i > 0; i-- { + sum = sum.Add(coefficients[i]).Multiply(x) + } + return sum.Add(coefficients[0]), nil +} + +// Reconstruct reconstructs a secret with at least t out of n shares using shamir secret sharing. +func Reconstruct(splitSecret secrets.Split, gf field.GaloisField) ([]byte, error) { + if err := validateReconstructInput(splitSecret); err != nil { + return nil, err + } + // We only need `threshold` shares to reconstruct the secrets. + shares := splitSecret.Shares[:splitSecret.Metadata.Threshold] + xVals := []field.Element{} + for _, s := range shares { + xi, err := gf.CreateElement(s.X) + if err != nil { + return nil, err + } + xVals = append(xVals, xi) + } + // Precompute the Lagrange coefficients before performing polynomial interpolation. + // The output to this step could be kept in memory, but it would require making + // this implementation thread safe. + coefficients, err := lagrangeCoefficients(xVals, gf) + if err != nil { + return nil, err + } + // Calculate the number of field elements per secret share based on the share size. + var numSubSecrets = len(shares[0].Value) / gf.ElementSize() + subsecrets := make([]field.Element, numSubSecrets, numSubSecrets) + for i := 0; i < numSubSecrets; i++ { + yVals := make([]field.Element, len(xVals)) + + for j, s := range shares { + yVals[j], err = gf.ReadElement(s.Value, i) + if err != nil { + return nil, err + } + } + // interpolatePolynomial recovers the C[0] coefficient, the geometric interpretation + // of the intersection with the Y axis. + subsecrets[i], err = interpolatePolynomial(coefficients, yVals, gf) + if err != nil { + return nil, err + } + } + // combine the subsecret field elements into the original secrets. + return gf.EncodeElements(subsecrets, splitSecret.SecretLen) +} + +// performs lagrange polynomial interpolation to recover a polynomial from a set of points. +// receives a set of points on a finite field: +// ∑i={1,n} y[i] * ( ∏j={1,n,j≠i} ( (x[j]) / ( x[j] - x[i]) ) ) +// lagrange coefficients (∏j={1,n,j≠i} ( (x[j]) / ( x[j] - x[i] ) )) are precalculated +// and the y coordinates are used to compute the sum. This function assumes, no coefficient is zero. +func interpolatePolynomial(lagCoeff []field.Element, yVals []field.Element, gf field.GaloisField) (field.Element, error) { + if len(lagCoeff) != len(yVals) { + return nil, fmt.Errorf("invalid lagrange coefficients") + } + sum, err := gf.CreateElement(0) + if err != nil { + return nil, err + } + // ∑i={1,n} y[i] * lagrange_coefficient[i] + for i, y := range yVals { + sum = sum.Add(y.Multiply(lagCoeff[i])) + } + return sum, nil +} + +// recovers the coefficients to perform lagrange polynomial interpolation using the x coordinates. +// ∏j={1,n,j≠i} ( (x[j]) / ( x[j] - x[i] ) ) +func lagrangeCoefficients(x []field.Element, gf field.GaloisField) ([]field.Element, error) { + if len(x) < 2 { + return nil, fmt.Errorf("must have at least 2 values") + } + out := []field.Element{} + for i := 0; i < len(x); i++ { + one, err := gf.CreateElement(1) + if err != nil { + return nil, err + } + out = append(out, one) + for j := 0; j < len(x); j++ { + if i == j { + continue + } + if x[i] == x[j] { + return nil, fmt.Errorf("all shares should be unique point") + } + out[i] = out[i].Multiply(x[j]) + // Perform ( x[j] * ( x[j] - x[i] )^-1 ) + // if x[j] > x[i]: (x[j] - x[i])^-1 * out[i] + // if x[j] < x[i] ((x[i] - x[j])^-1 * out[i]) - 1 mod FieldOrder + x1, x2 := x[j], x[i] + if !x[j].GT(x[i]) { + x1, x2 = x[i], x[j] + out[i] = out[i].Flip() + } + diff, err := x1.Subtract(x2).Inverse() + if err != nil { + return nil, err + } + out[i] = out[i].Multiply(diff) + } + } + return out, nil +} + +func validateSplitInput(metadata secrets.Metadata, secret []byte, gf field.GaloisField) error { + if len(secret) == 0 { + return fmt.Errorf("secret must not be nil") + } + if metadata.NumShares < 2 { + return fmt.Errorf("numShares must be larger than 1") + } + if metadata.Threshold < 2 { + return fmt.Errorf("threshold must be larger than 1") + } + if metadata.Threshold > metadata.NumShares { + return fmt.Errorf("threshold should be smaller than or equal to numShares") + } + if metadata.Field != gf.FieldID() { + return fmt.Errorf("field ID mismatch") + } + return nil +} + +func validateReconstructInput(splitSecret secrets.Split) error { + if splitSecret.Metadata.Threshold < 2 { + return fmt.Errorf("threshold should be at least 2") + } + if splitSecret.Metadata.NumShares < splitSecret.Metadata.Threshold { + return fmt.Errorf("threshold larger than number of shares") + } + if len(splitSecret.Shares) < splitSecret.Metadata.Threshold { + return fmt.Errorf("not enough shares to reconstruct the secret, need at least %d, got: %d", len(splitSecret.Shares), splitSecret.Metadata.Threshold) + } + for _, s := range splitSecret.Shares { + if s.X == 0 { + return fmt.Errorf("invalid X value") + } + if len(s.Value) == 0 { + return fmt.Errorf("empty secret value") + } + } + return nil +} diff --git a/client/internal/secret_sharing/internal/shamirgeneric/shamir_generic_test.go b/client/internal/secret_sharing/internal/shamirgeneric/shamir_generic_test.go new file mode 100644 index 0000000..50bcc59 --- /dev/null +++ b/client/internal/secret_sharing/internal/shamirgeneric/shamir_generic_test.go @@ -0,0 +1,202 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package shamirgeneric_test + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + "testing" + + "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/finitefield" + "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/internal/field/gf32" + "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/internal/shamirgeneric" + "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/secrets" +) + +func getRandomBytes(t *testing.T, n int) []byte { + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + t.Fatalf("Failed to read random bytes: %v", err) + } + return b +} + +func createMetadata(threshold, numShares int) secrets.Metadata { + return secrets.Metadata{ + Field: finitefield.GF32, + NumShares: numShares, + Threshold: threshold, + } +} + +func TestSplitReconstructWorks(t *testing.T) { + secret := []byte("abcdefghijklmnopqrstuvwxyz123456") + split, err := shamirgeneric.SplitSecret(createMetadata(4, 6), secret, gf32.New()) + if err != nil { + t.Fatalf("shamirgeneric.SplitSecret() err = %v, want nil", err) + } + recon, err := shamirgeneric.Reconstruct(split, gf32.New()) + if err != nil { + t.Fatal(err) + } + if got, want := []byte(recon), secret; !bytes.Equal(got, want) { + t.Errorf("got %v, want %v", hex.EncodeToString(got), hex.EncodeToString(want)) + } +} + +func TestSplitReconstructLargeValues(t *testing.T) { + secret := getRandomBytes(t, 300) + split, err := shamirgeneric.SplitSecret(createMetadata(50, 80), secret, gf32.New()) + if err != nil { + t.Fatal(err) + } + recon, err := shamirgeneric.Reconstruct(split, gf32.New()) + if err != nil { + t.Fatal(err) + } + if got, want := []byte(recon), secret; !bytes.Equal(got, want) { + t.Errorf("got %v, want %v", hex.EncodeToString(got), hex.EncodeToString(want)) + } +} + +func removeAtIndex(s []secrets.Share, index int) []secrets.Share { + return append(s[:index], s[index+1:]...) +} + +func swap(s []secrets.Share, i int, j int) { + s[i], s[j] = s[j], s[i] +} + +func TestReconstructWithoutAllShares(t *testing.T) { + secret := []byte("abcdefghijklmnopqrstuvwxyz123456") + split, err := shamirgeneric.SplitSecret(createMetadata(4, 6), secret, gf32.New()) + if err != nil { + t.Fatal(err) + } + split.Shares = removeAtIndex(split.Shares, 5) + split.Shares = removeAtIndex(split.Shares, 0) + recon, err := shamirgeneric.Reconstruct(split, gf32.New()) + if err != nil { + t.Fatal(err) + } + if got, want := []byte(recon), secret; !bytes.Equal(got, want) { + t.Errorf("got %v, want %v", hex.EncodeToString(got), hex.EncodeToString(want)) + } + // swapping the order shouldn't matter. + swap(split.Shares, 0, 2) + if got, want := []byte(recon), secret; !bytes.Equal(got, want) { + t.Errorf("got %v, want %v", hex.EncodeToString(got), hex.EncodeToString(want)) + } +} + +func TestReconstructWithAlteredValueBeforeThresholdFails(t *testing.T) { + secret := getRandomBytes(t, 32) + split, err := shamirgeneric.SplitSecret(createMetadata(2, 3), secret, gf32.New()) + if err != nil { + t.Fatalf("shamirgeneric.SplitSecret() err = %v, want nil", err) + } + split.Shares[0].Value = getRandomBytes(t, len(split.Shares[0].Value)) + recon, err := shamirgeneric.Reconstruct(split, gf32.New()) + if err != nil { + t.Fatal(err) + } + if got, want := []byte(recon), secret; bytes.Equal(got, want) { + t.Errorf("reconsturcting altered value should fail") + } +} + +func TestReconstructWithAlteredValueAfterThresholdDoesNotAffectResult(t *testing.T) { + secret := getRandomBytes(t, 32) + split, err := shamirgeneric.SplitSecret(createMetadata(2, 3), secret, gf32.New()) + if err != nil { + t.Fatalf("shamirgeneric.SplitSecret() err = %v, want nil", err) + } + split.Shares[2].Value = getRandomBytes(t, len(split.Shares[0].Value)) + recon, err := shamirgeneric.Reconstruct(split, gf32.New()) + if err != nil { + t.Fatal(err) + } + if got, want := []byte(recon), secret; !bytes.Equal(got, want) { + t.Errorf("got %v, want %v", hex.EncodeToString(got), hex.EncodeToString(want)) + } +} + +func TestWithLessSharesThanThresholdFails(t *testing.T) { + secret := []byte("abcdefghijklmnopqrstuvwxyz123456") + splitSecret, err := shamirgeneric.SplitSecret(createMetadata(4, 6), secret, gf32.New()) + if err != nil { + t.Fatal(err) + } + splitSecret.Shares = removeAtIndex(splitSecret.Shares, 5) + splitSecret.Shares = removeAtIndex(splitSecret.Shares, 1) + splitSecret.Shares = removeAtIndex(splitSecret.Shares, 0) + if _, err := shamirgeneric.Reconstruct(splitSecret, gf32.New()); err == nil { + t.Fatalf("Reconstruct() err = nil, want error") + } +} + +func TestReconstructFromStaticShares(t *testing.T) { + shares := []secrets.Share{ + {Value: []byte{112, 207, 118, 46, 110, 212, 170, 28}, X: 1}, + {Value: []byte{48, 160, 197, 172, 38, 235, 145, 204}, X: 2}, + {Value: []byte{63, 115, 238, 144, 40, 68, 183, 71}, X: 3}, + {Value: []byte{29, 72, 240, 207, 114, 224, 26, 141}, X: 4}, + {Value: []byte{74, 31, 204, 116, 6, 189, 187, 136}, X: 5}, + } + want := []byte{0, 0, 0, byte(uint8(33))} + split := secrets.Split{ + Shares: shares, + Metadata: secrets.Metadata{ + Field: finitefield.GF32, + NumShares: len(shares), + Threshold: 3, + }, + SecretLen: len(want), + } + recon, err := shamirgeneric.Reconstruct(split, gf32.New()) + if err != nil { + t.Fatal(err) + } + if got := []byte(recon); !bytes.Equal(got, want) { + t.Errorf("got %v, want %v", hex.EncodeToString(got), hex.EncodeToString(want)) + } +} + +func TestReconstructFromStaticSharesWithLessThanN(t *testing.T) { + shares := []secrets.Share{ + {Value: []byte{112, 207, 118, 46, 110, 212, 170, 28}, X: 1}, + {Value: []byte{29, 72, 240, 207, 114, 224, 26, 141}, X: 4}, + {Value: []byte{74, 31, 204, 116, 6, 189, 187, 136}, X: 5}, + } + want := []byte{0, 0, 0, byte(uint8(33))} + split := secrets.Split{ + Shares: shares, + Metadata: secrets.Metadata{ + Field: finitefield.GF32, + NumShares: len(shares), + Threshold: 3, + }, + SecretLen: len(want), + } + recon, err := shamirgeneric.Reconstruct(split, gf32.New()) + if err != nil { + t.Fatal(err) + } + if got := []byte(recon); !bytes.Equal(got, want) { + t.Errorf("got %v, want %v", hex.EncodeToString(got), hex.EncodeToString(want)) + } +} diff --git a/client/internal/secret_sharing/secrets/BUILD b/client/internal/secret_sharing/secrets/BUILD new file mode 100644 index 0000000..578d3d3 --- /dev/null +++ b/client/internal/secret_sharing/secrets/BUILD @@ -0,0 +1,28 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +package( + default_visibility = [ + "//client/internal/secret_sharing:__subpackages__", + ], +) + +go_library( + name = "secrets", + srcs = ["secrets.go"], + importpath = "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/secrets", + deps = ["//client/internal/secret_sharing/finitefield"], +) diff --git a/client/internal/secret_sharing/secrets/secrets.go b/client/internal/secret_sharing/secrets/secrets.go new file mode 100644 index 0000000..6549a54 --- /dev/null +++ b/client/internal/secret_sharing/secrets/secrets.go @@ -0,0 +1,43 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package secrets contains types for secret sharing. When splitting a secret, a dealer needs +// to provide both the `secret` + `Metadata`. A dealer would then get a `Split`, which contains +// the `Metadata`, the secret shares, and the secret length. +package secrets + +import ( + "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/finitefield" +) + +// Metadata contains the necessary secret sharing scheme information to split and/or reconstruct a secret. +type Metadata struct { + Field finitefield.ID + NumShares int + Threshold int +} + +// Split represents a secret split into shares alongside the metadata needed to reconstruct it. +type Split struct { + Metadata Metadata + Shares []Share + // The length of the original split secret in bytes. + SecretLen int +} + +// Share represents one share of a shared secret without any metadata. +type Share struct { + Value []byte + X int +} diff --git a/client/internal/secret_sharing/shamir/BUILD b/client/internal/secret_sharing/shamir/BUILD new file mode 100644 index 0000000..0bf96fe --- /dev/null +++ b/client/internal/secret_sharing/shamir/BUILD @@ -0,0 +1,48 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +package( + default_visibility = [ + "//client/internal/secret_sharing:__subpackages__", + ], +) + +licenses(["notice"]) + +go_library( + name = "shamir", + srcs = ["shamir.go"], + importpath = "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/shamir", + deps = [ + "//client/internal/secret_sharing/finitefield", + "//client/internal/secret_sharing/internal/field", + "//client/internal/secret_sharing/internal/field/gf32", + "//client/internal/secret_sharing/internal/field/gf8", + "//client/internal/secret_sharing/internal/shamirgeneric", + "//client/internal/secret_sharing/secrets", + ], +) + +go_test( + name = "shamir_test", + size = "small", + srcs = ["shamir_test.go"], + deps = [ + ":shamir", + "//client/internal/secret_sharing/finitefield", + "//client/internal/secret_sharing/secrets", + ], +) diff --git a/client/internal/secret_sharing/shamir/shamir.go b/client/internal/secret_sharing/shamir/shamir.go new file mode 100644 index 0000000..4f2e9af --- /dev/null +++ b/client/internal/secret_sharing/shamir/shamir.go @@ -0,0 +1,81 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package shamir encapsulates all of the logic needed to perform t-of-n [Shamir +// Secret Sharing] (SSS) on arbitrary-size secrets over +// a finite field. SSS is based on the Lagrange interpolation theorem, which +// states that `k` points are enough to uniquely determine a polynomial of +// degree less than or equal to `k - 1`. +// +// This scheme is secure under the following assumptions: +// - The scheme requires a trusted dealer to generate the shares. Participants +// must trust the dealer with access to the secret and to properly generate the +// shares. +// - The scheme assumes a passive adversary which can observe (n - t) shares +// without being able to reconstruct the secrets. However, this scheme +// assumes the adversary isn't allowed to participate in the `reconstruct` step by +// providing a chosen share. +// Examples of this attack: https://crypto.stackexchange.com/q/41994/76875 +// +// [Shamir Secret Sharing]: https://web.mit.edu/6.857/OldStuff/Fall03/ref/Shamir-HowToShareAsecrets.pdf +package shamir + +import ( + "fmt" + + "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/finitefield" + "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/internal/field" + "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/internal/field/gf32" + "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/internal/field/gf8" + "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/internal/shamirgeneric" + "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/secrets" +) + +func createField(fieldID finitefield.ID) (field.GaloisField, error) { + switch fieldID { + case finitefield.GF32: + return gf32.New(), nil + case finitefield.GF8: + return gf8.New(), nil + default: + return nil, fmt.Errorf("invalid field: %q", fieldID) + } +} + +// SplitSecret splits a secret into metadata.NumShares shares where metadata.Threshold +// or more shares can be combined to reconstruct the original secret. +func SplitSecret(metadata secrets.Metadata, secret []byte) (secrets.Split, error) { + f, err := createField(metadata.Field) + if err != nil { + return secrets.Split{}, err + } + return shamirgeneric.SplitSecret(metadata, secret, f) +} + +// Reconstruct reconstructs the secret from secretSplit. +// +// The number of shares provided must meet the threshold specified when the +// shares were created by [SplitSecret]. +// +// Reconstruct will not detect bogus or corrupted shares. +func Reconstruct(secretSplit secrets.Split) ([]byte, error) { + if len(secretSplit.Shares) == 0 { + return nil, fmt.Errorf("no shares provided") + } + f, err := createField(secretSplit.Metadata.Field) + if err != nil { + return nil, err + } + return shamirgeneric.Reconstruct(secretSplit, f) +} diff --git a/client/internal/secret_sharing/shamir/shamir_test.go b/client/internal/secret_sharing/shamir/shamir_test.go new file mode 100644 index 0000000..e9fe4cd --- /dev/null +++ b/client/internal/secret_sharing/shamir/shamir_test.go @@ -0,0 +1,313 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package shamir_test + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + "testing" + + "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/finitefield" + "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/secrets" + "github.com/GoogleCloudPlatform/stet/client/internal/secret_sharing/shamir" +) + +const smallSecret = "abcdefghijklmnopqrstuvwxyz123456" + +func getRandomBytes(t *testing.T, n int) []byte { + t.Helper() + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + t.Fatalf("Failed to read random bytes: %v", err) + } + return b +} + +func removeAtIndex(s []secrets.Share, index int) []secrets.Share { + return append(s[:index], s[index+1:]...) +} + +func swap(s []secrets.Share, i int, j int) { + s[i], s[j] = s[j], s[i] +} + +type testCase struct { + name string + secret []byte + metadata secrets.Metadata + shares []secrets.Share +} + +func TestSplitReconstructWorks(t *testing.T) { + for _, tc := range []testCase{ + { + name: "small secret gf32 n-6 t-4", + secret: []byte(smallSecret), + metadata: secrets.Metadata{ + Field: finitefield.GF32, + NumShares: 6, + Threshold: 4, + }, + }, + { + name: "large secret gf32 n-80 t-50", + secret: getRandomBytes(t, 300), + metadata: secrets.Metadata{ + Field: finitefield.GF32, + NumShares: 80, + Threshold: 50, + }, + }, + { + name: "small secret g8 n-6 t-4", + secret: []byte(smallSecret), + metadata: secrets.Metadata{ + Field: finitefield.GF8, + NumShares: 6, + Threshold: 4, + }, + }, + { + name: "large secret g8 n-80 t-50", + secret: getRandomBytes(t, 300), + metadata: secrets.Metadata{ + Field: finitefield.GF8, + NumShares: 80, + Threshold: 50, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + splitSecret, err := shamir.SplitSecret(tc.metadata, tc.secret) + if err != nil { + t.Fatalf("shamir.SplitSecret() err = %v, want nil", err) + } + recon, err := shamir.Reconstruct(splitSecret) + if err != nil { + t.Fatal(err) + } + if got, want := recon, tc.secret; !bytes.Equal(got, want) { + t.Errorf("got %v, want %v", hex.EncodeToString(got), hex.EncodeToString(want)) + } + }) + } +} + +func buildTestVectors(t *testing.T, numShares, threshold int) []testCase { + t.Helper() + return []testCase{ + { + name: "GF32", + secret: getRandomBytes(t, 32), + metadata: secrets.Metadata{Field: finitefield.GF32, NumShares: numShares, Threshold: threshold}, + }, + { + name: "GF8", + secret: getRandomBytes(t, 32), + metadata: secrets.Metadata{Field: finitefield.GF8, NumShares: numShares, Threshold: threshold}, + }, + } +} + +func TestReconstructWithoutAllShares(t *testing.T) { + numShares := 6 + threshold := 4 + for _, tc := range buildTestVectors(t, numShares, threshold) { + t.Run(tc.name, func(t *testing.T) { + splitSecret, err := shamir.SplitSecret(tc.metadata, tc.secret) + if err != nil { + t.Fatal(err) + } + splitSecret.Shares = removeAtIndex(splitSecret.Shares, 5) + splitSecret.Shares = removeAtIndex(splitSecret.Shares, 0) + recon, err := shamir.Reconstruct(splitSecret) + if err != nil { + t.Fatal(err) + } + if got, want := recon, tc.secret; !bytes.Equal(got, want) { + t.Errorf("got %v, want %v", hex.EncodeToString(got), hex.EncodeToString(want)) + } + // swapping the order shouldn't matter. + swap(splitSecret.Shares, 0, 2) + if got, want := recon, tc.secret; !bytes.Equal(got, want) { + t.Errorf("got %v, want %v", hex.EncodeToString(got), hex.EncodeToString(want)) + } + }) + } +} + +func TestReconstructWithAlteredValueUnderThresholdFails(t *testing.T) { + numShares := 3 + threshold := 2 + for _, tc := range buildTestVectors(t, numShares, threshold) { + t.Run(tc.name, func(t *testing.T) { + splitSecret, err := shamir.SplitSecret(tc.metadata, tc.secret) + if err != nil { + t.Fatalf("shamir.SplitSecret() err = %v, want nil", err) + } + splitSecret.Shares[0].Value = getRandomBytes(t, len(splitSecret.Shares[0].Value)) + // reconstruct shouldn't return an error + recon, err := shamir.Reconstruct(splitSecret) + if err != nil { + t.Fatalf("shamir.Reconstruct() err = %v, want nil", err) + } + if got, want := recon, tc.secret; bytes.Equal(got, want) { + t.Errorf("reconsturcting altered value should fail") + } + }) + } +} + +func TestReconstructWithAlteredValueAboveThresholdDoesNotAffectResult(t *testing.T) { + numShares := 3 + threshold := 2 + for _, tc := range buildTestVectors(t, numShares, threshold) { + t.Run(tc.name, func(t *testing.T) { + splitSecret, err := shamir.SplitSecret(tc.metadata, tc.secret) + if err != nil { + t.Fatalf("shamir.SplitSecret() err = %v, want nil", err) + } + splitSecret.Shares[2].Value = getRandomBytes(t, len(splitSecret.Shares[0].Value)) + recon, err := shamir.Reconstruct(splitSecret) + if err != nil { + t.Fatal(err) + } + if got, want := recon, tc.secret; !bytes.Equal(got, want) { + t.Errorf("got %v, want %v", hex.EncodeToString(got), hex.EncodeToString(want)) + } + }) + } +} + +func TestReconstructWithFewerSharesThanThresholdFails(t *testing.T) { + numShares := 6 + threshold := 4 + for _, tc := range buildTestVectors(t, numShares, threshold) { + t.Run(tc.name, func(t *testing.T) { + splitSecret, err := shamir.SplitSecret(tc.metadata, tc.secret) + if err != nil { + t.Fatal(err) + } + splitSecret.Shares = removeAtIndex(splitSecret.Shares, 5) + splitSecret.Shares = removeAtIndex(splitSecret.Shares, 1) + splitSecret.Shares = removeAtIndex(splitSecret.Shares, 0) + if _, err := shamir.Reconstruct(splitSecret); err == nil { + t.Fatalf("Reconstruct() err = nil, want error") + } + }) + } +} + +func TestReconstructFromStaticShares(t *testing.T) { + for _, tc := range []testCase{ + { + name: "GF32", + secret: []byte{0, 0, 0, byte(uint8(33))}, + metadata: secrets.Metadata{ + Field: finitefield.GF32, + NumShares: 5, + Threshold: 3, + }, + shares: []secrets.Share{ + {Value: []byte{112, 207, 118, 46, 110, 212, 170, 28}, X: 1}, + {Value: []byte{48, 160, 197, 172, 38, 235, 145, 204}, X: 2}, + {Value: []byte{63, 115, 238, 144, 40, 68, 183, 71}, X: 3}, + {Value: []byte{29, 72, 240, 207, 114, 224, 26, 141}, X: 4}, + {Value: []byte{74, 31, 204, 116, 6, 189, 187, 136}, X: 5}, + }, + }, + { + name: "GF8", + secret: []byte("YELLOW_SUBMARINE"), + metadata: secrets.Metadata{ + Field: finitefield.GF8, + NumShares: 5, + Threshold: 3, + }, + shares: []secrets.Share{ + {Value: []byte{0xca, 0x6a, 0x5e, 0xe5, 0x13, 0x14, 0x08, 0x88, 0xf0, 0xab, 0x3a, 0x3b, 0xee, 0x7b, 0xd0, 0xdc}, X: 0xd3}, + {Value: []byte{0xf9, 0xa1, 0xf9, 0xb9, 0x00, 0xe4, 0x9c, 0x39, 0xcc, 0xce, 0x1f, 0xd9, 0xab, 0x3c, 0xe5, 0x72}, X: 0x97}, + {Value: []byte{0xb8, 0x03, 0x95, 0x32, 0x0f, 0x82, 0xa9, 0xf8, 0x1b, 0x42, 0x71, 0x20, 0xdb, 0x04, 0xa2, 0x51}, X: 0x53}, + {Value: []byte{0x7b, 0xc9, 0x47, 0x5e, 0xf8, 0x67, 0xff, 0x7c, 0xbc, 0x91, 0xdd, 0xa9, 0x8b, 0xa2, 0x7e, 0x84}, X: 0xff}, + {Value: []byte{0x0b, 0x98, 0x6c, 0x4a, 0x32, 0x23, 0x11, 0xfe, 0x62, 0x5e, 0xcc, 0x5a, 0x47, 0x2a, 0x4e, 0x15}, X: 0x5d}, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + split := secrets.Split{ + SecretLen: len(tc.secret), + Metadata: tc.metadata, + Shares: tc.shares, + } + recon, err := shamir.Reconstruct(split) + if err != nil { + t.Fatal(err) + } + if got := recon; !bytes.Equal(got, tc.secret) { + t.Errorf("got %v, want %v", hex.EncodeToString(got), hex.EncodeToString(tc.secret)) + } + }) + } +} + +func TestReconstructFromStaticSharesWithoutAllShares(t *testing.T) { + for _, tc := range []testCase{ + { + name: "GF32", + secret: []byte{0, 0, 0, byte(uint8(33))}, + metadata: secrets.Metadata{ + Field: finitefield.GF32, + NumShares: 5, + Threshold: 3, + }, + shares: []secrets.Share{ + {Value: []byte{112, 207, 118, 46, 110, 212, 170, 28}, X: 1}, + {Value: []byte{63, 115, 238, 144, 40, 68, 183, 71}, X: 3}, + {Value: []byte{74, 31, 204, 116, 6, 189, 187, 136}, X: 5}, + }, + }, + { + name: "GF8", + secret: []byte("YELLOW_SUBMARINE"), + metadata: secrets.Metadata{ + Field: finitefield.GF8, + NumShares: 5, + Threshold: 3, + }, + shares: []secrets.Share{ + {Value: []byte{0xca, 0x6a, 0x5e, 0xe5, 0x13, 0x14, 0x08, 0x88, 0xf0, 0xab, 0x3a, 0x3b, 0xee, 0x7b, 0xd0, 0xdc}, X: 0xd3}, + {Value: []byte{0xf9, 0xa1, 0xf9, 0xb9, 0x00, 0xe4, 0x9c, 0x39, 0xcc, 0xce, 0x1f, 0xd9, 0xab, 0x3c, 0xe5, 0x72}, X: 0x97}, + {Value: []byte{0x7b, 0xc9, 0x47, 0x5e, 0xf8, 0x67, 0xff, 0x7c, 0xbc, 0x91, 0xdd, 0xa9, 0x8b, 0xa2, 0x7e, 0x84}, X: 0xff}, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + split := secrets.Split{ + SecretLen: len(tc.secret), + Metadata: tc.metadata, + Shares: tc.shares, + } + recon, err := shamir.Reconstruct(split) + if err != nil { + t.Fatal(err) + } + if got := recon; !bytes.Equal(got, tc.secret) { + t.Errorf("got %v, want %v", hex.EncodeToString(got), hex.EncodeToString(tc.secret)) + } + }) + } +} diff --git a/client/jwt/BUILD b/client/jwt/BUILD index 0bcb73a..f6008cc 100644 --- a/client/jwt/BUILD +++ b/client/jwt/BUILD @@ -13,7 +13,6 @@ # limitations under the License. load("@io_bazel_rules_go//go:def.bzl", "go_library") -load("//tools/build_defs/go:go_library.bzl", "go_library") package( default_visibility = ["//:__subpackages__"], diff --git a/client/securesession/BUILD b/client/securesession/BUILD index fc57cee..f103ba9 100644 --- a/client/securesession/BUILD +++ b/client/securesession/BUILD @@ -13,8 +13,6 @@ # limitations under the License. load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") -load("//tools/build_defs/go:go_library.bzl", "go_library") -load("//tools/build_defs/go:go_test.bzl", "go_test") package( default_visibility = ["//:__subpackages__"], diff --git a/client/shares/BUILD b/client/shares/BUILD index 150a554..bc88bd2 100644 --- a/client/shares/BUILD +++ b/client/shares/BUILD @@ -13,8 +13,6 @@ # limitations under the License. load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") -load("//tools/build_defs/go:go_library.bzl", "go_library") -load("//tools/build_defs/go:go_test.bzl", "go_test") package( default_visibility = ["//:__subpackages__"], diff --git a/client/testutil/BUILD b/client/testutil/BUILD index 7801e4b..4483164 100644 --- a/client/testutil/BUILD +++ b/client/testutil/BUILD @@ -1,5 +1,4 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") -load("//tools/build_defs/go:go_library.bzl", "go_library") package( default_visibility = ["//:__subpackages__"], diff --git a/client/vpc/BUILD b/client/vpc/BUILD index 689831b..54876cf 100644 --- a/client/vpc/BUILD +++ b/client/vpc/BUILD @@ -13,8 +13,6 @@ # limitations under the License. load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") -load("//tools/build_defs/go:go_library.bzl", "go_library") -load("//tools/build_defs/go:go_test.bzl", "go_test") package( default_visibility = ["//:__subpackages__"], diff --git a/cmd/conformance/BUILD b/cmd/conformance/BUILD index 2ee749e..58e9877 100644 --- a/cmd/conformance/BUILD +++ b/cmd/conformance/BUILD @@ -1,5 +1,4 @@ load("@io_bazel_rules_go//go:def.bzl", "go_binary") -load("//tools/build_defs/go:go_binary.bzl", "go_binary") go_binary( name = "main", diff --git a/cmd/securesession/BUILD b/cmd/securesession/BUILD index fb8af2d..f433d0d 100644 --- a/cmd/securesession/BUILD +++ b/cmd/securesession/BUILD @@ -13,7 +13,6 @@ # limitations under the License. load("@io_bazel_rules_go//go:def.bzl", "go_binary") -load("//tools/build_defs/go:go_binary.bzl", "go_binary") licenses(["notice"]) diff --git a/cmd/server/BUILD b/cmd/server/BUILD index ce56c0b..440c80c 100644 --- a/cmd/server/BUILD +++ b/cmd/server/BUILD @@ -13,7 +13,6 @@ # limitations under the License. load("@io_bazel_rules_go//go:def.bzl", "go_binary") -load("//tools/build_defs/go:go_binary.bzl", "go_binary") licenses(["notice"]) diff --git a/cmd/stet/BUILD b/cmd/stet/BUILD index bf37af7..3528202 100644 --- a/cmd/stet/BUILD +++ b/cmd/stet/BUILD @@ -13,7 +13,6 @@ # limitations under the License. load("@io_bazel_rules_go//go:def.bzl", "go_binary") -load("//tools/build_defs/go:go_binary.bzl", "go_binary") licenses(["notice"]) diff --git a/constants/BUILD b/constants/BUILD index b69ab5c..3e911e8 100644 --- a/constants/BUILD +++ b/constants/BUILD @@ -13,7 +13,6 @@ # limitations under the License. load("@io_bazel_rules_go//go:def.bzl", "go_library") -load("//tools/build_defs/go:go_library.bzl", "go_library") licenses(["notice"]) diff --git a/proto/BUILD b/proto/BUILD index 9b3d9bd..a0373a7 100644 --- a/proto/BUILD +++ b/proto/BUILD @@ -16,7 +16,6 @@ load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") load("@rules_proto//proto:defs.bzl", "proto_library") -load("//tools/build_defs/go:go_proto_library.bzl", "go_proto_library") licenses(["notice"]) diff --git a/server/BUILD b/server/BUILD index 0495598..0ea718b 100644 --- a/server/BUILD +++ b/server/BUILD @@ -13,8 +13,6 @@ # limitations under the License. load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") -load("//tools/build_defs/go:go_library.bzl", "go_library") -load("//tools/build_defs/go:go_test.bzl", "go_test") licenses(["notice"]) diff --git a/transportshim/BUILD b/transportshim/BUILD index 07c9491..bcf3451 100644 --- a/transportshim/BUILD +++ b/transportshim/BUILD @@ -13,8 +13,6 @@ # limitations under the License. load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") -load("//tools/build_defs/go:go_library.bzl", "go_library") -load("//tools/build_defs/go:go_test.bzl", "go_test") licenses(["notice"])