Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 88 additions & 10 deletions ear.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,20 @@
package ear

import (
"crypto"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
"time"

"github.com/fxamacker/cbor/v2"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v3/jwt"
"github.com/veraison/eat"
cose "github.com/veraison/go-cose"
)

// EatProfile is the EAT profile implemented by this package
Expand All @@ -27,18 +32,18 @@ const EatTrusteeProfile = "tag:github.com,2024:confidential-containers/Trustee"
// by the verifier. It is serialized to JSON and signed by the verifier using
// JWT.
type AttestationResult struct {
Profile *string `json:"eat_profile"`
VerifierID *VerifierIdentity `json:"ear.verifier-id"`
RawEvidence *B64Url `json:"ear.raw-evidence,omitempty"`
IssuedAt *int64 `json:"iat"`
Nonce *string `json:"eat_nonce,omitempty"`
Submods map[string]*Appraisal `json:"submods"`
Profile *string `cbor:"265,keyasint" json:"eat_profile"`
VerifierID *VerifierIdentity `cbor:"1004,keyasint" json:"ear.verifier-id"`
RawEvidence *B64Url `cbor:"1002,keyasint,omitempty" json:"ear.raw-evidence,omitempty"`
IssuedAt *int64 `cbor:"6,keyasint" json:"iat"`
Nonce *eat.Nonce `cbor:"10,keyasint,omitempty" json:"eat_nonce,omitempty"`
Submods map[string]*Appraisal `cbor:"266,keyasint" json:"submods"`

AttestationResultExtensions
}

type AttestationResultExtensions struct {
VeraisonTeeInfo *VeraisonTeeInfo `json:"ear.veraison.tee-info,omitempty"`
VeraisonTeeInfo *VeraisonTeeInfo `cbor:"65001" json:"ear.veraison.tee-info,omitempty"`
}

// B64Url is base64url (§5 of RFC4648) without padding.
Expand Down Expand Up @@ -158,9 +163,8 @@ func (o AttestationResult) validate() error {
}

