Skip to content

Commit

Permalink
Bind TEE attestations to nodes and enforce freshness
Browse files Browse the repository at this point in the history
  • Loading branch information
kostko committed Sep 9, 2022
1 parent a2aa326 commit de331d6
Show file tree
Hide file tree
Showing 36 changed files with 442 additions and 141 deletions.
1 change: 1 addition & 0 deletions .changelog/4926.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Bind TEE attestations to nodes and enforce freshness
48 changes: 15 additions & 33 deletions go/common/node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)

Expand Down Expand Up @@ -480,18 +491,16 @@ 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[:]...)
return hash.NewFromBytes(hData)
}

// 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.
Expand All @@ -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
}
Expand Down
105 changes: 105 additions & 0 deletions go/common/node/sgx.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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.
Expand Down Expand Up @@ -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
}

Expand All @@ -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.
Expand Down Expand Up @@ -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)
}
13 changes: 13 additions & 0 deletions go/common/node/sgx_test.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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")
Expand Down
39 changes: 23 additions & 16 deletions go/common/node/tee.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 = &quote.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 = &quote.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
}
39 changes: 23 additions & 16 deletions go/common/node/tee_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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(&quote.Policy{
PCS: existingPcsPolicy,
})
require.EqualValues(defaultIasPolicy, policy.IAS)
require.EqualValues(existingPcsPolicy, policy.PCS)
sc = SGXConstraints{
Policy: &quote.Policy{
PCS: existingPcsPolicy,
},
}

tf.ApplyDefaultConstraints(&sc)
require.EqualValues(defaultIasPolicy, sc.Policy.IAS)
require.EqualValues(existingPcsPolicy, sc.Policy.PCS)

tf = TEEFeaturesSGX{
PCS: false,
Expand All @@ -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")
}
Binary file modified go/common/node/testdata/sgx_attestation_v1.bin
Binary file not shown.
2 changes: 1 addition & 1 deletion go/consensus/tendermint/apps/keymanager/keymanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions go/consensus/tendermint/apps/registry/transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ func (app *registryApplication) registerNode( // nolint: gocyclo
sigNode,
untrustedEntity,
ctx.Now(),
uint64(ctx.BlockHeight()),
ctx.IsInitChain(),
false,
epoch,
Expand Down
Loading

0 comments on commit de331d6

Please sign in to comment.