diff --git a/.changelog/4926.feature.md b/.changelog/4926.feature.md new file mode 100644 index 00000000000..8a3f777b418 --- /dev/null +++ b/.changelog/4926.feature.md @@ -0,0 +1 @@ +Bind TEE attestations to nodes and enforce freshness diff --git a/go/common/node/node.go b/go/common/node/node.go index 7e8b18dbf62..9fae26b34a8 100644 --- a/go/common/node/node.go +++ b/go/common/node/node.go @@ -35,8 +35,19 @@ var ( // identity doesn't match the required values. ErrBadEnclaveIdentity = errors.New("node: bad TEE enclave identity") + // ErrBadAttestationSignature is the error returned when the TEE attestation + // signature fails verification. + ErrBadAttestationSignature = errors.New("node: bad TEE attestation signature") + + // ErrAttestationNotFresh is the error returned when the TEE attestation is + // not fresh enough. + ErrAttestationNotFresh = errors.New("node: TEE attestation not fresh enough") + teeHashContext = []byte("oasis-core/node: TEE RAK binding") + // AttestationSignatureContext is the signature context used for TEE attestation signatures. + AttestationSignatureContext = signature.NewContext("oasis-core/node: TEE attestation signature") + _ prettyprint.PrettyPrinter = (*MultiSignedNode)(nil) ) @@ -480,8 +491,8 @@ type CapabilityTEE struct { Attestation []byte `json:"attestation"` } -// RAKHash computes the expected AVR report hash bound to a given public RAK. -func RAKHash(rak signature.PublicKey) hash.Hash { +// HashRAK computes the expected report data hash bound to a given public RAK. +func HashRAK(rak signature.PublicKey) hash.Hash { hData := make([]byte, 0, len(teeHashContext)+signature.PublicKeySize) hData = append(hData, teeHashContext...) hData = append(hData, rak[:]...) @@ -489,9 +500,7 @@ func RAKHash(rak signature.PublicKey) hash.Hash { } // Verify verifies the node's TEE capabilities, at the provided timestamp. -func (c *CapabilityTEE) Verify(teeCfg *TEEFeatures, ts time.Time, constraints []byte) error { - rakHash := RAKHash(c.RAK) - +func (c *CapabilityTEE) Verify(teeCfg *TEEFeatures, ts time.Time, height uint64, constraints []byte, nodeID signature.PublicKey) error { switch c.Hardware { case TEEHardwareIntelSGX: // Parse SGX remote attestation. @@ -512,34 +521,7 @@ func (c *CapabilityTEE) Verify(teeCfg *TEEFeatures, ts time.Time, constraints [] return fmt.Errorf("node: malformed SGX constraints: %w", err) } - // Use default from consensus parameters if policy is unset. - if teeCfg != nil { - sc.Policy = teeCfg.SGX.ApplyDefaultPolicy(sc.Policy) - } - - // Verify the quote. - verifiedQuote, err := sa.Quote.Verify(sc.Policy, ts) - if err != nil { - return err - } - - // Ensure that the MRENCLAVE/MRSIGNER match what is specified - // in the TEE-specific constraints field. - if !sc.ContainsEnclave(verifiedQuote.Identity) { - return ErrBadEnclaveIdentity - } - - // Ensure that the report data includes the hash of the node's RAK. - var avrRAKHash hash.Hash - _ = avrRAKHash.UnmarshalBinary(verifiedQuote.ReportData[:hash.Size]) - if !rakHash.Equal(&avrRAKHash) { - return ErrRAKHashMismatch - } - - // The last 32 bytes of the quote ReportData are deliberately - // ignored. - - return nil + return sa.Verify(teeCfg, ts, height, &sc, c.RAK, nodeID) default: return ErrInvalidTEEHardware } diff --git a/go/common/node/sgx.go b/go/common/node/sgx.go index 33e7b929f37..ec7cc04ef02 100644 --- a/go/common/node/sgx.go +++ b/go/common/node/sgx.go @@ -1,9 +1,14 @@ package node import ( + "encoding/binary" "fmt" + "time" "github.com/oasisprotocol/oasis-core/go/common/cbor" + "github.com/oasisprotocol/oasis-core/go/common/crypto/hash" + "github.com/oasisprotocol/oasis-core/go/common/crypto/signature" + "github.com/oasisprotocol/oasis-core/go/common/crypto/tuplehash" "github.com/oasisprotocol/oasis-core/go/common/sgx" "github.com/oasisprotocol/oasis-core/go/common/sgx/ias" "github.com/oasisprotocol/oasis-core/go/common/sgx/quote" @@ -26,6 +31,9 @@ type SGXConstraints struct { // Policy is the quote policy. Policy *quote.Policy `json:"policy,omitempty"` + + // MaxAttestationAge is the maximum attestation age (in blocks). + MaxAttestationAge uint64 `json:"max_attestation_age,omitempty"` } // sgxConstraintsV0 are the version 0 Intel SGX TEE constraints which only supports IAS. @@ -95,6 +103,11 @@ func (sc *SGXConstraints) ValidateBasic(cfg *TEEFeatures) error { if !cfg.SGX.PCS && sc.V != 0 { return fmt.Errorf("unsupported SGX constraints version: %d", sc.V) } + // Sanity check version (should never fail as deserialization already checks this). + if sc.V > LatestSGXConstraintsVersion { + return fmt.Errorf("unsupported SGX constraints version: %d", sc.V) + } + return nil } @@ -121,6 +134,12 @@ type SGXAttestation struct { // Quote is an Intel SGX quote. Quote quote.Quote `json:"quote"` + + // Height is the runtime's view of the consensus layer height at the time of attestation. + Height uint64 `json:"height"` + + // Signature is the signature of the attestation by the enclave (RAK). + Signature signature.RawSignature `json:"signature"` } // UnmarshalCBOR is a custom deserializer that handles different structure versions. @@ -175,5 +194,91 @@ func (sa *SGXAttestation) ValidateBasic(cfg *TEEFeatures) error { if !cfg.SGX.PCS && sa.V != 0 { return fmt.Errorf("unsupported SGX attestation version: %d", sa.V) } + // Sanity check version (should never fail as deserialization already checks this). + if sa.V > LatestSGXAttestationVersion { + return fmt.Errorf("unsupported SGX attestation version: %d", sa.V) + } + + return nil +} + +// Verify verifies the SGX attestation. +func (sa *SGXAttestation) Verify( + cfg *TEEFeatures, + ts time.Time, + height uint64, + sc *SGXConstraints, + rak signature.PublicKey, + nodeID signature.PublicKey, +) error { + if cfg == nil { + cfg = &emptyFeatures + } + + // Use defaults from consensus parameters. + cfg.SGX.ApplyDefaultConstraints(sc) + + // Verify the quote. + verifiedQuote, err := sa.Quote.Verify(sc.Policy, ts) + if err != nil { + return err + } + + // Ensure that the MRENCLAVE/MRSIGNER match what is specified + // in the TEE-specific constraints field. + if !sc.ContainsEnclave(verifiedQuote.Identity) { + return ErrBadEnclaveIdentity + } + + // Ensure that the report data includes the hash of the node's RAK. + var reportDataRAKHash hash.Hash + _ = reportDataRAKHash.UnmarshalBinary(verifiedQuote.ReportData[:hash.Size]) + rakHash := HashRAK(rak) + if !rakHash.Equal(&reportDataRAKHash) { + return ErrRAKHashMismatch + } + + // The last 32 bytes of the quote ReportData are deliberately + // ignored. + + if cfg.SGX.SignedAttestations { + // In case the signed attestation feature is enabled, verify the signature. + return sa.verifyAttestationSignature(sc, rak, verifiedQuote.ReportData, nodeID, height) + } + + return nil +} + +func (sa *SGXAttestation) verifyAttestationSignature( + sc *SGXConstraints, + rak signature.PublicKey, + reportData []byte, + nodeID signature.PublicKey, + height uint64, +) error { + h := HashAttestation(reportData, nodeID, sa.Height) + if !rak.Verify(AttestationSignatureContext, h, sa.Signature[:]) { + return ErrBadAttestationSignature + } + + // Check height is relatively recent. + if sa.Height > height || height-sa.Height > sc.MaxAttestationAge { + return ErrAttestationNotFresh + } + return nil } + +// HashAttestations hashes the required data that needs to be signed by RAK producing the +// attestation signature. +func HashAttestation(reportData []byte, nodeID signature.PublicKey, height uint64) []byte { + // id := TupleHash[AttestationSignatureContext](reportData, nodeID, height) + h := tuplehash.New256(32, []byte(AttestationSignatureContext)) + _, _ = h.Write(reportData) + rawNodeID, _ := nodeID.MarshalBinary() + _, _ = h.Write(rawNodeID) + var rawHeight [8]byte + binary.LittleEndian.PutUint64(rawHeight[:], height) + _, _ = h.Write(rawHeight[:]) + return h.Sum(nil) +} diff --git a/go/common/node/sgx_test.go b/go/common/node/sgx_test.go index f19e2a0d815..19a9f072003 100644 --- a/go/common/node/sgx_test.go +++ b/go/common/node/sgx_test.go @@ -1,12 +1,14 @@ package node import ( + "encoding/hex" "io/ioutil" "testing" "github.com/stretchr/testify/require" "github.com/oasisprotocol/oasis-core/go/common/cbor" + "github.com/oasisprotocol/oasis-core/go/common/crypto/signature" "github.com/oasisprotocol/oasis-core/go/common/sgx" "github.com/oasisprotocol/oasis-core/go/common/sgx/ias" "github.com/oasisprotocol/oasis-core/go/common/sgx/pcs" @@ -111,6 +113,17 @@ func TestSGXAttestationV1(t *testing.T) { require.EqualValues(enc, raw, "serialization should round-trip") } +func TestHashAttestation(t *testing.T) { + require := require.New(t) + + // func HashAttestation(reportData []byte, nodeID signature.PublicKey, height uint64) []byte + var nodeID signature.PublicKey + _ = nodeID.UnmarshalHex("47aadd91516ac548decdb436fde957992610facc09ba2f850da0fe1b2be96119") + h := HashAttestation([]byte("foo bar"), nodeID, 42) + hHex := hex.EncodeToString(h) + require.EqualValues("0f01a5084bbf432427873cbce5f8c3bff76bc22b9d1e0674b852e43698abb195", hHex) +} + func FuzzSGXConstraints(f *testing.F) { // Add some V0 constraints. raw, err := ioutil.ReadFile("testdata/sgx_constraints_v0.bin") diff --git a/go/common/node/tee.go b/go/common/node/tee.go index dc24beeba6f..04fb2acdead 100644 --- a/go/common/node/tee.go +++ b/go/common/node/tee.go @@ -18,27 +18,34 @@ type TEEFeaturesSGX struct { // remote attestation is supported for Intel SGX-based TEEs. PCS bool `json:"pcs"` + // SignedAttestations is a feature flag specifying whether attestations need to include an + // additional signature binding it to a specific node. + SignedAttestations bool `json:"signed_attestations,omitempty"` + // DefaultPolicy is the default quote policy. DefaultPolicy *quote.Policy `json:"default_policy,omitempty"` + + // DefaultMaxAttestationAge is the default maximum attestation age (in blocks). + DefaultMaxAttestationAge uint64 `json:"max_attestation_age,omitempty"` } -// ApplyDefaultPolicy applies configured quote policy defaults to the given policy, returning the -// new policy with defaults applied. -// -// In case no quote policy defaults are configured returns the policy unchanged. -func (fs *TEEFeaturesSGX) ApplyDefaultPolicy(policy *quote.Policy) *quote.Policy { - if fs.DefaultPolicy == nil { - return policy +// ApplyDefaultPolicy applies configured SGX constraint defaults to the given structure. +func (fs *TEEFeaturesSGX) ApplyDefaultConstraints(sc *SGXConstraints) { + // Default policy. + if fs.DefaultPolicy != nil { + if sc.Policy == nil { + sc.Policy = "e.Policy{} + } + if sc.Policy.IAS == nil { + sc.Policy.IAS = fs.DefaultPolicy.IAS + } + if sc.Policy.PCS == nil && fs.PCS { + sc.Policy.PCS = fs.DefaultPolicy.PCS + } } - if policy == nil { - policy = "e.Policy{} - } - if policy.IAS == nil { - policy.IAS = fs.DefaultPolicy.IAS - } - if policy.PCS == nil && fs.PCS { - policy.PCS = fs.DefaultPolicy.PCS + // Default maximum attestation age. + if sc.MaxAttestationAge == 0 { + sc.MaxAttestationAge = fs.DefaultMaxAttestationAge } - return policy } diff --git a/go/common/node/tee_test.go b/go/common/node/tee_test.go index 8edf8eb9882..1af02cee586 100644 --- a/go/common/node/tee_test.go +++ b/go/common/node/tee_test.go @@ -10,15 +10,16 @@ import ( "github.com/oasisprotocol/oasis-core/go/common/sgx/quote" ) -func TestTEEFeaturesSGXApplyDefaultPolicy(t *testing.T) { +func TestTEEFeaturesSGXApplyDefaultConstraints(t *testing.T) { require := require.New(t) tf := TEEFeaturesSGX{ PCS: true, } + sc := SGXConstraints{} - policy := tf.ApplyDefaultPolicy(nil) - require.Nil(policy, "policy should remain nil when no default policy is configured") + tf.ApplyDefaultConstraints(&sc) + require.Nil(sc.Policy, "policy should remain nil when no default policy is configured") defaultIasPolicy := &ias.QuotePolicy{} defaultPcsPolicy := &pcs.QuotePolicy{ @@ -33,22 +34,27 @@ func TestTEEFeaturesSGXApplyDefaultPolicy(t *testing.T) { PCS: defaultPcsPolicy, }, } + sc = SGXConstraints{} - policy = tf.ApplyDefaultPolicy(nil) - require.NotNil(policy, "a default policy should be used") - require.EqualValues(defaultIasPolicy, policy.IAS) - require.EqualValues(defaultPcsPolicy, policy.PCS) + tf.ApplyDefaultConstraints(&sc) + require.NotNil(sc.Policy, "a default policy should be used") + require.EqualValues(defaultIasPolicy, sc.Policy.IAS) + require.EqualValues(defaultPcsPolicy, sc.Policy.PCS) existingPcsPolicy := &pcs.QuotePolicy{ TCBValidityPeriod: 20, MinTCBEvaluationDataNumber: 1, } - policy = tf.ApplyDefaultPolicy("e.Policy{ - PCS: existingPcsPolicy, - }) - require.EqualValues(defaultIasPolicy, policy.IAS) - require.EqualValues(existingPcsPolicy, policy.PCS) + sc = SGXConstraints{ + Policy: "e.Policy{ + PCS: existingPcsPolicy, + }, + } + + tf.ApplyDefaultConstraints(&sc) + require.EqualValues(defaultIasPolicy, sc.Policy.IAS) + require.EqualValues(existingPcsPolicy, sc.Policy.PCS) tf = TEEFeaturesSGX{ PCS: false, @@ -57,9 +63,10 @@ func TestTEEFeaturesSGXApplyDefaultPolicy(t *testing.T) { PCS: defaultPcsPolicy, }, } + sc = SGXConstraints{} - policy = tf.ApplyDefaultPolicy(nil) - require.NotNil(policy, "a default policy should be used") - require.EqualValues(defaultIasPolicy, policy.IAS) - require.Nil(policy.PCS, "PCS policy should remain unset when PCS is disabled") + tf.ApplyDefaultConstraints(&sc) + require.NotNil(sc.Policy, "a default policy should be used") + require.EqualValues(defaultIasPolicy, sc.Policy.IAS) + require.Nil(sc.Policy.PCS, "PCS policy should remain unset when PCS is disabled") } diff --git a/go/common/node/testdata/sgx_attestation_v1.bin b/go/common/node/testdata/sgx_attestation_v1.bin index c3e8058bc21..3a8f660e759 100644 Binary files a/go/common/node/testdata/sgx_attestation_v1.bin and b/go/common/node/testdata/sgx_attestation_v1.bin differ diff --git a/go/consensus/tendermint/apps/keymanager/keymanager.go b/go/consensus/tendermint/apps/keymanager/keymanager.go index 9d14ee4187f..947255067f6 100644 --- a/go/consensus/tendermint/apps/keymanager/keymanager.go +++ b/go/consensus/tendermint/apps/keymanager/keymanager.go @@ -244,7 +244,7 @@ func (app *keymanagerApplication) generateStatus( continue } - initResponse, err := api.VerifyExtraInfo(ctx.Logger(), kmrt, nodeRt, ctx.Now(), params) + initResponse, err := api.VerifyExtraInfo(ctx.Logger(), n.ID, kmrt, nodeRt, ctx.Now(), uint64(ctx.BlockHeight()), params) if err != nil { ctx.Logger().Error("failed to validate ExtraInfo", "err", err, diff --git a/go/consensus/tendermint/apps/registry/transactions.go b/go/consensus/tendermint/apps/registry/transactions.go index 753e3468d40..f0a8e2b3b51 100644 --- a/go/consensus/tendermint/apps/registry/transactions.go +++ b/go/consensus/tendermint/apps/registry/transactions.go @@ -205,6 +205,7 @@ func (app *registryApplication) registerNode( // nolint: gocyclo sigNode, untrustedEntity, ctx.Now(), + uint64(ctx.BlockHeight()), ctx.IsInitChain(), false, epoch, diff --git a/go/consensus/tendermint/apps/scheduler/scheduler.go b/go/consensus/tendermint/apps/scheduler/scheduler.go index 64dc519c45f..3ef84acf718 100644 --- a/go/consensus/tendermint/apps/scheduler/scheduler.go +++ b/go/consensus/tendermint/apps/scheduler/scheduler.go @@ -364,7 +364,13 @@ func (app *schedulerApplication) isSuitableExecutorWorker( if nrt.Capabilities.TEE.Hardware != rt.TEEHardware { return false } - if err := nrt.Capabilities.TEE.Verify(registryParams.TEEFeatures, ctx.Now(), activeDeployment.TEE); err != nil { + if err := nrt.Capabilities.TEE.Verify( + registryParams.TEEFeatures, + ctx.Now(), + uint64(ctx.BlockHeight()), + activeDeployment.TEE, + n.node.ID, + ); err != nil { ctx.Logger().Warn("failed to verify node TEE attestaion", "err", err, "node_id", n.node.ID, diff --git a/go/consensus/tendermint/apps/supplementarysanity/checks.go b/go/consensus/tendermint/apps/supplementarysanity/checks.go index 910a0c4ba4a..a49167e815c 100644 --- a/go/consensus/tendermint/apps/supplementarysanity/checks.go +++ b/go/consensus/tendermint/apps/supplementarysanity/checks.go @@ -67,7 +67,7 @@ func checkRegistry(ctx *abciAPI.Context, now beacon.EpochTime) error { if err != nil { return fmt.Errorf("SignedNodes: %w", err) } - _, err = registry.SanityCheckNodes(logger, params, signedNodes, seenEntities, runtimeLookup, false, now, ctx.Now()) + _, err = registry.SanityCheckNodes(logger, params, signedNodes, seenEntities, runtimeLookup, false, now, ctx.Now(), uint64(ctx.BlockHeight())) if err != nil { return fmt.Errorf("SanityCheckNodes: %w", err) } diff --git a/go/genesis/api/sanity_check.go b/go/genesis/api/sanity_check.go index ec2071a8d6a..904fc883858 100644 --- a/go/genesis/api/sanity_check.go +++ b/go/genesis/api/sanity_check.go @@ -30,7 +30,7 @@ func (d *Document) SanityCheck() error { } epoch := d.Beacon.Base // Note: d.Height has no easy connection to the epoch. - if err := d.Registry.SanityCheck(d.Time, epoch, d.Staking.Ledger, d.Staking.Parameters.Thresholds, pkBlacklist); err != nil { + if err := d.Registry.SanityCheck(d.Time, uint64(d.Height), epoch, d.Staking.Ledger, d.Staking.Parameters.Thresholds, pkBlacklist); err != nil { return err } if err := d.RootHash.SanityCheck(); err != nil { diff --git a/go/keymanager/api/api.go b/go/keymanager/api/api.go index d051b545714..d250d2dcdf0 100644 --- a/go/keymanager/api/api.go +++ b/go/keymanager/api/api.go @@ -121,9 +121,11 @@ func (r *SignedInitResponse) Verify(pk signature.PublicKey) error { // blob for a key manager. func VerifyExtraInfo( logger *logging.Logger, + nodeID signature.PublicKey, rt *registry.Runtime, nodeRt *node.Runtime, ts time.Time, + height uint64, params *registry.ConsensusParameters, ) (*InitResponse, error) { var ( @@ -139,7 +141,7 @@ func VerifyExtraInfo( } if hw != rt.TEEHardware { return nil, fmt.Errorf("keymanager: TEEHardware mismatch") - } else if err := registry.VerifyNodeRuntimeEnclaveIDs(logger, nodeRt, rt, params.TEEFeatures, ts); err != nil { + } else if err := registry.VerifyNodeRuntimeEnclaveIDs(logger, nodeID, nodeRt, rt, params.TEEFeatures, ts, height); err != nil { return nil, err } if nodeRt.ExtraInfo == nil { diff --git a/go/oasis-node/cmd/debug/byzantine/node.go b/go/oasis-node/cmd/debug/byzantine/node.go index fbdefbf93f1..9b23f7fb66e 100644 --- a/go/oasis-node/cmd/debug/byzantine/node.go +++ b/go/oasis-node/cmd/debug/byzantine/node.go @@ -152,7 +152,7 @@ func initializeAndRegisterByzantineNode( // Register node. if viper.GetBool(CfgFakeSGX) { - if b.rak, b.capabilities, err = initFakeCapabilitiesSGX(); err != nil { + if b.rak, b.capabilities, err = initFakeCapabilitiesSGX(b.identity.NodeSigner.Public()); err != nil { return nil, fmt.Errorf("initFakeCapabilitiesSGX: %w", err) } } diff --git a/go/oasis-node/cmd/debug/byzantine/steps.go b/go/oasis-node/cmd/debug/byzantine/steps.go index fd0cbf83804..896d77f34bf 100644 --- a/go/oasis-node/cmd/debug/byzantine/steps.go +++ b/go/oasis-node/cmd/debug/byzantine/steps.go @@ -16,6 +16,7 @@ import ( "github.com/oasisprotocol/oasis-core/go/common/node" "github.com/oasisprotocol/oasis-core/go/common/sgx" "github.com/oasisprotocol/oasis-core/go/common/sgx/ias" + sgxQuote "github.com/oasisprotocol/oasis-core/go/common/sgx/quote" ) func initDefaultIdentity(dataDir string) (*identity.Identity, error) { @@ -35,13 +36,13 @@ func initDefaultIdentity(dataDir string) (*identity.Identity, error) { // To also populate EnclaveIdentity in the Quote from // runtime.version.fake_enclave flag, this function requires viper to be // initialized and the flag registered first. -func initFakeCapabilitiesSGX() (signature.Signer, *node.Capabilities, error) { +func initFakeCapabilitiesSGX(nodeID signature.PublicKey) (signature.Signer, *node.Capabilities, error) { // Get fake RAK. fr, err := memorySigner.NewFactory().Generate(signature.SignerUnknown, rand.Reader) if err != nil { return nil, nil, err } - rakHash := node.RAKHash(fr.Public()) + rakHash := node.HashRAK(fr.Public()) // Read EnclaveIdentity from cmd. // Requires viper to be initialized before that! @@ -81,14 +82,28 @@ func initFakeCapabilitiesSGX() (signature.Signer, *node.Capabilities, error) { ISVEnclaveQuoteBody: quoteBinary, }) + // Generate attestation signature. + h := node.HashAttestation(quote.Report.ReportData[:], nodeID, 1) + attestationSig, err := signature.Sign(fr, node.AttestationSignatureContext, h) + if err != nil { + return nil, nil, err + } + // Populate TEE attribute. fc := node.Capabilities{} fc.TEE = &node.CapabilityTEE{ Hardware: node.TEEHardwareIntelSGX, - Attestation: cbor.Marshal(ias.AVRBundle{ - Body: body, - // Everything we do is simulated, and we wouldn't be able to get a real signed AVR. - }), + Attestation: cbor.Marshal(cbor.Marshal(node.SGXAttestation{ + Versioned: cbor.NewVersioned(node.LatestSGXAttestationVersion), + Quote: sgxQuote.Quote{ + IAS: &ias.AVRBundle{ + Body: body, + // Everything we do is simulated, and we wouldn't be able to get a real signed AVR. + }, + }, + Height: 1, + Signature: attestationSig.Signature, + })), RAK: fr.Public(), } diff --git a/go/oasis-node/cmd/debug/fixgenesis/fixgenesis.go b/go/oasis-node/cmd/debug/fixgenesis/fixgenesis.go index 6daccf7536f..00c4ffff570 100644 --- a/go/oasis-node/cmd/debug/fixgenesis/fixgenesis.go +++ b/go/oasis-node/cmd/debug/fixgenesis/fixgenesis.go @@ -704,7 +704,7 @@ NodeLoop: continue NodeLoop } if rt.Capabilities.TEE != nil { - if err := registry.VerifyNodeRuntimeEnclaveIDs(logger, rt, knownRt, newDoc.Registry.Parameters.TEEFeatures, oldDoc.Time); err != nil { + if err := registry.VerifyNodeRuntimeEnclaveIDs(logger, node.ID, rt, knownRt, newDoc.Registry.Parameters.TEEFeatures, oldDoc.Time, uint64(oldDoc.Height)); err != nil { logger.Warn("removing node with invalid TEE capability", "err", err, "node_id", node.ID, diff --git a/go/oasis-node/cmd/genesis/genesis.go b/go/oasis-node/cmd/genesis/genesis.go index 8ce723a7e50..18ebb07a1fb 100644 --- a/go/oasis-node/cmd/genesis/genesis.go +++ b/go/oasis-node/cmd/genesis/genesis.go @@ -66,6 +66,7 @@ const ( cfgRegistryEnableRuntimeGovernanceModels = "registry.enable_runtime_governance_models" CfgRegistryTEEFeaturesSGXPCS = "registry.tee_features.sgx.pcs" CfgRegistryTEEFeaturesFreshnessProofs = "registry.tee_features.freshness_proofs" + CfgRegistryTEEFeaturesSignedAttestations = "registry.tee_features.signed_attestations" // Scheduler config flags. cfgSchedulerMinValidators = "scheduler.min_validators" @@ -359,6 +360,14 @@ func AppendRegistryState(doc *genesis.Document, entities, runtimes, nodes []stri regSt.Parameters.TEEFeatures.FreshnessProofs = true } + if viper.GetBool(CfgRegistryTEEFeaturesSignedAttestations) { + if regSt.Parameters.TEEFeatures == nil { + regSt.Parameters.TEEFeatures = &node.TEEFeatures{} + } + regSt.Parameters.TEEFeatures.SGX.SignedAttestations = true + regSt.Parameters.TEEFeatures.SGX.DefaultMaxAttestationAge = 1200 // ~2 hours at 6 sec per block. + } + for _, gmStr := range viper.GetStringSlice(cfgRegistryEnableRuntimeGovernanceModels) { var gm registry.RuntimeGovernanceModel if err := gm.UnmarshalText([]byte(strings.ToLower(gmStr))); err != nil { @@ -784,6 +793,7 @@ func init() { initGenesisFlags.StringSlice(cfgRegistryEnableRuntimeGovernanceModels, []string{"entity"}, "set of enabled runtime governance models") initGenesisFlags.Bool(CfgRegistryTEEFeaturesSGXPCS, true, "enable PCS support for SGX TEEs") initGenesisFlags.Bool(CfgRegistryTEEFeaturesFreshnessProofs, true, "enable freshness proofs") + initGenesisFlags.Bool(CfgRegistryTEEFeaturesSignedAttestations, true, "enable signed attestations") _ = initGenesisFlags.MarkHidden(cfgRegistryDebugAllowUnroutableAddresses) _ = initGenesisFlags.MarkHidden(CfgRegistryDebugAllowTestRuntimes) _ = initGenesisFlags.MarkHidden(cfgRegistryDebugBypassStake) diff --git a/go/oasis-test-runner/scenario/e2e/upgrade.go b/go/oasis-test-runner/scenario/e2e/upgrade.go index e42084b640d..6269905358d 100644 --- a/go/oasis-test-runner/scenario/e2e/upgrade.go +++ b/go/oasis-test-runner/scenario/e2e/upgrade.go @@ -101,6 +101,12 @@ func (n *upgradeTeePcsChecker) PostUpgradeFn(ctx context.Context, ctrl *oasis.Co if !registryParams.TEEFeatures.FreshnessProofs { return fmt.Errorf("freshness proofs TEE feature is disabled") } + if !registryParams.TEEFeatures.SGX.SignedAttestations { + return fmt.Errorf("signed attestations TEE feature is disabled") + } + if registryParams.TEEFeatures.SGX.DefaultMaxAttestationAge != 1200 { + return fmt.Errorf("default max attestation age is not set correctly") + } if registryParams.GasCosts[registry.GasOpProveFreshness] != registry.DefaultGasCosts[registry.GasOpProveFreshness] { return fmt.Errorf("default gas cost for freshness proofs is not set") } diff --git a/go/registry/api/api.go b/go/registry/api/api.go index c3eb7cf8c3b..8d23d80b1b0 100644 --- a/go/registry/api/api.go +++ b/go/registry/api/api.go @@ -476,6 +476,7 @@ func VerifyRegisterNodeArgs( // nolint: gocyclo sigNode *node.MultiSignedNode, entity *entity.Entity, now time.Time, + height uint64, isGenesis bool, isSanityCheck bool, epoch beacon.EpochTime, @@ -597,7 +598,7 @@ func VerifyRegisterNodeArgs( // nolint: gocyclo // If the node indicates TEE support for any of it's runtimes, // validate the attestation evidence. - if err := VerifyNodeRuntimeEnclaveIDs(logger, rt, regRt, params.TEEFeatures, now); err != nil && !isSanityCheck { + if err := VerifyNodeRuntimeEnclaveIDs(logger, n.ID, rt, regRt, params.TEEFeatures, now, height); err != nil && !isSanityCheck { return nil, nil, err } @@ -783,10 +784,12 @@ func VerifyRegisterNodeArgs( // nolint: gocyclo // VerifyNodeRuntimeEnclaveIDs verifies TEE-specific attributes of the node's runtime. func VerifyNodeRuntimeEnclaveIDs( logger *logging.Logger, + nodeID signature.PublicKey, rt *node.Runtime, regRt *Runtime, teeCfg *node.TEEFeatures, ts time.Time, + height uint64, ) error { // If no TEE available, do nothing. if rt.Capabilities.TEE == nil { @@ -810,7 +813,7 @@ func VerifyNodeRuntimeEnclaveIDs( continue } - if err := rt.Capabilities.TEE.Verify(teeCfg, ts, rtVersionInfo.TEE); err != nil { + if err := rt.Capabilities.TEE.Verify(teeCfg, ts, height, rtVersionInfo.TEE, nodeID); err != nil { logger.Error("VerifyNodeRuntimeEnclaveIDs: failed to validate attestation", "runtime_id", rt.ID, "ts", ts, diff --git a/go/registry/api/sanity_check.go b/go/registry/api/sanity_check.go index 70d9d35f54e..d8dff5c4da1 100644 --- a/go/registry/api/sanity_check.go +++ b/go/registry/api/sanity_check.go @@ -19,6 +19,7 @@ import ( // SanityCheck does basic sanity checking on the genesis state. func (g *Genesis) SanityCheck( now time.Time, + height uint64, baseEpoch beacon.EpochTime, stakeLedger map[staking.Address]*staking.Account, stakeThresholds map[staking.ThresholdKind]quantity.Quantity, @@ -48,7 +49,7 @@ func (g *Genesis) SanityCheck( } // Check nodes. - nodeLookup, err := SanityCheckNodes(logger, &g.Parameters, g.Nodes, seenEntities, runtimesLookup, true, baseEpoch, now) + nodeLookup, err := SanityCheckNodes(logger, &g.Parameters, g.Nodes, seenEntities, runtimesLookup, true, baseEpoch, now, height) if err != nil { return err } @@ -165,6 +166,7 @@ func SanityCheckNodes( isGenesis bool, epoch beacon.EpochTime, now time.Time, + height uint64, ) (NodeLookup, error) { // nolint: gocyclo nodeLookup := &sanityCheckNodeLookup{ @@ -193,6 +195,7 @@ func SanityCheckNodes( signedNode, entity, now, + height, isGenesis, true, epoch, diff --git a/go/runtime/host/protocol/types.go b/go/runtime/host/protocol/types.go index e1f04d07455..d85b2f63f5e 100644 --- a/go/runtime/host/protocol/types.go +++ b/go/runtime/host/protocol/types.go @@ -78,7 +78,7 @@ type Body struct { RuntimeCapabilityTEERakAvrRequest *RuntimeCapabilityTEERakAvrRequest `json:",omitempty"` RuntimeCapabilityTEERakAvrResponse *Empty `json:",omitempty"` RuntimeCapabilityTEERakQuoteRequest *RuntimeCapabilityTEERakQuoteRequest `json:",omitempty"` - RuntimeCapabilityTEERakQuoteResponse *Empty `json:",omitempty"` + RuntimeCapabilityTEERakQuoteResponse *RuntimeCapabilityTEERakQuoteResponse `json:",omitempty"` RuntimeRPCCallRequest *RuntimeRPCCallRequest `json:",omitempty"` RuntimeRPCCallResponse *RuntimeRPCCallResponse `json:",omitempty"` RuntimeLocalRPCCallRequest *RuntimeLocalRPCCallRequest `json:",omitempty"` @@ -115,6 +115,8 @@ type Body struct { HostFetchGenesisHeightResponse *HostFetchGenesisHeightResponse `json:",omitempty"` HostProveFreshnessRequest *HostProveFreshnessRequest `json:",omitempty"` HostProveFreshnessResponse *HostProveFreshnessResponse `json:",omitempty"` + HostIdentityRequest *HostIdentityRequest `json:",omitempty"` + HostIdentityResponse *HostIdentityResponse `json:",omitempty"` } // Type returns the message type by determining the name of the first non-nil member. @@ -218,6 +220,15 @@ type RuntimeCapabilityTEERakQuoteRequest struct { Quote quote.Quote `json:"quote"` } +// RuntimeCapabilityTEERakQuoteResponse is a worker RFC 0009 CapabilityTEE RAK quote setup response message body. +type RuntimeCapabilityTEERakQuoteResponse struct { + // Height is the runtime's view of the consensus layer height at the time of attestation. + Height uint64 `json:"height"` + + // Signature is the signature of the attestation by the enclave. + Signature signature.RawSignature `json:"signature"` +} + // RuntimeRPCCallRequest is a worker RPC call request message body. type RuntimeRPCCallRequest struct { // Request. @@ -546,3 +557,12 @@ type HostProveFreshnessResponse struct { // Proof of transaction inclusion in a block. Proof *consensusTx.Proof `json:"proof"` } + +// HostIdentityRequest is a request to host to return its identity. +type HostIdentityRequest struct{} + +// HostIdentityResponse is a response from host returning its identity. +type HostIdentityResponse struct { + // NodeID is the host node identifier. + NodeID signature.PublicKey `json:"node_id"` +} diff --git a/go/runtime/host/sgx/ecdsa.go b/go/runtime/host/sgx/ecdsa.go index e9180ddf476..411e1f0118b 100644 --- a/go/runtime/host/sgx/ecdsa.go +++ b/go/runtime/host/sgx/ecdsa.go @@ -153,7 +153,7 @@ func (ec *teeStateECDSA) Update(ctx context.Context, sp *sgxProvisioner, conn pr } // Call the runtime with the quote and TCB bundle. - _, err = conn.Call( + rspBody, err := conn.Call( ctx, &protocol.Body{ RuntimeCapabilityTEERakQuoteRequest: &protocol.RuntimeCapabilityTEERakQuoteRequest{ @@ -164,9 +164,15 @@ func (ec *teeStateECDSA) Update(ctx context.Context, sp *sgxProvisioner, conn pr if err != nil { return nil, fmt.Errorf("error while configuring quote: %w", err) } + rsp := rspBody.RuntimeCapabilityTEERakQuoteResponse + if rsp == nil { + return nil, fmt.Errorf("unexpected response from runtime") + } return cbor.Marshal(node.SGXAttestation{ Versioned: cbor.NewVersioned(node.LatestSGXAttestationVersion), Quote: q, + Height: rsp.Height, + Signature: rsp.Signature, }), nil } diff --git a/go/runtime/host/sgx/epid.go b/go/runtime/host/sgx/epid.go index 64f7d0f8790..d04ea18dc38 100644 --- a/go/runtime/host/sgx/epid.go +++ b/go/runtime/host/sgx/epid.go @@ -89,7 +89,7 @@ func (ep *teeStateEPID) Update(ctx context.Context, sp *sgxProvisioner, conn pro IAS: avrBundle, } - _, err = conn.Call( + rspBody, err := conn.Call( ctx, &protocol.Body{ // TODO: Use RuntimeCapabilityTEERakQuoteRequest once all runtimes support it. @@ -125,9 +125,16 @@ func (ep *teeStateEPID) Update(ctx context.Context, sp *sgxProvisioner, conn pro var attestation []byte if supportsAttestationV1 { // Use V1 attestation format. + rsp := rspBody.RuntimeCapabilityTEERakQuoteResponse + if rsp == nil { + return nil, fmt.Errorf("unexpected response from runtime") + } + attestation = cbor.Marshal(node.SGXAttestation{ Versioned: cbor.NewVersioned(node.LatestSGXAttestationVersion), Quote: q, + Height: rsp.Height, + Signature: rsp.Signature, }) } else { // Use V0 attestation format. diff --git a/go/runtime/registry/host.go b/go/runtime/registry/host.go index e730fb6ae7b..98a427aa2e0 100644 --- a/go/runtime/registry/host.go +++ b/go/runtime/registry/host.go @@ -354,6 +354,20 @@ func (h *runtimeHostHandler) handleHostProveFreshness( }, nil } +func (h *runtimeHostHandler) handleHostIdentity( + ctx context.Context, + rq *protocol.HostIdentityRequest, +) (*protocol.HostIdentityResponse, error) { + identity, err := h.env.GetNodeIdentity(ctx) + if err != nil { + return nil, err + } + + return &protocol.HostIdentityResponse{ + NodeID: identity.NodeSigner.Public(), + }, nil +} + // Implements protocol.Handler. func (h *runtimeHostHandler) Handle(ctx context.Context, rq *protocol.Body) (*protocol.Body, error) { var ( @@ -389,6 +403,9 @@ func (h *runtimeHostHandler) Handle(ctx context.Context, rq *protocol.Body) (*pr case rq.HostProveFreshnessRequest != nil: // Prove freshness. rsp.HostProveFreshnessResponse, err = h.handleHostProveFreshness(ctx, rq.HostProveFreshnessRequest) + case rq.HostIdentityRequest != nil: + // Host identity. + rsp.HostIdentityResponse, err = h.handleHostIdentity(ctx, rq.HostIdentityRequest) default: err = errMethodNotSupported } diff --git a/go/upgrade/migrations/consensus_tee_pcs.go b/go/upgrade/migrations/consensus_tee_pcs.go index 135a7680bb0..2d0b92f2fc5 100644 --- a/go/upgrade/migrations/consensus_tee_pcs.go +++ b/go/upgrade/migrations/consensus_tee_pcs.go @@ -39,7 +39,9 @@ func (th *teePcsHandler) ConsensusUpgrade(ctx *Context, privateCtx interface{}) params.TEEFeatures = &node.TEEFeatures{ SGX: node.TEEFeaturesSGX{ - PCS: true, + PCS: true, + SignedAttestations: true, + DefaultMaxAttestationAge: 1200, // ~2 hours at 6 sec per block. }, FreshnessProofs: true, } diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index dbe8d4e6490..d36480dbf58 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -43,7 +43,7 @@ curve25519-dalek = "3.2.0" x25519-dalek = "1.1.0" ed25519-dalek = "1.0.1" deoxysii = "0.2.3" -tiny-keccak = { version = "2.0.2", features = ["sha3"] } +tiny-keccak = { version = "2.0.2", features = ["sha3", "tuple_hash"] } sp800-185 = "0.2.0" zeroize = "1.4" intrusive-collections = "0.9.4" diff --git a/runtime/src/attestation.rs b/runtime/src/attestation.rs index 1b92d51123d..fbd8c14db3b 100644 --- a/runtime/src/attestation.rs +++ b/runtime/src/attestation.rs @@ -11,14 +11,18 @@ use slog::Logger; #[cfg(target_env = "sgx")] use crate::{ + common::crypto::signature::Signer, common::sgx::Quote, - consensus::registry::{SGXConstraints, TEEHardware}, + consensus::registry::{ + SGXAttestation, SGXConstraints, TEEHardware, ATTESTATION_SIGNATURE_CONTEXT, + }, consensus::state::registry::ImmutableState as RegistryState, types::Body, }; use crate::{ common::{logger::get_logger, namespace::Namespace, version::Version}, consensus::verifier::Verifier, + host::Host, rak::RAK, }; @@ -28,6 +32,8 @@ pub struct Handler { #[cfg_attr(not(target_env = "sgx"), allow(unused))] rak: Arc, #[cfg_attr(not(target_env = "sgx"), allow(unused))] + host: Arc, + #[cfg_attr(not(target_env = "sgx"), allow(unused))] consensus_verifier: Arc, #[cfg_attr(not(target_env = "sgx"), allow(unused))] runtime_id: Namespace, @@ -41,12 +47,14 @@ impl Handler { /// Create a new instance of the attestation flow handler. pub fn new( rak: Arc, + host: Arc, consensus_verifier: Arc, runtime_id: Namespace, version: Version, ) -> Self { Self { rak, + host, consensus_verifier, runtime_id, version, @@ -123,8 +131,14 @@ impl Handler { }; // Configure the quote and policy on the RAK. - self.rak.set_quote(quote, policy)?; + let verified_quote = self.rak.set_quote(quote, policy)?; + + // Sign the report data, latest verified consensus height and host node ID. + let height = consensus_state.height(); + let node_id = self.host.identity()?; + let h = SGXAttestation::hash(&verified_quote.report_data, node_id, height); + let signature = self.rak.sign(ATTESTATION_SIGNATURE_CONTEXT, &h)?; - Ok(Body::RuntimeCapabilityTEERakQuoteResponse {}) + Ok(Body::RuntimeCapabilityTEERakQuoteResponse { height, signature }) } } diff --git a/runtime/src/common/sgx/ias.rs b/runtime/src/common/sgx/ias.rs index 6be8114529a..b1a073f1145 100644 --- a/runtime/src/common/sgx/ias.rs +++ b/runtime/src/common/sgx/ias.rs @@ -41,8 +41,6 @@ enum AVRError { MissingQuoteStatus, #[error("AVR did not contain quote body")] MissingQuoteBody, - #[error("AVR did not contain nonce")] - MissingNonce, #[error("failed to parse quote")] MalformedQuote, #[error("unable to find exactly 2 certificates")] @@ -220,13 +218,6 @@ impl ParsedAVR { }; parse_avr_timestamp(timestamp) } - - pub(crate) fn nonce(&self) -> Result { - match self.body["nonce"].as_str() { - Some(nonce) => Ok(nonce.to_string()), - None => Err(AVRError::MissingNonce.into()), - } - } } /// Verify attestation report. @@ -260,8 +251,6 @@ pub fn verify(avr: &AVR, policy: &QuotePolicy) -> Result { return Err(AVRError::TimestampOutOfRange.into()); } - let nonce = avr_body.nonce()?; - let quote_status = avr_body.isv_enclave_quote_status()?; match quote_status.as_str() { "OK" | "SW_HARDENING_NEEDED" => {} @@ -315,7 +304,6 @@ pub fn verify(avr: &AVR, policy: &QuotePolicy) -> Result { mr_signer: MrSigner::from(quote_body.report_body.mrsigner.to_vec()), }, timestamp, - nonce: nonce.into_bytes(), }) } diff --git a/runtime/src/common/sgx/mod.rs b/runtime/src/common/sgx/mod.rs index b6375d91b28..22600958319 100644 --- a/runtime/src/common/sgx/mod.rs +++ b/runtime/src/common/sgx/mod.rs @@ -111,5 +111,4 @@ pub struct VerifiedQuote { pub report_data: Vec, pub identity: EnclaveIdentity, pub timestamp: i64, - pub nonce: Vec, } diff --git a/runtime/src/common/sgx/pcs.rs b/runtime/src/common/sgx/pcs.rs index 38a2c2a111e..8b2411e8968 100644 --- a/runtime/src/common/sgx/pcs.rs +++ b/runtime/src/common/sgx/pcs.rs @@ -224,18 +224,13 @@ impl QuoteBundle { return Err(Error::ProductionEnclave); } - let report_data = report_body.reportdata.to_vec(); - // The last 32 bytes of the report data (for runtime enclaves) is the nonce. - let nonce = report_data[32..].to_vec(); - Ok(VerifiedQuote { - report_data, + report_data: report_body.reportdata.to_vec(), identity: EnclaveIdentity { mr_enclave: MrEnclave::from(report_body.mrenclave.to_vec()), mr_signer: MrSigner::from(report_body.mrsigner.to_vec()), }, timestamp, - nonce, }) } } diff --git a/runtime/src/consensus/registry.rs b/runtime/src/consensus/registry.rs index a7f78224bd9..ee49d89688b 100644 --- a/runtime/src/consensus/registry.rs +++ b/runtime/src/consensus/registry.rs @@ -7,10 +7,14 @@ use std::collections::BTreeMap; use num_traits::Zero; +use tiny_keccak::{Hasher, TupleHash}; use crate::{ common::{ - crypto::{hash::Hash, signature::PublicKey}, + crypto::{ + hash::Hash, + signature::{PublicKey, Signature}, + }, namespace::Namespace, quantity, sgx, version::Version, @@ -19,6 +23,9 @@ use crate::{ rak::RAK, }; +/// Attestation signature context. +pub const ATTESTATION_SIGNATURE_CONTEXT: &[u8] = b"oasis-core/node: TEE attestation signature"; + /// Represents the address of a TCP endpoint. #[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)] pub struct TCPAddress { @@ -492,9 +499,11 @@ pub enum SGXConstraints { /// Old V0 format that only supported IAS policies. #[cbor(rename = 0, missing)] V0 { + /// The allowed MRENCLAVE/MRSIGNER pairs. #[cbor(optional)] enclaves: Vec, + /// A set of allowed quote statuses. #[cbor(optional)] allowed_quote_statuses: Vec, }, @@ -502,11 +511,17 @@ pub enum SGXConstraints { /// New V1 format that supports both IAS and PCS policies. #[cbor(rename = 1)] V1 { + /// The allowed MRENCLAVE/MRSIGNER pairs. #[cbor(optional)] enclaves: Vec, + /// The quote policy. #[cbor(optional)] policy: sgx::QuotePolicy, + + /// The maximum attestation age (in blocks). + #[cbor(optional)] + max_attestation_age: u64, }, } @@ -548,7 +563,14 @@ pub enum SGXAttestation { /// New V1 format that supports both IAS and PCS policies. #[cbor(rename = 1)] - V1 { quote: sgx::Quote }, + V1 { + /// An Intel SGX quote. + quote: sgx::Quote, + /// The runtime's view of the consensus layer height at the time of attestation. + height: u64, + /// The signature of the attestation by the enclave (RAK). + signature: Signature, + }, } impl SGXAttestation { @@ -556,9 +578,20 @@ impl SGXAttestation { pub fn quote(&self) -> sgx::Quote { match self { Self::V0(avr) => sgx::Quote::Ias(avr.clone()), - Self::V1 { quote } => quote.clone(), + Self::V1 { quote, .. } => quote.clone(), } } + + /// Hashes the required data that needs to be signed by RAK producing the attestation signature. + pub fn hash(report_data: &[u8], node_id: PublicKey, height: u64) -> [u8; 32] { + let mut h = TupleHash::v256(ATTESTATION_SIGNATURE_CONTEXT); + h.update(report_data); + h.update(node_id.as_ref()); + h.update(&height.to_le_bytes()); + let mut result = [0u8; 32]; + h.finalize(&mut result); + result + } } /// TEE hardware implementation. @@ -669,6 +702,8 @@ pub struct RuntimeGenesis { mod tests { use std::net::Ipv4Addr; + use rustc_hex::ToHex; + use crate::common::quantity::Quantity; use super::*; @@ -988,4 +1023,17 @@ mod tests { }); assert_eq!(ad, None); } + + #[test] + fn test_hash_attestation() { + let h = SGXAttestation::hash( + b"foo bar", + PublicKey::from("47aadd91516ac548decdb436fde957992610facc09ba2f850da0fe1b2be96119"), + 42, + ); + assert_eq!( + h.to_hex::(), + "0f01a5084bbf432427873cbce5f8c3bff76bc22b9d1e0674b852e43698abb195" + ); + } } diff --git a/runtime/src/dispatcher.rs b/runtime/src/dispatcher.rs index c33f6a06cd1..e5f165503f1 100644 --- a/runtime/src/dispatcher.rs +++ b/runtime/src/dispatcher.rs @@ -272,6 +272,7 @@ impl Dispatcher { txn_dispatcher: Arc::from(txn_dispatcher), attestation_handler: attestation::Handler::new( self.rak.clone(), + protocol.clone(), consensus_verifier, protocol.get_runtime_id(), protocol.get_config().version, diff --git a/runtime/src/host.rs b/runtime/src/host.rs new file mode 100644 index 00000000000..6bb1818d434 --- /dev/null +++ b/runtime/src/host.rs @@ -0,0 +1,33 @@ +//! Host interface. +use io_context::Context; +use thiserror::Error; + +use crate::{ + common::crypto::signature::PublicKey, + protocol::Protocol, + types::{self, Body}, +}; + +/// Errors. +#[derive(Error, Debug)] +pub enum Error { + #[error("bad response from host")] + BadResponse, + #[error("{0}")] + Other(#[from] types::Error), +} + +/// Interface to the (untrusted) host node. +pub trait Host: Send + Sync { + /// Returns the identity of the host node. + fn identity(&self) -> Result; +} + +impl Host for Protocol { + fn identity(&self) -> Result { + match self.call_host(Context::background(), Body::HostIdentityRequest {})? { + Body::HostIdentityResponse { node_id } => Ok(node_id), + _ => Err(Error::BadResponse), + } + } +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 691accc1110..7f14ec36383 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -28,6 +28,7 @@ pub mod config; pub mod consensus; pub mod dispatcher; pub mod enclave_rpc; +pub mod host; pub mod init; pub mod macros; pub mod protocol; diff --git a/runtime/src/rak.rs b/runtime/src/rak.rs index e4e48998288..f598f00e18a 100644 --- a/runtime/src/rak.rs +++ b/runtime/src/rak.rs @@ -5,8 +5,16 @@ use std::{ }; use anyhow::Result; +#[cfg(target_env = "sgx")] +use base64; +#[cfg(target_env = "sgx")] +use rand::{rngs::OsRng, Rng}; +#[cfg(target_env = "sgx")] +use sgx_isa::Report; use sgx_isa::Targetinfo; use thiserror::Error; +#[cfg(target_env = "sgx")] +use tiny_keccak::{Hasher, TupleHash}; #[cfg_attr(not(target_env = "sgx"), allow(unused))] use crate::common::crypto::hash::Hash; @@ -16,16 +24,12 @@ use crate::common::{ time::insecure_posix_time, }; -#[cfg(target_env = "sgx")] -use base64; -#[cfg(target_env = "sgx")] -use rand::{rngs::OsRng, Rng}; -#[cfg(target_env = "sgx")] -use sgx_isa::Report; - /// Context used for computing the RAK digest. #[cfg_attr(not(target_env = "sgx"), allow(unused))] const RAK_HASH_CONTEXT: &[u8] = b"oasis-core/node: TEE RAK binding"; +#[cfg_attr(not(target_env = "sgx"), allow(unused))] +/// Context used for deriving the nonce used in quotes. +const QUOTE_NONCE_CONTEXT: &[u8] = b"oasis-core/node: TEE quote nonce"; /// RAK-related error. #[derive(Error, Debug)] @@ -63,7 +67,7 @@ struct Inner { #[allow(unused)] target_info: Option, #[allow(unused)] - nonce: Option, + nonce: Option<[u8; 32]>, } /// Runtime attestation key. @@ -104,20 +108,18 @@ impl RAK { Hash::digest_bytes(&message) } - /// Generate a random 32 character nonce, for anti-replay. + /// Generate a random 256-bit nonce, for anti-replay. #[cfg(target_env = "sgx")] - fn generate_nonce() -> String { - // Note: The IAS protocol specifies this as 32 characters, and - // it's passed around as a JSON string, so this uses 24 bytes - // of entropy, Base64 encoded. - // - // XXX/yawning: Whiten the output, exposing raw OsRng output - // to outside the enclave makes me uneasy. + fn generate_nonce() -> [u8; 32] { let mut rng = OsRng {}; - let mut nonce_bytes = [0u8; 24]; // 24 bytes is 32 chars in Base64. + let mut nonce_bytes = [0u8; 32]; rng.fill(&mut nonce_bytes); - base64::encode(&nonce_bytes) + let mut h = TupleHash::v256(QUOTE_NONCE_CONTEXT); + h.update(&nonce_bytes); + h.finalize(&mut nonce_bytes); + + nonce_bytes } /// Get the SGX target info. @@ -161,12 +163,16 @@ impl RAK { // Generate a new anti-replay nonce. let nonce = Self::generate_nonce(); + // The derived nonce is only used in case IAS-based attestation is used + // as it is included in the outer AVR envelope. But given that the body + // also includes the nonce in our specific case, this is not relevant. + let quote_nonce = base64::encode(&nonce[..24]); // Generate report body. let report_body = Self::report_body_for_rak(&rak_pub); let mut report_data = [0; 64]; report_data[0..32].copy_from_slice(report_body.as_ref()); - report_data[32..64].copy_from_slice(nonce.as_bytes()); + report_data[32..64].copy_from_slice(nonce.as_ref()); let report = Report::for_target(&target_info, &report_data); @@ -177,12 +183,12 @@ impl RAK { let mut inner = self.inner.write().unwrap(); inner.nonce = Some(nonce.clone()); - (rak_pub, report, nonce) + (rak_pub, report, quote_nonce) } /// Configure the remote attestation quote for RAK. #[cfg(target_env = "sgx")] - pub(crate) fn set_quote(&self, quote: Quote, policy: QuotePolicy) -> Result<()> { + pub(crate) fn set_quote(&self, quote: Quote, policy: QuotePolicy) -> Result { let rak_pub = self.public_key().expect("RAK must be configured"); let mut inner = self.inner.write().unwrap(); @@ -201,7 +207,8 @@ impl RAK { inner.nonce = None; let verified_quote = quote.verify(&policy)?; - if expected_nonce.as_bytes() != verified_quote.nonce { + let nonce = &verified_quote.report_data[32..]; + if expected_nonce.as_ref() != nonce { return Err(QuoteError::NonceMismatch.into()); } @@ -220,17 +227,12 @@ impl RAK { // Verify that the quote has H(RAK) in report body. Self::verify_binding(&verified_quote, &rak_pub)?; - // Verify that the quote's report also contains the nonce. - if verified_quote.nonce != &verified_quote.report_data[32..64] { - return Err(QuoteError::NonceMismatch.into()); - } - // If there is an existing quote that is dated more recently than // the one being set, silently ignore the update. if inner.quote.is_some() { let existing_timestamp = inner.quote_timestamp.unwrap(); if existing_timestamp > verified_quote.timestamp { - return Ok(()); + return Ok(verified_quote); } } @@ -246,7 +248,7 @@ impl RAK { inner.known_quotes.pop_front(); } - Ok(()) + Ok(verified_quote) } /// Public part of RAK. diff --git a/runtime/src/types.rs b/runtime/src/types.rs index eb8388d0b83..64815755b32 100644 --- a/runtime/src/types.rs +++ b/runtime/src/types.rs @@ -135,7 +135,10 @@ pub enum Body { RuntimeCapabilityTEERakQuoteRequest { quote: Quote, }, - RuntimeCapabilityTEERakQuoteResponse {}, + RuntimeCapabilityTEERakQuoteResponse { + height: u64, + signature: Signature, + }, RuntimeRPCCallRequest { request: Vec, }, @@ -253,6 +256,10 @@ pub enum Body { signed_tx: SignedTransaction, proof: Proof, }, + HostIdentityRequest {}, + HostIdentityResponse { + node_id: PublicKey, + }, } impl Default for Body {