if o.Nonce != nil {
nLen := len(*o.Nonce)
if nLen > 88 || nLen < 8 {
invalid = append(invalid, fmt.Sprintf("eat_nonce (%d bytes)", nLen))
if err := o.Nonce.Validate(); err != nil {
invalid = append(invalid, fmt.Sprintf("eat_nonce (%s)", err.Error()))
}
}

Expand Down Expand Up @@ -277,3 +281,77 @@ func (o *AttestationResult) populateFromMap(m map[string]interface{}) error {

return populateStructFromMap(o, m, "json", parsers, stringPtrParser, true)
}

// MarshalCBOR validates and serializes to JSON an AttestationResult object
func (o AttestationResult) ToCBOR() ([]byte, error) {
if err := o.validate(); err != nil {
return nil, err
}

return cbor.Marshal(o)
}

// UnmarshalCBOR de-serializes an AttestationResult object from its JSON
// representation and validates it.
func (o *AttestationResult) FromCBOR(data []byte) error {
if err := cbor.Unmarshal(data, o); err != nil {
return err
}

return o.validate()
}

// Verify cryptographically verifies the CWT data using the supplied key and
// algorithm. The payload is then parsed and validated. On success, the target
// AttestationResult object is populated with the decoded claims (possibly
// including the Trustworthiness vector).
func (o *AttestationResult) VerifyCWT(data []byte, alg cose.Algorithm, publicKey crypto.PublicKey) error {
// create a verifier from a trusted private key
verifier, err := cose.NewVerifier(alg, publicKey)
if err != nil {
return err
}

// Try COSE_Sign1
var sign1 cose.Sign1Message
if err := sign1.UnmarshalCBOR(data); err == nil {
if err := sign1.Verify(nil, verifier); err != nil {
return fmt.Errorf("failed verifying COSE_Sign1 message: %w", err)
}
if err := o.FromCBOR(sign1.Payload); err != nil {
return err
}
return nil
}
return fmt.Errorf("failed to parse CWT message (only COSE_Sign1 is supported now): %w", err)
}

// Sign validates the AttestationResult object, encodes it to JSON and wraps it
// in a JWT using the supplied private key for signing. The key must be
// compatible with the requested signing algorithm. On success, the complete
// JWT token is returned.
func (o AttestationResult) SignCWT(alg cose.Algorithm, privateKey crypto.Signer) ([]byte, error) {
if err := o.validate(); err != nil {
return nil, err
}

signer, err := cose.NewSigner(alg, privateKey)
if err != nil {
return nil, err
}

// create message header
headers := cose.Headers{
Protected: cose.ProtectedHeader{
cose.HeaderLabelAlgorithm: cose.AlgorithmES256,
},
}

data, err := o.ToCBOR()
if err != nil {
return nil, err
}

// sign and marshal message
return cose.Sign1(rand.Reader, signer, headers, data, nil)
}
15 changes: 9 additions & 6 deletions ear_appraisal.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,18 @@ import (
"encoding/base64"
"errors"
"fmt"

"github.com/veraison/eat"
)

// Appraisal represents the result of an evidence appraisal
// by the verifier. It wraps the AR4SI trustworthiness vector together with
// other metadata that are relevant to establish the appraisal context - the
// evidence itself, the appraisal policy used, the time of appraisal.
type Appraisal struct {
Status *TrustTier `json:"ear.status"`
TrustVector *TrustVector `json:"ear.trustworthiness-vector,omitempty"`
AppraisalPolicyID *string `json:"ear.appraisal-policy-id,omitempty"`
Status *TrustTier `cbor:"1000,keyasint" json:"ear.status"`
TrustVector *TrustVector `cbor:"1001,keyasint,omitempty" json:"ear.trustworthiness-vector,omitempty"`
AppraisalPolicyID *string `cbor:"1003,keyasint,omitempty" json:"ear.appraisal-policy-id,omitempty"`

AppraisalExtensions
}
Expand All @@ -29,9 +31,10 @@ type Appraisal struct {
// attached to the Appraisal. For now only veraison-specific extensions are
// supported.
type AppraisalExtensions struct {
VeraisonAnnotatedEvidence *map[string]interface{} `json:"ear.veraison.annotated-evidence,omitempty"`
VeraisonPolicyClaims *map[string]interface{} `json:"ear.veraison.policy-claims,omitempty"`
VeraisonKeyAttestation *map[string]interface{} `json:"ear.veraison.key-attestation,omitempty"`
EatClaimsSet *eat.Eat `cbor:"65000,keyasint,omitempty" json:"ear.eat-claims-set,omitempty"`
VeraisonAnnotatedEvidence *map[string]interface{} `cbor:"-70000,keyasint,omitempty" json:"ear.veraison.annotated-evidence,omitempty"`
VeraisonPolicyClaims *map[string]interface{} `cbor:"-70001,keyasint,omitempty" json:"ear.veraison.policy-claims,omitempty"`
VeraisonKeyAttestation *map[string]interface{} `cbor:"-70002,keyasint,omitempty" json:"ear.veraison.key-attestation,omitempty"`
}

// SetKeyAttestation sets the value of `akpub` in the
Expand Down
87 changes: 87 additions & 0 deletions ear_appraisal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,26 @@ import (
"math/big"
"testing"

"github.com/fxamacker/cbor/v2"
"github.com/stretchr/testify/assert"
"github.com/veraison/eat"
"github.com/veraison/swid"
)

func TestAppraisal_ok(t *testing.T) {
// A1 # map(1)
// 19 03E8 # unsigned(1000)
// 02 # unsigned(2)
tv := []byte{0xA1, 0x19, 0x03, 0xE8, 0x02}

var appraisal Appraisal
err := cbor.Unmarshal(tv, &appraisal)
assert.NoError(t, err)

expectedStatus := TrustTier(2)
assert.Equal(t, &expectedStatus, appraisal.Status)
}

func TestAppraisalExtensions_SetGetKeyAttestation_ok(t *testing.T) {
expected := AppraisalExtensions{
VeraisonKeyAttestation: &map[string]interface{}{
Expand Down Expand Up @@ -91,3 +108,73 @@ func TestAppraisalExtensions_GetKeyAttestation_fail_akpub_no_b64url(t *testing.T
_, err := tv.GetKeyAttestation()
assert.EqualError(t, err, `"ear.veraison.key-attestation" malformed: decoding "akpub": illegal base64 data at input byte 84`)
}

func TestAppraisalExtensions_TEEPClaims_ok(t *testing.T) {
// A1 # map(1)
// 19 FDE8 # unsigned(65000)
// A6 # map(6)
// 0A # unsigned(10)
// 48 # bytes(8)
// 948F8860D13A463E # "\x94\x8F\x88`\xD1:F>"
// 19 0100 # unsigned(256)
// 50 # bytes(16)
// 0198F50A4FF6C05861C8860D13A638EA # "\u0001\x98\xF5\nO\xF6\xC0XaȆ\r\u0013\xA68\xEA"
// 19 0102 # unsigned(258)
// 43 # bytes(3)
// 064242 # "\u0006BB"
// 19 0103 # unsigned(259)
// 50 # bytes(16)
// EE80F5A66C1FB9742999A8FDAB930893 # "\xEE\x80\xF5\xA6l\u001F\xB9t)\x99\xA8\xFD\xAB\x93\b\x93"
// 19 0104 # unsigned(260)
// 82 # array(2)
// 65 # text(5)
// 312E322E35 # "1.2.5"
// 19 4000 # unsigned(16384)
// 19 0109 # unsigned(265)
// 74 # text(20)
// 75726E3A696574663A7266633A72666358585858 # "urn:ietf:rfc:rfcXXXX"

expected := []byte{
0xA1, 0x19, 0xFD, 0xE8, 0xA6, 0x0A, 0x48, 0x94,
0x8F, 0x88, 0x60, 0xD1, 0x3A, 0x46, 0x3E, 0x19,
0x01, 0x00, 0x50, 0x01, 0x98, 0xF5, 0x0A, 0x4F,
0xF6, 0xC0, 0x58, 0x61, 0xC8, 0x86, 0x0D, 0x13,
0xA6, 0x38, 0xEA, 0x19, 0x01, 0x02, 0x43, 0x06,
0x42, 0x42, 0x19, 0x01, 0x03, 0x50, 0xEE, 0x80,
0xF5, 0xA6, 0x6C, 0x1F, 0xB9, 0x74, 0x29, 0x99,
0xA8, 0xFD, 0xAB, 0x93, 0x08, 0x93, 0x19, 0x01,
0x04, 0x82, 0x65, 0x31, 0x2E, 0x32, 0x2E, 0x35,
0x19, 0x40, 0x00, 0x19, 0x01, 0x09, 0x74, 0x75,
0x72, 0x6E, 0x3A, 0x69, 0x65, 0x74, 0x66, 0x3A,
0x72, 0x66, 0x63, 0x3A, 0x72, 0x66, 0x63, 0x58,
0x58, 0x58, 0x58,
}

testNonce := eat.Nonce{}
assert.Nil(t, testNonce.UnmarshalCBOR([]byte{0x48, 0x94, 0x8F, 0x88, 0x60, 0xD1, 0x3A, 0x46, 0x3E}))
testProfile := eat.Profile{}
testProfile.Set("urn:ietf:rfc:rfcXXXX")
var testVersionScheme swid.VersionScheme
testVersionScheme.SetCode(swid.VersionSchemeSemVer)

tv := AppraisalExtensions{
EatClaimsSet: &eat.Eat{
Nonce: &testNonce,
UEID: &eat.UEID{0x01, 0x98, 0xF5, 0x0A, 0x4F, 0xF6, 0xC0, 0x58, 0x61, 0xC8, 0x86, 0x0D, 0x13, 0xA6, 0x38, 0xEA},
OemID: &[]byte{0x06, 0x42, 0x42},
HardwareModel: &[]byte{
0xEE, 0x80, 0xF5, 0xA6, 0x6C, 0x1F, 0xB9, 0x74,
0x29, 0x99, 0xA8, 0xFD, 0xAB, 0x93, 0x08, 0x93,
},
HardwareVersion: &eat.Version{
Version: "1.2.5",
Scheme: &testVersionScheme,
},
Profile: &testProfile,
},
}

data, err := cbor.Marshal(tv)
assert.NoError(t, err)
assert.Equal(t, expected, data)
}
Loading