mirror of
https://github.com/luxfi/tee.git
synced 2026-10-30 15:15:53 +00:00
268 lines
7.4 KiB
Go
268 lines
7.4 KiB
Go
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package rlwetee
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
|
|
coronaThreshold "github.com/luxfi/corona/threshold"
|
|
|
|
"github.com/luxfi/mpc/pkg/approval"
|
|
)
|
|
|
|
// Sign produces a Ring-LWE corona threshold signature on msg, gated
|
|
// by the supplied attestation Envelope.
|
|
//
|
|
// Flow:
|
|
//
|
|
// 1. approval gate → gate.Issue → env.Verify → env.VerifyEvidence →
|
|
// gate.Release (same as slhdsa-tee / mldsa-tee).
|
|
// 2. Inside the TEE: read the wrapped master trusted-dealer key,
|
|
// deterministically regenerate all n key shares + GroupKey via
|
|
// corona.threshold.GenerateKeys, then drive Round1 → Round2 →
|
|
// Finalize across all n parties in-process.
|
|
// 3. Zeroize the master key + per-party share state.
|
|
// 4. Return the wire-form corona Signature.
|
|
//
|
|
// Output is a valid corona threshold signature on (gk, msg) that
|
|
// any verifier holding the published GroupKey bytes can validate
|
|
// via corona.threshold.VerifyBytes.
|
|
func (s *Signer) Sign(ctx context.Context, env *Envelope, jobID [32]byte, msg []byte) ([]byte, *SignReceipt, error) {
|
|
if env == nil {
|
|
return nil, nil, ErrAttestationRequired
|
|
}
|
|
if len(msg) == 0 {
|
|
return nil, nil, fmt.Errorf("rlwe-tee: empty message")
|
|
}
|
|
|
|
sealed, err := s.auditedRelease(ctx, env, jobID, msg)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
key, err := s.hsmP.GetKey(ctx, s.cfg.WrappedSeedKeyID)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("%w: HSM GetKey: %v", ErrHSMUnreachable, err)
|
|
}
|
|
defer zeroize(key)
|
|
|
|
if len(key) != MasterKeySize {
|
|
return nil, nil, fmt.Errorf("%w: HSM-stored key length %d does not match MasterKeySize %d",
|
|
ErrCorruptWrappedSeed, len(key), MasterKeySize)
|
|
}
|
|
|
|
// Re-derive every key share + GroupKey deterministically from
|
|
// the master key. corona's GenerateKeys reads sign.KeySize bytes
|
|
// from randSource; bytes.NewReader pins the run reproducibly.
|
|
// The whole GenerateKeys + Round1 + Round2 + Finalize sequence
|
|
// holds coronaSerializer because corona's threshold globals
|
|
// (sign.K, sign.Threshold) and per-party internal state are
|
|
// written by every party in this sequence.
|
|
coronaSerializer.Lock()
|
|
shares, gk, err := coronaThreshold.GenerateKeysTrustedDealer(s.cfg.Threshold, s.cfg.Participants, bytes.NewReader(key))
|
|
if err != nil {
|
|
coronaSerializer.Unlock()
|
|
return nil, nil, fmt.Errorf("%w: GenerateKeys: %v", ErrCorruptWrappedSeed, err)
|
|
}
|
|
|
|
sigBytes, err := s.runCoronaThresholdSign(jobID, msg, shares, gk)
|
|
coronaSerializer.Unlock()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("%w: %v", ErrCoronaProtocol, err)
|
|
}
|
|
|
|
audit, err := s.auditSignature(ctx, jobID, msg, sealed.Epoch, env.RIM)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("rlwe-tee: audit signature: %w", err)
|
|
}
|
|
|
|
recv := &SignReceipt{
|
|
JobID: jobID,
|
|
Epoch: sealed.Epoch,
|
|
IssuedNonce: sealed.IssuedNonce,
|
|
EphemeralPub: sealed.EphemeralPub,
|
|
EvidenceKind: string(env.Kind),
|
|
EvidenceIssuer: evidenceIssuerString(env),
|
|
AuditSignature: audit,
|
|
}
|
|
return sigBytes, recv, nil
|
|
}
|
|
|
|
// runCoronaThresholdSign drives Round1 → Round2 → Finalize for all
|
|
// n signers in-process. The canonical signer set is [0..n) — corona
|
|
// requires distinct integer indices and we honour that. The PRF key
|
|
// is sha256(GroupKey bytes || jobID) so distinct sign calls bind
|
|
// distinct sessions.
|
|
func (s *Signer) runCoronaThresholdSign(jobID [32]byte, msg []byte, shares []*coronaThreshold.KeyShare, gk *coronaThreshold.GroupKey) ([]byte, error) {
|
|
n := len(shares)
|
|
signerIDs := make([]int, n)
|
|
for i := range signerIDs {
|
|
signerIDs[i] = i
|
|
}
|
|
|
|
gkBytes, err := gk.MarshalBinary()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("gk.MarshalBinary: %w", err)
|
|
}
|
|
prfDigest := sha256.New()
|
|
prfDigest.Write(gkBytes)
|
|
prfDigest.Write(jobID[:])
|
|
prfKey := prfDigest.Sum(nil)
|
|
|
|
sessionID := sessionIDFromJobID(jobID)
|
|
|
|
signers := make([]*coronaThreshold.Signer, n)
|
|
for i := 0; i < n; i++ {
|
|
signers[i] = coronaThreshold.NewSigner(shares[i])
|
|
}
|
|
|
|
r1Data := make(map[int]*coronaThreshold.Round1Data, n)
|
|
for _, signer := range signers {
|
|
r1, err := signer.Round1(sessionID, prfKey, signerIDs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("round1: %w", err)
|
|
}
|
|
r1Data[r1.PartyID] = r1
|
|
}
|
|
|
|
msgStr := string(msg)
|
|
r2Data := make(map[int]*coronaThreshold.Round2Data, n)
|
|
for _, signer := range signers {
|
|
r2, err := signer.Round2(sessionID, msgStr, prfKey, signerIDs, r1Data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("round2: %w", err)
|
|
}
|
|
r2Data[r2.PartyID] = r2
|
|
}
|
|
|
|
sig, err := signers[0].Finalize(r2Data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("finalize: %w", err)
|
|
}
|
|
|
|
if !coronaThreshold.Verify(gk, msgStr, sig) {
|
|
return nil, fmt.Errorf("self-verify failed (kernel bug)")
|
|
}
|
|
|
|
wire, err := sig.MarshalBinary()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sig.MarshalBinary: %w", err)
|
|
}
|
|
return wire, nil
|
|
}
|
|
|
|
// SignReceipt is the audit blob returned alongside the corona
|
|
// threshold signature.
|
|
type SignReceipt struct {
|
|
JobID [32]byte
|
|
Epoch uint64
|
|
IssuedNonce [32]byte
|
|
EphemeralPub [32]byte
|
|
EvidenceKind string
|
|
EvidenceIssuer string
|
|
AuditSignature []byte
|
|
}
|
|
|
|
func (s *Signer) auditSignature(ctx context.Context, jobID [32]byte, msg []byte, epoch uint64, rim [32]byte) ([]byte, error) {
|
|
h := sha256.New()
|
|
h.Write([]byte("LUX-RLWE-TEE-AUDIT-V1"))
|
|
h.Write([]byte{0x00})
|
|
h.Write(jobID[:])
|
|
h.Write(epochBytes(epoch))
|
|
h.Write(rim[:])
|
|
d := sha256.Sum256(msg)
|
|
h.Write(d[:])
|
|
auditDigest := h.Sum(nil)
|
|
return s.hsmP.Sign(ctx, s.cfg.KMSKeyID, auditDigest)
|
|
}
|
|
|
|
func epochBytes(e uint64) []byte {
|
|
return []byte{
|
|
byte(e >> 56), byte(e >> 48), byte(e >> 40), byte(e >> 32),
|
|
byte(e >> 24), byte(e >> 16), byte(e >> 8), byte(e),
|
|
}
|
|
}
|
|
|
|
func evidenceIssuerString(env *Envelope) string {
|
|
switch is := env.EvidenceIssuers(); len(is) {
|
|
case 0:
|
|
return ""
|
|
default:
|
|
return is[0]
|
|
}
|
|
}
|
|
|
|
// signIntent satisfies approval.CanonicalIntent.
|
|
type signIntent struct {
|
|
jobID [32]byte
|
|
msg []byte
|
|
env envelopeSummary
|
|
}
|
|
|
|
type envelopeSummary struct {
|
|
Kind string
|
|
RIM [32]byte
|
|
Hardware [32]byte
|
|
TEEPub [32]byte
|
|
}
|
|
|
|
func newSignIntent(jobID [32]byte, msg []byte, env *Envelope) *signIntent {
|
|
return &signIntent{
|
|
jobID: jobID,
|
|
msg: append([]byte(nil), msg...),
|
|
env: envelopeSummary{
|
|
Kind: string(env.Kind),
|
|
RIM: env.RIM,
|
|
Hardware: env.Hardware,
|
|
TEEPub: env.TEEPub,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (si *signIntent) Digest() [32]byte {
|
|
h := sha256.New()
|
|
h.Write([]byte("LUX-RLWE-TEE-INTENT-V1"))
|
|
h.Write([]byte{0x00})
|
|
h.Write(si.jobID[:])
|
|
mdigest := sha256.Sum256(si.msg)
|
|
h.Write(mdigest[:])
|
|
h.Write([]byte(si.env.Kind))
|
|
h.Write([]byte{0x00})
|
|
h.Write(si.env.RIM[:])
|
|
h.Write(si.env.Hardware[:])
|
|
h.Write(si.env.TEEPub[:])
|
|
var out [32]byte
|
|
copy(out[:], h.Sum(nil))
|
|
return out
|
|
}
|
|
|
|
func (si *signIntent) Bytes() []byte {
|
|
out := make([]byte, 0, 32+32+len(si.env.Kind)+1+32+32+32+32)
|
|
out = append(out, []byte("LUX-RLWE-TEE-INTENT-V1")...)
|
|
out = append(out, 0x00)
|
|
out = append(out, si.jobID[:]...)
|
|
mdigest := sha256.Sum256(si.msg)
|
|
out = append(out, mdigest[:]...)
|
|
out = append(out, []byte(si.env.Kind)...)
|
|
out = append(out, 0x00)
|
|
out = append(out, si.env.RIM[:]...)
|
|
out = append(out, si.env.Hardware[:]...)
|
|
out = append(out, si.env.TEEPub[:]...)
|
|
return out
|
|
}
|
|
|
|
var _ approval.CanonicalIntent = (*signIntent)(nil)
|
|
|
|
// FreshJobID returns 32 bytes of crypto/rand.
|
|
func FreshJobID() ([32]byte, error) {
|
|
var out [32]byte
|
|
if _, err := rand.Read(out[:]); err != nil {
|
|
return out, err
|
|
}
|
|
return out, nil
|
|
}
|