From b4b5ec6c38edd1812d71939349d78fc13e0add21 Mon Sep 17 00:00:00 2001 From: setrofim Date: Tue, 22 Nov 2022 14:42:50 +0000 Subject: [PATCH] Usability features (#20) * Rename TClaim to TrustClaim The new name is both, consistent with TrustTier, and clearer. * Add TrustClaim constants Add constants definitions for the defined claim values across trust vector claim categories. * Usability features Implement a number of changes and additions to aid in usability of this library. No material changes are being made toe the forts of EAR or AR4SI as part of this. - Rename To/FromJSON to Marshal/UnmarshaJSON to be consistent with common golang conventions. Rename ToJSONPretty to MarshalJSONIndent for the same reason. - Add AsMap() methods to TrustVector and AttestationResult that converts these structs to map[string]interface{}. - Add UpdateStatusFromTrustVector() to AttestationResult, that brings Status into alignment with TrustVector values (unless it was explicitly set to a lower trust value). - Add ToTrustTier and ToTrustClaim to convert arbitrary interfaces to corresponding structs. - Add TrustClaim.GetTier() that returns the tier corresponding to the claim value. - Add NewAttestationResult that returns a fully initialized attestation result. - Switch to using jwt package for signing and verifying. * Lower coverage requirements to 80% Previous commit introduces an number of type switches with multitudes of identical cases for all the different integer types. This technically lowers statement coverage of the tests without materially affecting logical coverage of the code. Signed-off-by: setrofim --- .github/workflows/ci-go-cover.yml | 2 +- arc/cmd/create.go | 2 +- arc/cmd/create_test.go | 10 +- arc/cmd/verify.go | 2 +- doc.go | 8 +- ear.go | 427 +++++++++++++++++++++++++++--- ear_test.go | 167 ++++++++++-- example_test.go | 18 +- go.mod | 1 + go.sum | 2 + tclaim_test.go | 59 ----- tclaim.go => trustclaim.go | 362 ++++++++++++++++++------- trustclaim_test.go | 116 ++++++++ trustvector.go | 147 +++++++++- trustvector_test.go | 65 +++++ util.go | 21 ++ util_test.go | 36 +++ 17 files changed, 1221 insertions(+), 224 deletions(-) delete mode 100644 tclaim_test.go rename tclaim.go => trustclaim.go (55%) create mode 100644 trustclaim_test.go create mode 100644 util.go create mode 100644 util_test.go diff --git a/.github/workflows/ci-go-cover.yml b/.github/workflows/ci-go-cover.yml index d50052b..dc8b31a 100644 --- a/.github/workflows/ci-go-cover.yml +++ b/.github/workflows/ci-go-cover.yml @@ -14,7 +14,7 @@ # 1. Change workflow name from "cover 100%" to "cover ≥92.5%". Script will automatically use 92.5%. # 2. Update README.md to use the new path to badge.svg because the path includes the workflow name. -name: cover ≥90.0% +name: cover ≥80.0% on: [push, pull_request] jobs: cover: diff --git a/arc/cmd/create.go b/arc/cmd/create.go index 62809b7..c908988 100644 --- a/arc/cmd/create.go +++ b/arc/cmd/create.go @@ -51,7 +51,7 @@ the key in the default key file "skey.json", and save the result to "my-ear.jwt" return fmt.Errorf("loading EAR claims-set from %q: %w", createClaims, err) } - if err = ar.FromJSON(claimsSet); err != nil { + if err = ar.UnmarshalJSON(claimsSet); err != nil { return fmt.Errorf("decoding EAR claims-set from %q: %w", createClaims, err) } diff --git a/arc/cmd/create_test.go b/arc/cmd/create_test.go index 8de6008..d8bb5b5 100644 --- a/arc/cmd/create_test.go +++ b/arc/cmd/create_test.go @@ -89,10 +89,10 @@ func Test_CreateCmd_skey_not_ok_for_signing(t *testing.T) { } cmd.SetArgs(args) - expectedErr := `signing EAR: failed to generate signature for signer #0 (alg=ES256): failed to sign payload: failed to retrieve ecdsa.PrivateKey out of *jwk.ecdsaPublicKey: failed to produce ecdsa.PrivateKey from *jwk.ecdsaPublicKey: argument to AssignIfCompatible() must be compatible with *ecdsa.PublicKey (was *ecdsa.PrivateKey)` + expectedErr := `failed to generate signature for signer #0 (alg=ES256): failed to sign payload: failed to retrieve ecdsa.PrivateKey out of *jwk.ecdsaPublicKey: failed to produce ecdsa.PrivateKey from *jwk.ecdsaPublicKey: argument to AssignIfCompatible() must be compatible with *ecdsa.PublicKey (was *ecdsa.PrivateKey)` err := cmd.Execute() - assert.EqualError(t, err, expectedErr) + assert.ErrorContains(t, err, expectedErr) } func Test_CreateCmd_input_file_not_found(t *testing.T) { @@ -131,7 +131,7 @@ func Test_CreateCmd_input_file_bad_format(t *testing.T) { } cmd.SetArgs(args) - expectedErr := `decoding EAR claims-set from "ear-claims.json": missing mandatory 'eat_profile', 'status', 'iat'` + expectedErr := `decoding EAR claims-set from "ear-claims.json": missing mandatory 'ear.status', 'eat_profile', 'iat'` err := cmd.Execute() assert.EqualError(t, err, expectedErr) @@ -154,10 +154,10 @@ func Test_CreateCmd_unknown_signing_alg(t *testing.T) { } cmd.SetArgs(args) - expectedErr := `signing EAR: jws.Sign: expected algorithm to be of type jwa.SignatureAlgorithm but got ("XYZ", jwa.InvalidKeyAlgorithm)` + expectedErr := `expected algorithm to be of type jwa.SignatureAlgorithm but got ("XYZ", jwa.InvalidKeyAlgorithm)` err := cmd.Execute() - assert.EqualError(t, err, expectedErr) + assert.ErrorContains(t, err, expectedErr) } func Test_CreateCmd_ok(t *testing.T) { diff --git a/arc/cmd/verify.go b/arc/cmd/verify.go index f25bba2..22637a5 100644 --- a/arc/cmd/verify.go +++ b/arc/cmd/verify.go @@ -69,7 +69,7 @@ embedded EAR claims-set and present a report of the trustworthiness vector. fmt.Printf(">> %q signature successfully verified using %q\n", verifyInput, verifyPKey) fmt.Println("[claims-set]") - if claimsSet, err = ar.ToJSONPretty(); err != nil { + if claimsSet, err = ar.MarshalJSONIndent("", " "); err != nil { return fmt.Errorf("unable to re-serialize the EAR claims-set: %w", err) } fmt.Println(string(claimsSet)) diff --git a/doc.go b/doc.go index 6686e5d..ced67d9 100644 --- a/doc.go +++ b/doc.go @@ -6,7 +6,7 @@ Package ear implements an EAT attestation result format based on the information model defined in https://datatracker.ietf.org/doc/draft-ietf-rats-ar4si/ -Construction +# Construction An AttestationResult object is constructed by populating the relevant fields. The mandatory attributes are: status, timestamp and profile. @@ -40,7 +40,7 @@ and their meaning.) ar.TrustVector := &tv -Signing and Serializing +# Signing and Serializing Once the AttestationResult is populated, it can be signed (i.e., wrapped in a JWT) by invoking the Sign method: @@ -61,7 +61,7 @@ In this case, the returned buf contains a signed ES256 JWT with the JSON serialization of the AttestationResult object as its payload. This is the usual JWT format that can be used as-is for interchange with other applications. -Parsing and Verifying +# Parsing and Verifying On the consumer end of the protocol, when the EAT containing the attestation result is received from a veraison verifier, the relying party needs to first @@ -91,7 +91,7 @@ entity. // handle troubles with appraisal } -Pretty printing +# Pretty printing The package provides a Report method that allows pretty printing of the Trustworthiness Vector. The caller can request a short summary or a detailed diff --git a/ear.go b/ear.go index 8196a56..da019ea 100644 --- a/ear.go +++ b/ear.go @@ -4,13 +4,17 @@ package ear import ( + "encoding/base64" "encoding/json" "errors" "fmt" + "math" + "strconv" "strings" + "time" "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jws" + "github.com/lestrrat-go/jwx/v2/jwt" ) // EatProfile is the EAT profile implemented by this package @@ -40,7 +44,7 @@ const ( ) var ( - StatusTierToString = map[TrustTier]string{ + TrustTierToString = map[TrustTier]string{ TrustTierNone: "none", TrustTierAffirming: "affirming", TrustTierWarning: "warning", @@ -53,15 +57,165 @@ var ( "warning": TrustTierWarning, "contraindicated": TrustTierContraindicated, } + + IntToTrustTier = map[int]TrustTier{ + 0: TrustTierNone, + 2: TrustTierAffirming, + 32: TrustTierWarning, + 96: TrustTierContraindicated, + } ) +// NewTrustTier returns a pointer to a newly-created TrustTier that has the +// specified value. If the provided value is invalid for a TrustTier, +// TrustTierNone will be used instead. +// (This is essentially a Must- wrapper for ToTrustTier().) +func NewTrustTier(v interface{}) *TrustTier { + tt, err := ToTrustTier(v) + + if err != nil { + none := TrustTierNone + return &none + } + + return tt +} + +func getTrustTierFromInt(i int) (TrustTier, error) { + tier, ok := IntToTrustTier[i] + if !ok { + return TrustTierNone, fmt.Errorf("not a valid TrustTier value: %d", i) + } + + return tier, nil +} + +func getTrustTierFromString(s string) (TrustTier, error) { + tier, ok := StringToTrustTier[s] + if !ok { + i, err := strconv.Atoi(s) + if err == nil { + return getTrustTierFromInt(i) + } + return TrustTierNone, fmt.Errorf("not a valid TrustTier name: %q", s) + } + + return tier, nil +} + +func ToTrustTier(v interface{}) (*TrustTier, error) { + var ( + err error + ok bool + + tier = TrustTierNone + ) + + switch t := v.(type) { + case string: + tier, err = getTrustTierFromString(t) + case []byte: + tier, err = getTrustTierFromString(string(t)) + case TrustClaim: + tier = t.GetTier() + case int: + tier, err = getTrustTierFromInt(t) + case int8: + tier, err = getTrustTierFromInt(int(t)) + case int16: + tier, err = getTrustTierFromInt(int(t)) + case int32: + tier, err = getTrustTierFromInt(int(t)) + case int64: + tier, err = getTrustTierFromInt(int(t)) + case uint8: + tier, err = getTrustTierFromInt(int(t)) + case uint16: + tier, err = getTrustTierFromInt(int(t)) + case uint32: + tier, err = getTrustTierFromInt(int(t)) + case uint: + if t > math.MaxInt64 { + err = fmt.Errorf("not a valid TrustTier value: %d", t) + } else { + tier, err = getTrustTierFromInt(int(t)) + } + case uint64: + if t > math.MaxInt64 { + err = fmt.Errorf("not a valid TrustTier value: %d", t) + + } else { + tier, err = getTrustTierFromInt(int(t)) + } + case float64: + tier, ok = IntToTrustTier[int(t)] + if !ok { + err = fmt.Errorf("not a valid TrustTier value: %f (%d)", t, int(t)) + } + case json.Number: + i, e := t.Int64() + if e != nil { + err = fmt.Errorf("not a valid TrustTier value: %v: %w", t, err) + } else { + tier, ok = IntToTrustTier[int(i)] + if !ok { + err = fmt.Errorf("not a valid TrustTier value: %v (%d)", t, int(i)) + } + } + default: + err = fmt.Errorf("cannot convert %v (type %T) to TrustTier", t, t) + } + + return &tier, err +} + +func (o TrustTier) Format(color bool) string { + if color { + return o.ColorString() + } + + return o.String() +} + +func (o TrustTier) String() string { + return TrustTierToString[o] +} + +func (o TrustTier) ColorString() string { + const ( + reset = `\033[0m` + red = `\033[41m` + yellow = `\033[43m` + green = `\033[42m` + white = `\033[47m` + + unexpected = `\033[1;33;41m` + ) + + var color string + switch o { + case TrustTierNone: + color = white + case TrustTierAffirming: + color = green + case TrustTierWarning: + color = yellow + case TrustTierContraindicated: + color = red + default: + color = unexpected + } + + return color + o.String() + reset +} + func (o TrustTier) MarshalJSON() ([]byte, error) { var ( s string ok bool ) - s, ok = StatusTierToString[o] + s, ok = TrustTierToString[o] if !ok { return nil, fmt.Errorf("unknown trust tier '%d'", o) } @@ -102,33 +256,111 @@ type AttestationResult struct { Extensions } -// ToJSON validates and serializes to JSON an AttestationResult object -func (o AttestationResult) ToJSON() ([]byte, error) { +// NewAttestationResult returns a pointer to a new fully-initialized +// AttestationResult. +func NewAttestationResult() *AttestationResult { + status := TrustTierNone + iat := time.Now().Unix() + profile := EatProfile + + return &AttestationResult{ + Status: &status, + Profile: &profile, + TrustVector: &TrustVector{}, + IssuedAt: &iat, + } +} + +// MarshalJSON validates and serializes to JSON an AttestationResult object +func (o AttestationResult) MarshalJSON() ([]byte, error) { if err := o.validate(); err != nil { return nil, err } - return json.Marshal(o) + return json.Marshal(o.AsMap()) } -// ToJSONPretty does the same as ToJSON but add NL and indentation to improve -// readability -func (o AttestationResult) ToJSONPretty() ([]byte, error) { +// MarshalJSONIndent is like MarshalJSON but applies Indent to format the +// output. Each JSON element in the output will begin on a new line beginning +// with prefix followed by one or more copies of indent according to the +// indentation nesting. +func (o AttestationResult) MarshalJSONIndent(prefix, indent string) ([]byte, error) { if err := o.validate(); err != nil { return nil, err } - return json.MarshalIndent(o, "", " ") + return json.MarshalIndent(o.AsMap(), prefix, indent) } -// FromJSON de-serializes an AttestationResult object from its JSON +// UnmarshalJSON de-serializes an AttestationResult object from its JSON // representation and validates it. -func (o *AttestationResult) FromJSON(data []byte) error { - err := json.Unmarshal(data, o) - if err == nil { - return o.validate() +func (o *AttestationResult) UnmarshalJSON(data []byte) error { + var oMap map[string]interface{} + if err := json.Unmarshal(data, &oMap); err != nil { + return err + } + + if err := o.populateFromMap(oMap); err != nil { + return err + } + + return o.validate() +} + +// AsMap returns a map[string]interface{} with EAR claim names mapped onto +// corresponding values. +func (o AttestationResult) AsMap() map[string]interface{} { + oMap := make(map[string]interface{}) + + if o.Status != nil { + oMap["ear.status"] = *o.Status + } + + if o.Profile != nil { + oMap["eat_profile"] = *o.Profile + } + + if o.IssuedAt != nil { + oMap["iat"] = *o.IssuedAt + } + + if o.TrustVector != nil { + oMap["ear.trustworthiness-vector"] = o.TrustVector.AsMap() + } + + if o.RawEvidence != nil { + oMap["ear.raw-evidence"] = *o.RawEvidence + } + + if o.AppraisalPolicyID != nil { + oMap["ear.appraisal-policy-id"] = *o.AppraisalPolicyID + } + + if o.VeraisonProcessedEvidence != nil { + oMap["ear.veraison.processed-evidence"] = *o.VeraisonProcessedEvidence + } + + if o.VeraisonVerifierAddedClaims != nil { + oMap["ear.veraison.verifier-added-claims"] = *o.VeraisonVerifierAddedClaims + } + + return oMap +} + +// UpdateStatusFromTrustVector ensure that Status trustworthiness is not +// higher than is warranted by trust vector claims. For every claim that has +// been made (i.e. is not in TrustTierNone), if the claim's trust tier is lower +// than that of the Status, adjust the status to the claim's tier. This means +// that the overall result will not assert to be more trustworthy than +// individual vector claims (though it could be less trustworthy if had been +// manually set that way). +func (o *AttestationResult) UpdateStatusFromTrustVector() { + for _, claimValue := range o.TrustVector.AsMap() { + claimTier := claimValue.GetTier() + if *o.Status < claimTier { + *o.Status = claimTier + } } - return err } func (o AttestationResult) validate() error { @@ -175,24 +407,15 @@ type Extensions struct { // AttestationResult object is populated with the decoded claims (possibly // including the Trustworthiness vector). func (o *AttestationResult) Verify(data []byte, alg jwa.KeyAlgorithm, key interface{}) error { - buf, err := jws.Verify(data, jws.WithKey(alg, key)) + token, err := jwt.Parse(data, jwt.WithKey(alg, key)) if err != nil { return fmt.Errorf("failed verifying JWT message: %w", err) } - // TODO(tho) add any JWT specific checks on top of the base JWS verification - // See https://github.com/veraison/ear/issues/6 - - var ar AttestationResult + claims := token.PrivateClaims() + claims["iat"] = token.IssuedAt().Unix() - err = ar.FromJSON(buf) - if err != nil { - return fmt.Errorf("failed parsing JWT payload: %w", err) - } - - *o = ar - - return nil + return o.populateFromMap(claims) } // Sign validates the AttestationResult object, encodes it to JSON and wraps it @@ -200,10 +423,150 @@ func (o *AttestationResult) Verify(data []byte, alg jwa.KeyAlgorithm, key interf // compatible with the requested signing algorithm. On success, the complete // JWT token is returned. func (o AttestationResult) Sign(alg jwa.KeyAlgorithm, key interface{}) ([]byte, error) { - payload, err := o.ToJSON() - if err != nil { + if err := o.validate(); err != nil { return nil, err } - return jws.Sign(payload, jws.WithKey(alg, key)) + token := jwt.New() + for k, v := range o.AsMap() { + if err := token.Set(k, v); err != nil { + return nil, fmt.Errorf("setting %s: %w", k, err) + } + } + + return jwt.Sign(token, jwt.WithKey(alg, key)) +} + +func (o *AttestationResult) populateFromMap(m map[string]interface{}) error { + var err error + + var missing, invalid []string + + expected := []string{ + "ear.status", + "eat_profile", + "iat", + "ear.trustworthiness-vector", + "ear.raw-evidence", + "ear.appraisal-policy-id", + "ear.veraison.processed-evidence", + "ear.veraison.verifier-added-claims", + } + extra := getExtraKeys(m, expected) + + v, ok := m["ear.status"] + if ok { + o.Status, err = ToTrustTier(v) + if err != nil { + invalid = append(invalid, "'ear.status'") + } + } else { + missing = append(missing, "'ear.status'") + } + + v, ok = m["eat_profile"] + if ok { + profile, okay := v.(string) + if !okay { + invalid = append(invalid, "'eat_profiles'") + } + o.Profile = &profile + + } else { + missing = append(missing, "'eat_profile'") + } + + v, ok = m["iat"] + if ok { + var iat int64 + switch t := v.(type) { + case float64: + iat = int64(t) + case int: + iat = int64(t) + case int64: + iat = t + default: + invalid = append(invalid, "'iat'") + } + o.IssuedAt = &iat + } else { + missing = append(missing, "'iat'") + } + + v, ok = m["ear.trustworthiness-vector"] + if ok { + o.TrustVector, err = ToTrustVector(v) + if err != nil { + invalid = append(invalid, fmt.Sprintf("'ear.trustworthiness-vector' (%s)", err)) + } + } + + var stringVal string + + v, ok = m["ear.raw-evidence"] + if ok { + rawEvString, okay := v.(string) + if !okay { + invalid = append(invalid, "'ear.raw-evidence'") + } + + decodedRawEv, err := base64.StdEncoding.DecodeString(rawEvString) + if err != nil { + invalid = append(invalid, "'ear.raw-evidence'") + } + + o.RawEvidence = &decodedRawEv + } + + v, ok = m["ear.appraisal-policy-id"] + if ok { + stringVal, ok = v.(string) + if !ok { + invalid = append(invalid, "'ear.appraisal-policy-id'") + } + o.AppraisalPolicyID = &stringVal + } + + v, ok = m["ear.veraison.processed-evidence"] + if ok { + processedEvidence, okay := v.(map[string]interface{}) + if !okay { + invalid = append(invalid, "'ear.veraison.processed-evidence'") + } + o.VeraisonProcessedEvidence = &processedEvidence + } + + v, ok = m["ear.veraison.verifier-added-claims"] + if ok { + addedClaims, okay := v.(map[string]interface{}) + if !okay { + invalid = append(invalid, "'ear.veraison.verifier-added-claims'") + } + o.VeraisonVerifierAddedClaims = &addedClaims + + } + + var problems []string + + if len(missing) > 0 { + msg := fmt.Sprintf("missing mandatory %s", strings.Join(missing, ", ")) + problems = append(problems, msg) + } + + if len(invalid) > 0 { + msg := fmt.Sprintf("invalid values(s) for %s", strings.Join(invalid, ", ")) + problems = append(problems, msg) + } + + if len(extra) > 0 { + msg := fmt.Sprintf("unexpected %s", strings.Join(extra, ", ")) + problems = append(problems, msg) + } + + if len(problems) > 0 { + return errors.New(strings.Join(problems, "; ")) + } + + return nil } diff --git a/ear_test.go b/ear_test.go index ac1b967..31b3880 100644 --- a/ear_test.go +++ b/ear_test.go @@ -5,6 +5,7 @@ package ear import ( "fmt" + "math" "testing" "github.com/lestrrat-go/jwx/v2/jwa" @@ -163,6 +164,52 @@ func TestTrustTier_UnmarshalJSON_fail(t *testing.T) { } } +func TestTrustTier_ToTrustTier(t *testing.T) { + var tt *TrustTier + var err error + + tt, err = ToTrustTier(2) + require.NoError(t, err) + assert.Equal(t, TrustTierAffirming, *tt) + + tt, err = ToTrustTier(2.5) + require.NoError(t, err) + assert.Equal(t, TrustTierAffirming, *tt) + + _, err = ToTrustTier(3.1) + assert.ErrorContains(t, err, "not a valid TrustTier value: 3.100000 (3)") + + _, err = ToTrustTier(math.MaxFloat32) + assert.ErrorContains(t, err, "not a valid TrustTier value: 34028234") + + tt, err = ToTrustTier(int8(32)) + require.NoError(t, err) + assert.Equal(t, TrustTierWarning, *tt) + + _, err = ToTrustTier(uint64(math.MaxUint64)) + assert.ErrorContains(t, err, + fmt.Sprintf("not a valid TrustTier value: %d", uint64(math.MaxUint64))) + + tt, err = ToTrustTier("affirming") + require.NoError(t, err) + assert.Equal(t, TrustTierAffirming, *tt) + + tt, err = ToTrustTier("96") + require.NoError(t, err) + assert.Equal(t, TrustTierContraindicated, *tt) + + tt, err = ToTrustTier([]byte{0x33, 0x32}) // "32" + require.NoError(t, err) + assert.Equal(t, TrustTierWarning, *tt) + + _, err = ToTrustTier("totally safe") + assert.ErrorContains(t, err, `not a valid TrustTier name: "totally safe"`) + + tt, err = ToTrustTier(UnrecognizedHardwareClaim) + require.NoError(t, err) + assert.Equal(t, TrustTierContraindicated, *tt) +} + func TestToJSON_fail(t *testing.T) { tvs := []struct { ar AttestationResult @@ -199,12 +246,12 @@ func TestToJSON_fail(t *testing.T) { }} for i, tv := range tvs { - _, err := tv.ar.ToJSON() + _, err := tv.ar.MarshalJSON() assert.EqualError(t, err, tv.expected, "failed test vector at index %d", i) } } -func TestFromJSON_fail(t *testing.T) { +func TestUnmarshalJSON_fail(t *testing.T) { tvs := []struct { ar string expected string @@ -215,18 +262,18 @@ func TestFromJSON_fail(t *testing.T) { }, { ar: `[]`, - expected: `json: cannot unmarshal array into Go value of type ear.AttestationResult`, + expected: `json: cannot unmarshal array into Go value of type map[string]interface {}`, }, { ar: `{}`, - expected: `missing mandatory 'eat_profile', 'status', 'iat'`, + expected: `missing mandatory 'ear.status', 'eat_profile', 'iat'`, }, } for i, tv := range tvs { var ar AttestationResult - err := ar.FromJSON([]byte(tv.ar)) + err := ar.UnmarshalJSON([]byte(tv.ar)) assert.EqualError(t, err, tv.expected, "failed test vector at index %d", i) } } @@ -235,20 +282,22 @@ func TestVerify_pass(t *testing.T) { tvs := []string{ // ok `eyJhbGciOiJFUzI1NiJ9.eyJlYXIuc3RhdHVzIjoiYWZmaXJtaW5nIiwiZWF0X3Byb2ZpbGUiOiJ0YWc6Z2l0aHViLmNvbSwyMDIyOnZlcmFpc29uL2VhciIsImlhdCI6MTY2NjA5MTM3MywiZWFyLmFwcHJhaXNhbC1wb2xpY3ktaWQiOiJodHRwczovL3ZlcmFpc29uLmV4YW1wbGUvcG9saWN5LzEvNjBhMDA2OGQiLCJlYXIudmVyYWlzb24ucHJvY2Vzc2VkLWV2aWRlbmNlIjp7ImsxIjoidjEiLCJrMiI6InYyIn0sImVhci52ZXJhaXNvbi52ZXJpZmllci1hZGRlZC1jbGFpbXMiOnsiYmFyIjoiYmF6IiwiZm9vIjoiYmFyIn19.P0yB2s_DmCQ7DSX2pOnyKbNMVCfTrqkxohWrDxwBdKqOMrrXoCYJmWlpgwtHV-AA56NXMRObeZk9zT_0TlPgpQ`, - // ok with trailing stuff (ignored) - `eyJhbGciOiJFUzI1NiJ9.eyJlYXIuc3RhdHVzIjoiYWZmaXJtaW5nIiwiZWF0X3Byb2ZpbGUiOiJ0YWc6Z2l0aHViLmNvbSwyMDIyOnZlcmFpc29uL2VhciIsImlhdCI6MTY2NjA5MTM3MywiZWFyLmFwcHJhaXNhbC1wb2xpY3ktaWQiOiJodHRwczovL3ZlcmFpc29uLmV4YW1wbGUvcG9saWN5LzEvNjBhMDA2OGQiLCJlYXIudmVyYWlzb24ucHJvY2Vzc2VkLWV2aWRlbmNlIjp7ImsxIjoidjEiLCJrMiI6InYyIn0sImVhci52ZXJhaXNvbi52ZXJpZmllci1hZGRlZC1jbGFpbXMiOnsiYmFyIjoiYmF6IiwiZm9vIjoiYmFyIn19.P0yB2s_DmCQ7DSX2pOnyKbNMVCfTrqkxohWrDxwBdKqOMrrXoCYJmWlpgwtHV-AA56NXMRObeZk9zT_0TlPgpQ.trailing-rubbish-is-ignored`, + // trailing stuff means the format is no longer valid. + `eyJhbGciOiJFUzI1NiJ9.eyJlYXIuc3RhdHVzIjoiYWZmaXJtaW5nIiwiZWF0X3Byb2ZpbGUiOiJ0YWc6Z2l0aHViLmNvbSwyMDIyOnZlcmFpc29uL2VhciIsImlhdCI6MTY2NjA5MTM3MywiZWFyLmFwcHJhaXNhbC1wb2xpY3ktaWQiOiJodHRwczovL3ZlcmFpc29uLmV4YW1wbGUvcG9saWN5LzEvNjBhMDA2OGQiLCJlYXIudmVyYWlzb24ucHJvY2Vzc2VkLWV2aWRlbmNlIjp7ImsxIjoidjEiLCJrMiI6InYyIn0sImVhci52ZXJhaXNvbi52ZXJpZmllci1hZGRlZC1jbGFpbXMiOnsiYmFyIjoiYmF6IiwiZm9vIjoiYmFyIn19.P0yB2s_DmCQ7DSX2pOnyKbNMVCfTrqkxohWrDxwBdKqOMrrXoCYJmWlpgwtHV-AA56NXMRObeZk9zT_0TlPgpQ.trailing-rubbish`, } k, err := jwk.ParseKey([]byte(testECDSAPublicKey)) require.NoError(t, err) - for _, tv := range tvs { - var ar AttestationResult + var ar AttestationResult - err := ar.Verify([]byte(tv), jwa.ES256, k) - assert.NoError(t, err) - assert.Equal(t, testAttestationResultsWithVeraisonExtns, ar) - } + err = ar.Verify([]byte(tvs[0]), jwa.ES256, k) + assert.NoError(t, err) + assert.Equal(t, testAttestationResultsWithVeraisonExtns, ar) + + var ar2 AttestationResult + err = ar2.Verify([]byte(tvs[1]), jwa.ES256, k) + assert.ErrorContains(t, err, "failed to parse token: invalid character 'e' looking for beginning of value") } func TestVerify_fail(t *testing.T) { @@ -274,7 +323,7 @@ func TestVerify_fail(t *testing.T) { { // empty attestation results token: `eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.e30.9Tvx3hVBNfkmVXTndrVfv9ZeNJgX59w0JpR2vyjUn8lGxL8VT7OggUeYSYFnxrouSi2TusNh61z8rLdOqxGA-A`, - expected: `failed parsing JWT payload: missing mandatory 'eat_profile', 'status', 'iat'`, + expected: `missing mandatory 'ear.status', 'eat_profile'`, }, } @@ -285,7 +334,7 @@ func TestVerify_fail(t *testing.T) { var ar AttestationResult err := ar.Verify([]byte(tv.token), jwa.ES256, k) - assert.EqualError(t, err, tv.expected, "failed test vector at index %d", i) + assert.ErrorContains(t, err, tv.expected, "failed test vector at index %d", i) } } @@ -341,3 +390,91 @@ func TestRoundTrip_tampering(t *testing.T) { err = actual.Verify(token, jwa.ES256, vfyK) assert.ErrorContains(t, err, "failed verifying JWT message") } + +func TestUpdateStatusFromTrustVector(t *testing.T) { + ar := NewAttestationResult() + + ar.UpdateStatusFromTrustVector() + assert.Equal(t, TrustTierNone, *ar.Status) + + ar.TrustVector.Configuration = ApprovedConfigClaim + ar.UpdateStatusFromTrustVector() + assert.Equal(t, TrustTierAffirming, *ar.Status) + + *ar.Status = TrustTierWarning + ar.UpdateStatusFromTrustVector() + assert.Equal(t, TrustTierWarning, *ar.Status) + + ar.TrustVector.Configuration = UnsupportableConfigClaim + ar.UpdateStatusFromTrustVector() + assert.Equal(t, TrustTierContraindicated, *ar.Status) +} + +func TestAsMap(t *testing.T) { + policyID := "foo" + + ar := NewAttestationResult() + status := NewTrustTier(TrustTierAffirming) + ar.Status = status + ar.TrustVector.Executables = ApprovedRuntimeClaim + ar.AppraisalPolicyID = &policyID + + expected := map[string]interface{}{ + "ear.status": *status, + "ear.trustworthiness-vector": map[string]TrustClaim{ + "instance-identity": NoClaim, + "configuration": NoClaim, + "executables": ApprovedRuntimeClaim, + "file-system": NoClaim, + "hardware": NoClaim, + "runtime-opaque": NoClaim, + "storage-opaque": NoClaim, + "sourced-data": NoClaim, + }, + "ear.appraisal-policy-id": "foo", + "eat_profile": EatProfile, + } + + m := ar.AsMap() + for _, field := range []string{ + "ear.status", + "ear.trustworthiness-vector", + "eat_profile", + "ear.appraisal-policy-id", + } { + assert.Equal(t, expected[field], m[field]) + } +} + +func Test_populateFromMap(t *testing.T) { + var ar AttestationResult + m := map[string]interface{}{ + "ear.status": 2, + "ear.trustworthiness-vector": map[string]interface{}{ + "instance-identity": 0, + "configuration": 0, + "executables": 2, + "file-system": 0, + "hardware": 0, + "runtime-opaque": 0, + "storage-opaque": 0, + "sourced-data": 0, + }, + "ear.raw-evidence": "SSBkaWRuJ3QgZG8gaXQ=", + "ear.appraisal-policy-id": "foo", + "iat": 1234, + "eat_profile": EatProfile, + } + + err := ar.populateFromMap(m) + assert.NoError(t, err) + assert.Equal(t, TrustTierAffirming, *ar.Status) + assert.Equal(t, EatProfile, *ar.Profile) +} + +func TestTrustTier_ColorString(t *testing.T) { + assert.Equal(t, "\\033[47mnone\\033[0m", TrustTierNone.ColorString()) + assert.Equal(t, "\\033[42maffirming\\033[0m", TrustTierAffirming.ColorString()) + assert.Equal(t, "\\033[43mwarning\\033[0m", TrustTierWarning.ColorString()) + assert.Equal(t, "\\033[41mcontraindicated\\033[0m", TrustTierContraindicated.ColorString()) +} diff --git a/example_test.go b/example_test.go index 3de1e31..33bbd2b 100644 --- a/example_test.go +++ b/example_test.go @@ -16,12 +16,12 @@ func Example_encode_minimalist() { Profile: &testProfile, } - j, _ := ar.ToJSON() + j, _ := ar.MarshalJSON() fmt.Println(string(j)) // Output: - // {"ear.status":"affirming","eat_profile":"tag:github.com,2022:veraison/ear","iat":1666091373,"ear.appraisal-policy-id":"https://veraison.example/policy/1/60a0068d"} + // {"ear.appraisal-policy-id":"https://veraison.example/policy/1/60a0068d","ear.status":"affirming","eat_profile":"tag:github.com,2022:veraison/ear","iat":1666091373} } func Example_encode_hefty() { @@ -45,23 +45,23 @@ func Example_encode_hefty() { Profile: &testProfile, } - j, _ := ar.ToJSON() + j, _ := ar.MarshalJSON() fmt.Println(string(j)) // Output: - // {"ear.status":"affirming","eat_profile":"tag:github.com,2022:veraison/ear","ear.trustworthiness-vector":{"instance-identity":2,"configuration":2,"executables":3,"file-system":2,"hardware":2,"runtime-opaque":2,"storage-opaque":2,"sourced-data":2},"ear.raw-evidence":"3q2+7w==","iat":1666091373,"ear.appraisal-policy-id":"https://veraison.example/policy/1/60a0068d"} + // {"ear.appraisal-policy-id":"https://veraison.example/policy/1/60a0068d","ear.raw-evidence":"3q2+7w==","ear.status":"affirming","ear.trustworthiness-vector":{"configuration":2,"executables":3,"file-system":2,"hardware":2,"instance-identity":2,"runtime-opaque":2,"sourced-data":2,"storage-opaque":2},"eat_profile":"tag:github.com,2022:veraison/ear","iat":1666091373} } func Example_encode_veraison_extensions() { ar := testAttestationResultsWithVeraisonExtns - j, _ := ar.ToJSON() + j, _ := ar.MarshalJSON() fmt.Println(string(j)) // Output: - // {"ear.status":"affirming","eat_profile":"tag:github.com,2022:veraison/ear","iat":1666091373,"ear.appraisal-policy-id":"https://veraison.example/policy/1/60a0068d","ear.veraison.processed-evidence":{"k1":"v1","k2":"v2"},"ear.veraison.verifier-added-claims":{"bar":"baz","foo":"bar"}} + // {"ear.appraisal-policy-id":"https://veraison.example/policy/1/60a0068d","ear.status":"affirming","ear.veraison.processed-evidence":{"k1":"v1","k2":"v2"},"ear.veraison.verifier-added-claims":{"bar":"baz","foo":"bar"},"eat_profile":"tag:github.com,2022:veraison/ear","iat":1666091373} } func Example_decode_veraison_extensions() { @@ -80,9 +80,9 @@ func Example_decode_veraison_extensions() { "eat_profile": "tag:github.com,2022:veraison/ear" }` var ar AttestationResult - _ = ar.FromJSON([]byte(j)) + _ = ar.UnmarshalJSON([]byte(j)) - fmt.Println(StatusTierToString[*ar.Status]) + fmt.Println(TrustTierToString[*ar.Status]) fmt.Println((*ar.VeraisonProcessedEvidence)["k1"]) fmt.Println((*ar.VeraisonVerifierAddedClaims)["bar"]) @@ -107,7 +107,7 @@ func Example_colors() { }` var ar AttestationResult - _ = ar.FromJSON([]byte(j)) + _ = ar.UnmarshalJSON([]byte(j)) short, color := true, true diff --git a/go.mod b/go.mod index b8e446d..858d40f 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/veraison/ear go 1.18 require ( + github.com/huandu/xstrings v1.3.3 github.com/lestrrat-go/jwx/v2 v2.0.6 github.com/spf13/afero v1.9.2 github.com/spf13/cobra v1.6.1 diff --git a/go.sum b/go.sum index b5ca5bb..99254ae 100644 --- a/go.sum +++ b/go.sum @@ -126,6 +126,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= diff --git a/tclaim_test.go b/tclaim_test.go deleted file mode 100644 index 8909d99..0000000 --- a/tclaim_test.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2022 Contributors to the Veraison project. -// SPDX-License-Identifier: Apache-2.0 - -package ear - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -var ( - ranges = map[string][]int{ - "none": { - -1, 0, 1, - }, - "affirming": { - // negative - -32, -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, - -19, -18, -17, -16, -15, -14, -13, -12, -11, -10, -9, -8, -7, -6, - -5, -4, -3, -2, - // positive - 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, - 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, - }, - "warning": { - // negative - -96, -95, -94, -93, -92, -91, -90, -89, -88, -87, -86, -85, -84, - -83, -82, -81, -80, -79, -78, -77, -76, -75, -74, -73, -72, -71, - -70, -69, -68, -67, -66, -65, -64, -63, -62, -61, -60, -59, -58, - -57, -56, -55, -54, -53, -52, -51, -50, -49, -48, -47, -46, -45, - -44, -43, -42, -41, -40, -39, -38, -37, -36, -35, -34, -33, - // positive - 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, - 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, - 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, - 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, - }, - "contraindicated": { - // negative - -128, -127, -126, -125, -124, -123, -122, -121, -120, -119, -118, - -117, -116, -115, -114, -113, -112, -111, -110, -109, -108, -107, - -106, -105, -104, -103, -102, -101, -100, -99, -98, -97, - // positive - 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, - 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, - 123, 124, 125, 126, 127, - }, - } -) - -func TestTClaim_TrustTier_entire_range(t *testing.T) { - for s, a := range ranges { - for _, i := range a { - color := false - assert.Equal(t, s, TClaim(i).TrustTier(color), "enum: %d", i) - } - } -} diff --git a/tclaim.go b/trustclaim.go similarity index 55% rename from tclaim.go rename to trustclaim.go index 0166b58..6f70977 100644 --- a/tclaim.go +++ b/trustclaim.go @@ -3,23 +3,97 @@ package ear -import "fmt" +import ( + "encoding/json" + "fmt" + "math" + "strconv" + "strings" + + "github.com/huandu/xstrings" +) // trustworthiness claim -type TClaim int8 +type TrustClaim int8 +// Description of a particular claim +// tag: an itentifier-compantible label to be used in serialized values as an +// +// alternative to integers. +// +// short: a short description for embedding in error messages, etc +// long: a longer, more descriptive explanation of the claim. type details struct { - short, long string + tag, short, long string } -type detailsMap map[TClaim]details +type detailsMap map[TrustClaim]details + +const ( + // See details definitions below for detailed claim value interpretations. + + // general + VerifierMalfunctionClaim = TrustClaim(-1) + NoClaim = TrustClaim(0) + UnexpectedEvidenceClaim = TrustClaim(1) + CryptoValidationFailedClaim = TrustClaim(99) + + // instance identity + TrustworthyInstanceClaim = TrustClaim(2) + UntrustworthyInstanceClaim = TrustClaim(96) + UnrecognizedInstanceClaim = TrustClaim(97) + + // config + ApprovedConfigClaim = TrustClaim(2) + NoConfigVulnsClaim = TrustClaim(3) + UnsafeConfigClaim = TrustClaim(32) + UnsupportableConfigClaim = TrustClaim(96) + + // executables & runtime + ApprovedRuntimeClaim = TrustClaim(2) + ApprovedBootClaim = TrustClaim(3) + UnsafeRuntimeClaim = TrustClaim(32) + UnrecognizedRuntimeClaim = TrustClaim(33) + ContraindicatedRuntimeClaim = TrustClaim(96) + + // file system + ApprovedFilesClaim = TrustClaim(2) + UnrecognizedFilesClaim = TrustClaim(32) + ContraindicatedFilesClaim = TrustClaim(96) + + // hardware + GenuineHardwareClaim = TrustClaim(2) + UnsafeHardwareClaim = TrustClaim(32) + ContraindicatedHardwareClaim = TrustClaim(96) + UnrecognizedHardwareClaim = TrustClaim(97) + + // opaque runtime + EncryptedMemoryRuntimeClaim = TrustClaim(2) + IsolatedMemoryRuntimeClaim = TrustClaim(32) + VisibleMemoryRuntimeClaim = TrustClaim(96) + + // opaque storage + HwKeysEncryptedSecretsClaim = TrustClaim(2) + SwKeysEncryptedSecretsClaim = TrustClaim(32) + UnencryptedSecretsClaim = TrustClaim(96) + + // sourced data + TrustedSourcesClaim = TrustClaim(2) + UntrustedSourcesClaim = TrustClaim(32) + ContraindicatedSourcesClaim = TrustClaim(96) +) var ( + // NOTE: tags are used when converting strings to claims. In order for + // this work, there must be an unabigous mapping between them and + // claims' integer values. It is OK of mulple claims to have the same + // tag, as long as their integer values are also the same. noneDetails = detailsMap{ // Value -1: A verifier malfunction occurred during the Verifier's // appraisal processing. // NOTE: similar to HTTP 5xx (server error) - -1: { + VerifierMalfunctionClaim: { + tag: "verifier_malfunction", short: "verifier malfunction", long: "A verifier malfunction occurred during the Verifier's appraisal processing.", }, @@ -30,7 +104,8 @@ var ( // Trustworthiness Claim with enumeration '0', and no Trustworthiness // Claim being provided. // NOTE: not sure why this is grouped with -1 and 1. - 0: { + NoClaim: { + tag: "no_claim", short: "no claim being made", long: "The Evidence received is insufficient to make a conclusion.", }, @@ -38,7 +113,8 @@ var ( // Verifier is unable to parse. An example might be that the wrong type // of Evidence has been delivered. // NOTE: similar to HTTP 4xx (client error) - 1: { + UnexpectedEvidenceClaim: { + tag: "unexected_evidence", short: "unexpected evidence", long: "The Evidence received contains unexpected elements which the Verifier is unable to parse.", }, @@ -49,19 +125,23 @@ var ( // should only be generated if the Verifier actually expects to recognize // the unique identity of the Attester.) instanceIdentityDetails = detailsMap{ - 2: { + TrustworthyInstanceClaim: { + tag: "recognized_instance", short: "recognized and not compromised", long: "The Attesting Environment is recognized, and the associated instance of the Attester is not known to be compromised.", }, - 96: { + UntrustworthyInstanceClaim: { + tag: "untrustworthy_instance", short: "recognized but not trustworthy", long: "The Attesting Environment is recognized, but its unique private key indicates a device which is not trustworthy.", }, - 97: { + UnrecognizedInstanceClaim: { + tag: "unrecognized_instance", short: "not recognized", long: "The Attesting Environment is not recognized; however the Verifier believes it should be.", }, - 99: { + CryptoValidationFailedClaim: { + tag: "crypto_failed", short: "cryptographic validation failed", long: "Cryptographic validation of the Evidence has failed.", }, @@ -69,23 +149,28 @@ var ( // A Verifier has appraised an Attester's configuration, and is able to make // conclusions regarding the exposure of known vulnerabilities. configurationDetails = detailsMap{ - 2: { + ApprovedConfigClaim: { + tag: "approved_config", short: "all recognized and approved", long: "The configuration is a known and approved config.", }, - 3: { + NoConfigVulnsClaim: { + tag: "safe_config", short: "no known vulnerabilities", long: "The configuration includes or exposes no known vulnerabilities", }, - 32: { + UnsafeConfigClaim: { + tag: "unsafe_config", short: "known vulnerabilities", long: "The configuration includes or exposes known vulnerabilities.", }, - 96: { + UnsupportableConfigClaim: { + tag: "unsupportable_config", short: "unacceptable security vulnerabilities", long: "The configuration is unsupportable as it exposes unacceptable security vulnerabilities", }, - 99: { + CryptoValidationFailedClaim: { + tag: "crypto_failed", short: "cryptographic validation failed", long: "Cryptographic validation of the Evidence has failed.", }, @@ -94,27 +179,33 @@ var ( // and/or other objects which have been loaded into the Target environment's // memory. executablesDetails = detailsMap{ - 2: { + ApprovedRuntimeClaim: { + tag: "approved_rt", short: "recognized and approved boot- and run-time", long: "Only a recognized genuine set of approved executables, scripts, files, and/or objects have been loaded during and after the boot process.", }, - 3: { + ApprovedBootClaim: { + tag: "approved_boot", short: "recognized and approved boot-time", long: "Only a recognized genuine set of approved executables have been loaded during the boot process.", }, - 32: { + UnsafeRuntimeClaim: { + tag: "unsafe_rt", short: "recognized but known bugs or vulnerabilities", long: "Only a recognized genuine set of executables, scripts, files, and/or objects have been loaded. However the Verifier cannot vouch for a subset of these due to known bugs or other known vulnerabilities.", }, - 33: { + UnrecognizedRuntimeClaim: { + tag: "unrecognized_rt", short: "unrecognized run-time", long: "Runtime memory includes executables, scripts, files, and/or objects which are not recognized.", }, - 96: { + ContraindicatedRuntimeClaim: { + tag: "contraindicated_rt", short: "contraindicated run-time", long: "Runtime memory includes executables, scripts, files, and/or object which are contraindicated.", }, - 99: { + CryptoValidationFailedClaim: { + tag: "crypto_failed", short: "cryptographic validation failed", long: "Cryptographic validation of the Evidence has failed.", }, @@ -124,19 +215,23 @@ var ( // these directory and expected files are via an unspecified management // interface.) fileSystemDetails = detailsMap{ - 2: { + ApprovedFilesClaim: { + tag: "approved_fs", short: "all recognized and approved", long: "Only a recognized set of approved files are found.", }, - 32: { + UnrecognizedFilesClaim: { + tag: "unrecognized_fs", short: "unrecognized item(s) found", long: "The file system includes unrecognized executables, scripts, or files.", }, - 96: { + ContraindicatedFilesClaim: { + tag: "contraindicated_fs", short: "contraindicated item(s) found", long: "The file system includes contraindicated executables, scripts, or files.", }, - 99: { + CryptoValidationFailedClaim: { + tag: "crypto_failed", short: "cryptographic validation failed", long: "Cryptographic validation of the Evidence has failed.", }, @@ -144,23 +239,28 @@ var ( // A Verifier has appraised any Attester hardware and firmware which are // able to expose fingerprints of their identity and running code. hardwareDetails = detailsMap{ - 2: { + GenuineHardwareClaim: { + tag: "genuine_hw", short: "genuine", long: "An Attester has passed its hardware and/or firmware verifications needed to demonstrate that these are genuine/supported.", }, - 32: { + UnsafeHardwareClaim: { + tag: "unsafe_hw", short: "genuine but known bugs or vulnerabilities", long: "An Attester contains only genuine/supported hardware and/or firmware, but there are known security vulnerabilities.", }, - 96: { + ContraindicatedHardwareClaim: { + tag: "contraindicated_hw", short: "genuine but contraindicated", long: "Attester hardware and/or firmware is recognized, but its trustworthiness is contraindicated.", }, - 97: { + UnrecognizedHardwareClaim: { + tag: "unrecognized_hw", short: "unrecognized", long: "A Verifier does not recognize an Attester's hardware or firmware, but it should be recognized.", }, - 99: { + CryptoValidationFailedClaim: { + tag: "crypto_failed", short: "cryptographic validation failed", long: "Cryptographic validation of the Evidence has failed.", }, @@ -168,20 +268,24 @@ var ( // A Verifier has appraised the visibility of Attester objects in memory // from perspectives outside the Attester. runtimeOpaqueDetails = detailsMap{ - 2: { + EncryptedMemoryRuntimeClaim: { + tag: "encrypted_rt", short: "memory encryption", long: "the Attester's executing Target Environment and Attesting Environments are encrypted and within Trusted Execution Environment(s) opaque to the operating system, virtual machine manager, and peer applications.", }, - 32: { + IsolatedMemoryRuntimeClaim: { // TODO(tho) not sure about the shorthand + tag: "isolated_rt", short: "memory isolation", long: "the Attester's executing Target Environment and Attesting Environments are inaccessible from any other parallel application or Guest VM running on the Attester's physical device.", }, - 96: { + VisibleMemoryRuntimeClaim: { + tag: "visible_rt", short: "visible", long: "The Verifier has concluded that in memory objects are unacceptably visible within the physical host that supports the Attester.", }, - 99: { + CryptoValidationFailedClaim: { + tag: "crypto_failed", short: "cryptographic validation failed", long: "Cryptographic validation of the Evidence has failed.", }, @@ -189,19 +293,23 @@ var ( // A Verifier has appraised that an Attester is capable of encrypting // persistent storage. storageOpaqueDetails = detailsMap{ - 2: { + HwKeysEncryptedSecretsClaim: { + tag: "hw_encrypted_secrets", short: "encrypted secrets with HW-backed keys", long: "the Attester encrypts all secrets in persistent storage via using keys which are never visible outside an HSM or the Trusted Execution Environment hardware.", }, - 32: { + SwKeysEncryptedSecretsClaim: { + tag: "sw_encrypted_secrets", short: "encrypted secrets with non HW-backed keys", long: "the Attester encrypts all persistently stored secrets, but without using hardware backed keys.", }, - 96: { + UnencryptedSecretsClaim: { + tag: "unencrypted_secrets", short: "unencrypted secrets", long: "There are persistent secrets which are stored unencrypted in an Attester.", }, - 99: { + CryptoValidationFailedClaim: { + tag: "crypto_failed", short: "cryptographic validation failed", long: "Cryptographic validation of the Evidence has failed.", }, @@ -209,90 +317,166 @@ var ( // A Verifier has evaluated the integrity of data objects from external // systems used by the Attester. sourcedDataDetails = detailsMap{ - 2: { + TrustedSourcesClaim: { + tag: "trusted_sources", short: "from attesters in the affirming tier", long: `All essential Attester source data objects have been provided by other Attester(s) whose most recent appraisal(s) had both no Trustworthiness Claims of "0" where the current Trustworthiness Claim is "Affirming", as well as no "Warning" or "Contraindicated" Trustworthiness Claims.`, }, - 32: { + UntrustedSourcesClaim: { + tag: "untrusted_sources", short: "from unattested sources or attesters in the warning tier", long: `Attester source data objects come from unattested sources, or attested sources with "Warning" type Trustworthiness Claims`, }, - 96: { + ContraindicatedSourcesClaim: { + tag: "contraindicated_sources", short: "from attesters in the contraindicated tier", long: "Attester source data objects come from contraindicated sources.", }, - 99: { + CryptoValidationFailedClaim: { + tag: "crypto_failed", short: "cryptographic validation failed", long: "Cryptographic validation of the Evidence has failed.", }, } ) -// TrustTier provides the trust tier bucket of the trustworthiness claim -func (o TClaim) TrustTier(color bool) string { - const ( - rst = `\033[0m` - red = `\033[41m` - yellow = `\033[43m` - green = `\033[42m` - white = `\033[47m` - ) +func getTrustClaimFromInt(i int) (TrustClaim, error) { + if i > 127 || i < -128 { + return NoClaim, fmt.Errorf("out of range for TrustClaim: %d", i) + } + return TrustClaim(i), nil +} - var s string +func getTrustClaimFromString(s string) (TrustClaim, error) { + i, err := strconv.Atoi(s) + if err == nil { + return getTrustClaimFromInt(i) + } + + detailsMaps := []detailsMap{ + configurationDetails, + executablesDetails, + fileSystemDetails, + hardwareDetails, + instanceIdentityDetails, + noneDetails, + runtimeOpaqueDetails, + sourcedDataDetails, + storageOpaqueDetails, + } - switch { - case o.IsNone(): - s = "none" - if color { - s = white + s + rst + canon := strings.Trim(xstrings.Translate(xstrings.ToSnakeCase(s), ".- ", "_"), " \t") + + for _, dm := range detailsMaps { + for claim, deets := range dm { + if deets.tag == canon { + return claim, nil + } } - case o.IsAffirming(): - s = "affirming" - if color { - s = green + s + rst + } + + return NoClaim, fmt.Errorf("not a valid TrustClaim value: %q", s) +} + +func ToTrustClaim(v interface{}) (*TrustClaim, error) { + var ( + claim TrustClaim + err error + ) + + switch t := v.(type) { + case TrustClaim: + claim = t + case *TrustClaim: + claim = *t + case json.Number: + i, e := t.Int64() + if e != nil { + err = fmt.Errorf("not a valid TrustClaim value: %v: %w", t, err) + } else { + claim, err = getTrustClaimFromInt(int(i)) } - case o.IsWarning(): - s = "warning" - if color { - s = yellow + s + rst + case string: + claim, err = getTrustClaimFromString(t) + case []byte: + claim, err = getTrustClaimFromString(string(t)) + case int: + claim, err = getTrustClaimFromInt(t) + case int8: + claim, err = getTrustClaimFromInt(int(t)) + case int16: + claim, err = getTrustClaimFromInt(int(t)) + case int32: + claim, err = getTrustClaimFromInt(int(t)) + case int64: + claim, err = getTrustClaimFromInt(int(t)) + case uint8: + claim, err = getTrustClaimFromInt(int(t)) + case uint16: + claim, err = getTrustClaimFromInt(int(t)) + case uint32: + claim, err = getTrustClaimFromInt(int(t)) + case uint: + if t > math.MaxInt64 { + err = fmt.Errorf("not a valid TrustClaim value: %d", t) + + } else { + claim, err = getTrustClaimFromInt(int(t)) } - case o.IsContraindicated(): - s = "contraindicated" - if color { - s = red + s + rst + case uint64: + if t > math.MaxInt64 { + err = fmt.Errorf("not a valid TrustClaim value: %d", t) + + } else { + claim, err = getTrustClaimFromInt(int(t)) } - default: - panic("unreachable") + case float64: + claim, err = getTrustClaimFromInt(int(t)) } - return s + return &claim, err +} + +// TrustTier provides the trust tier bucket of the trustworthiness claim +func (o TrustClaim) GetTier() TrustTier { + if o.IsNone() { + return TrustTierNone + } else if o.IsAffirming() { + return TrustTierAffirming + } else if o.IsWarning() { + return TrustTierWarning + } else if o.IsContraindicated() { + return TrustTierContraindicated + } else { + panic(o) // should never get here -- above conditions exhaust int8 range + } } -func (o TClaim) trustTierTag(color bool) string { - return "[" + o.TrustTier(color) + "]" +func (o TrustClaim) trustTierTag(color bool) string { + return "[" + o.GetTier().Format(color) + "]" } -func (o TClaim) IsNone() bool { +func (o TrustClaim) IsNone() bool { // none = [-1, 1] return o >= -1 && o <= 1 } -func (o TClaim) IsAffirming() bool { +func (o TrustClaim) IsAffirming() bool { // affirming = [-32, -2] U [2, 31] return (o >= -32 && o <= -2) || (o >= 2 && o <= 31) } -func (o TClaim) IsWarning() bool { +func (o TrustClaim) IsWarning() bool { // warning = [-96, -33] U [32, 95] return (o >= -96 && o <= -33) || (o >= 32 && o <= 95) } -func (o TClaim) IsContraindicated() bool { +func (o TrustClaim) IsContraindicated() bool { // contraindicated = [-128, -97] U [96, 127] return (o >= -128 && o <= -97) || (o >= 96 && o <= 127) } -func (o TClaim) detailsPrinter(dm detailsMap, short bool, color bool) string { +func (o TrustClaim) detailsPrinter(dm detailsMap, short bool, color bool) string { // "none" statuses have shared semantics if o.IsNone() { return noneToString(o, short, color) @@ -312,39 +496,39 @@ func (o TClaim) detailsPrinter(dm detailsMap, short bool, color bool) string { return s.long } -func (o TClaim) asInstanceIdentityDetails(short, color bool) string { +func (o TrustClaim) asInstanceIdentityDetails(short, color bool) string { return o.detailsPrinter(instanceIdentityDetails, short, color) } -func (o TClaim) asConfigurationDetails(short, color bool) string { +func (o TrustClaim) asConfigurationDetails(short, color bool) string { return o.detailsPrinter(configurationDetails, short, color) } -func (o TClaim) asExecutablesDetails(short, color bool) string { +func (o TrustClaim) asExecutablesDetails(short, color bool) string { return o.detailsPrinter(executablesDetails, short, color) } -func (o TClaim) asFileSystemDetails(short, color bool) string { +func (o TrustClaim) asFileSystemDetails(short, color bool) string { return o.detailsPrinter(fileSystemDetails, short, color) } -func (o TClaim) asHardwareDetails(short, color bool) string { +func (o TrustClaim) asHardwareDetails(short, color bool) string { return o.detailsPrinter(hardwareDetails, short, color) } -func (o TClaim) asRuntimeOpaqueDetails(short, color bool) string { +func (o TrustClaim) asRuntimeOpaqueDetails(short, color bool) string { return o.detailsPrinter(runtimeOpaqueDetails, short, color) } -func (o TClaim) asStorageOpaqueDetails(short, color bool) string { +func (o TrustClaim) asStorageOpaqueDetails(short, color bool) string { return o.detailsPrinter(storageOpaqueDetails, short, color) } -func (o TClaim) asSourcedDataDetails(short, color bool) string { +func (o TrustClaim) asSourcedDataDetails(short, color bool) string { return o.detailsPrinter(sourcedDataDetails, short, color) } -func noneToString(tc TClaim, short, color bool) string { +func noneToString(tc TrustClaim, short, color bool) string { s, ok := noneDetails[tc] if ok { if short { diff --git a/trustclaim_test.go b/trustclaim_test.go new file mode 100644 index 0000000..7a66277 --- /dev/null +++ b/trustclaim_test.go @@ -0,0 +1,116 @@ +// Copyright 2022 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 + +package ear + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + ranges = map[string][]int{ + "none": { + -1, 0, 1, + }, + "affirming": { + // negative + -32, -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, + -19, -18, -17, -16, -15, -14, -13, -12, -11, -10, -9, -8, -7, -6, + -5, -4, -3, -2, + // positive + 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + }, + "warning": { + // negative + -96, -95, -94, -93, -92, -91, -90, -89, -88, -87, -86, -85, -84, + -83, -82, -81, -80, -79, -78, -77, -76, -75, -74, -73, -72, -71, + -70, -69, -68, -67, -66, -65, -64, -63, -62, -61, -60, -59, -58, + -57, -56, -55, -54, -53, -52, -51, -50, -49, -48, -47, -46, -45, + -44, -43, -42, -41, -40, -39, -38, -37, -36, -35, -34, -33, + // positive + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, + 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, + 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, + 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, + }, + "contraindicated": { + // negative + -128, -127, -126, -125, -124, -123, -122, -121, -120, -119, -118, + -117, -116, -115, -114, -113, -112, -111, -110, -109, -108, -107, + -106, -105, -104, -103, -102, -101, -100, -99, -98, -97, + // positive + 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, + 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, + 123, 124, 125, 126, 127, + }, + } +) + +func TestTrustClaim_TrustTier_entire_range(t *testing.T) { + for s, a := range ranges { + for _, i := range a { + assert.Equal(t, s, TrustClaim(i).GetTier().String(), "enum: %d", i) + } + } +} + +func TestToTrustClaim(t *testing.T) { + v, err := ToTrustClaim(32) + require.NoError(t, err) + assert.Equal(t, UnsafeRuntimeClaim, *v) + + _, err = ToTrustClaim(512) + assert.ErrorContains(t, err, "out of range for TrustClaim: 512") + + v, err = ToTrustClaim("2") + require.NoError(t, err) + assert.Equal(t, TrustClaim(2), *v) + + v, err = ToTrustClaim("unsafe_hw") + require.NoError(t, err) + assert.Equal(t, TrustClaim(32), *v) + + v, err = ToTrustClaim("ApprovedFS") + require.NoError(t, err) + assert.Equal(t, ApprovedFilesClaim, *v) + + v, err = ToTrustClaim("CRYPTO-FAILED") + require.NoError(t, err) + assert.Equal(t, CryptoValidationFailedClaim, *v) + + v, err = ToTrustClaim("Trusted Sources") + require.NoError(t, err) + assert.Equal(t, TrustedSourcesClaim, *v) + + v, err = ToTrustClaim(TrustedSourcesClaim) + require.NoError(t, err) + assert.Equal(t, TrustedSourcesClaim, *v) + + tc := VerifierMalfunctionClaim + v, err = ToTrustClaim(&tc) + require.NoError(t, err) + assert.Equal(t, VerifierMalfunctionClaim, *v) + + n := json.Number("-1") + v, err = ToTrustClaim(n) + require.NoError(t, err) + assert.Equal(t, VerifierMalfunctionClaim, *v) + + _, err = ToTrustClaim("512") + assert.ErrorContains(t, err, "out of range for TrustClaim: 512") + + _, err = getTrustClaimFromString("Bogus Claim") + assert.ErrorContains(t, err, `not a valid TrustClaim value: "Bogus Claim"`) +} + +func TestTrustClaim_GetTier(t *testing.T) { + assert.Equal(t, TrustTierNone, VerifierMalfunctionClaim.GetTier()) + assert.Equal(t, TrustTierAffirming, ApprovedBootClaim.GetTier()) + assert.Equal(t, TrustTierWarning, UnsafeConfigClaim.GetTier()) + assert.Equal(t, TrustTierContraindicated, UnsupportableConfigClaim.GetTier()) +} diff --git a/trustvector.go b/trustvector.go index 68b8acf..31d0163 100644 --- a/trustvector.go +++ b/trustvector.go @@ -3,17 +3,148 @@ package ear +import ( + "fmt" + "strings" +) + // TrustVector is an implementation of the Trustworthiness Vector (and Claims) // described in §2.3 of draft-ietf-rats-ar4si-03, using a JSON serialization. type TrustVector struct { - InstanceIdentity TClaim `json:"instance-identity"` - Configuration TClaim `json:"configuration"` - Executables TClaim `json:"executables"` - FileSystem TClaim `json:"file-system"` - Hardware TClaim `json:"hardware"` - RuntimeOpaque TClaim `json:"runtime-opaque"` - StorageOpaque TClaim `json:"storage-opaque"` - SourcedData TClaim `json:"sourced-data"` + InstanceIdentity TrustClaim `json:"instance-identity"` + Configuration TrustClaim `json:"configuration"` + Executables TrustClaim `json:"executables"` + FileSystem TrustClaim `json:"file-system"` + Hardware TrustClaim `json:"hardware"` + RuntimeOpaque TrustClaim `json:"runtime-opaque"` + StorageOpaque TrustClaim `json:"storage-opaque"` + SourcedData TrustClaim `json:"sourced-data"` +} + +// AsMap() returns a map[string]TrustClaim with claims names mapped onto +// corresponding TrustClaim values. +func (o TrustVector) AsMap() map[string]TrustClaim { + return map[string]TrustClaim{ + "instance-identity": o.InstanceIdentity, + "configuration": o.Configuration, + "executables": o.Executables, + "file-system": o.FileSystem, + "hardware": o.Hardware, + "runtime-opaque": o.RuntimeOpaque, + "storage-opaque": o.StorageOpaque, + "sourced-data": o.SourcedData, + } +} + +func ToTrustVector(v interface{}) (*TrustVector, error) { + var ( + tv TrustVector + err error + ) + + switch t := v.(type) { + case TrustVector: + tv = t + case *TrustVector: + tv = *t + case map[string]interface{}: + tv, err = getTrustVectorFromMap(t) + case map[string]string: + m := make(map[string]interface{}, len(t)) + for k, v := range t { + m[k] = v + } + tv, err = getTrustVectorFromMap(m) + default: + err = fmt.Errorf("invalid value for TrustVector: %v", t) + } + + return &tv, err +} + +func getTrustVectorFromMap(m map[string]interface{}) (TrustVector, error) { + var vector TrustVector + + expected := []string{ + "instance-identity", + "configuration", + "executables", + "file-system", + "hardware", + "runtime-opaque", + "storage-opaque", + "sourced-data", + } + + extra := getExtraKeys(m, expected) + if len(extra) > 0 { + return vector, fmt.Errorf("found unexpected fields: %s", strings.Join(extra, ", ")) + } + + if err := populateClaimFromMap(m, "instance-identity", &vector.InstanceIdentity); err != nil { + return vector, err + } + + if err := populateClaimFromMap(m, "configuration", &vector.Configuration); err != nil { + return vector, err + } + + if err := populateClaimFromMap(m, "executables", &vector.Executables); err != nil { + return vector, err + } + + if err := populateClaimFromMap(m, "file-system", &vector.FileSystem); err != nil { + return vector, err + } + + if err := populateClaimFromMap(m, "hardware", &vector.Hardware); err != nil { + return vector, err + } + + if err := populateClaimFromMap(m, "runtime-opaque", &vector.RuntimeOpaque); err != nil { + return vector, err + } + + if err := populateClaimFromMap(m, "storage-opaque", &vector.StorageOpaque); err != nil { + return vector, err + } + + if err := populateClaimFromMap(m, "sourced-data", &vector.SourcedData); err != nil { + return vector, err + } + + return vector, nil +} + +func populateClaimFromMap(m map[string]interface{}, key string, dest *TrustClaim) error { + v, ok := m[key] + if !ok { + return nil + } + + claim, err := ToTrustClaim(v) + if err != nil { + return fmt.Errorf("bad value for %q: %w", key, err) + } + + *dest = *claim + + return err +} + +// SetAll sets all vector elements to the specified claim. This is primarily +// useful with globally-applicable claims such as -1 (verifier malfunction), 0 +// (no claim, in order to "reset" the vector), or 99 (cryptographic validation +// failed). +func (o *TrustVector) SetAll(c TrustClaim) { + o.InstanceIdentity = c + o.Configuration = c + o.Executables = c + o.FileSystem = c + o.Hardware = c + o.RuntimeOpaque = c + o.StorageOpaque = c + o.SourcedData = c } // Report provides an annotated view of the TrustVector state. diff --git a/trustvector_test.go b/trustvector_test.go index 71b21e9..8c3c6e1 100644 --- a/trustvector_test.go +++ b/trustvector_test.go @@ -70,3 +70,68 @@ Sourced Data [affirming]: unknown code-point -2 short = false assert.Equal(t, expectedLong, tv.Report(short, color)) } + +func TestToTrustVector(t *testing.T) { + tv, err := ToTrustVector(map[string]interface{}{ + "instance-identity": TrustworthyInstanceClaim, + "configuration": 2, + "executables": 2, + "file-system": "approved_fs", + "hardware": 32, + "runtime-opaque": -7, + "storage-opaque": 32, + "sourced-data": NoClaim, + }) + assert.NoError(t, err) + assert.Equal(t, TrustworthyInstanceClaim, tv.InstanceIdentity) + assert.Equal(t, UnsafeHardwareClaim, tv.Hardware) + assert.Equal(t, ApprovedFilesClaim, tv.FileSystem) + assert.Equal(t, TrustClaim(-7), tv.RuntimeOpaque) + assert.Equal(t, NoClaim, tv.SourcedData) + + tv, err = ToTrustVector(map[string]string{ + "runtime-opaque": "encrypted_rt", + "hardware": "unsafe_hw", + "file-system": "approved_fs", + }) + assert.NoError(t, err) + assert.Equal(t, EncryptedMemoryRuntimeClaim, tv.RuntimeOpaque) + assert.Equal(t, UnsafeHardwareClaim, tv.Hardware) + assert.Equal(t, ApprovedFilesClaim, tv.FileSystem) + assert.Equal(t, NoClaim, tv.Configuration) + + tv2 := TrustVector{ + InstanceIdentity: 2, + Configuration: 2, + Executables: 2, + } + + tv, err = ToTrustVector(tv2) + assert.NoError(t, err) + assert.Equal(t, TrustworthyInstanceClaim, tv.InstanceIdentity) + assert.Equal(t, ApprovedConfigClaim, tv.Configuration) + assert.Equal(t, ApprovedRuntimeClaim, tv.Executables) + + tv, err = ToTrustVector(&tv2) + assert.NoError(t, err) + assert.Equal(t, TrustworthyInstanceClaim, tv.InstanceIdentity) + assert.Equal(t, ApprovedConfigClaim, tv.Configuration) + assert.Equal(t, ApprovedRuntimeClaim, tv.Executables) + + _, err = ToTrustVector(42) + assert.ErrorContains(t, err, "invalid value for TrustVector: 42") + + _, err = ToTrustVector(map[string]interface{}{ + "instance-identity": TrustworthyInstanceClaim, + "hardware": "bad claim", + "file-system": "approved_fs", + }) + assert.ErrorContains(t, err, `bad value for "hardware": not a valid TrustClaim value: "bad claim"`) +} + +func TestTrustVector_SetAll(t *testing.T) { + var tv TrustVector + + tv.SetAll(VerifierMalfunctionClaim) + assert.Equal(t, VerifierMalfunctionClaim, tv.Configuration) +} diff --git a/util.go b/util.go new file mode 100644 index 0000000..ea07fd6 --- /dev/null +++ b/util.go @@ -0,0 +1,21 @@ +// Copyright 2022 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 + +package ear + +func getExtraKeys(m map[string]interface{}, expected []string) []string { + expectedMap := make(map[string]bool, len(expected)) + for _, e := range expected { + expectedMap[e] = true + } + + var extra []string + + for k := range m { + if _, found := expectedMap[k]; !found { + extra = append(extra, k) + } + } + + return extra +} diff --git a/util_test.go b/util_test.go new file mode 100644 index 0000000..9a579b6 --- /dev/null +++ b/util_test.go @@ -0,0 +1,36 @@ +// Copyright 2022 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 + +package ear + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_getExtraKeys(t *testing.T) { + m := map[string]interface{}{ + "name": "String Archer", + "alias": "duchess", + "age": 42, + "height": "6'2\"", + "eyes": "blue", + "hair": "black", + } + + extras := getExtraKeys(m, []string{"name", "age", "height"}) + assert.ElementsMatch(t, []string{"alias", "eyes", "hair"}, extras) + + extras = getExtraKeys(m, []string{"name", "alias", "age", "height", "eyes", "hair"}) + assert.ElementsMatch(t, []string{}, extras) + + extras = getExtraKeys(m, []string{"name", "age", "family", "language"}) + assert.ElementsMatch(t, []string{"alias", "height", "eyes", "hair"}, extras) + + extras = getExtraKeys(m, []string{}) + assert.ElementsMatch(t, []string{"name", "alias", "age", "height", "eyes", "hair"}, extras) + + extras = getExtraKeys(map[string]interface{}{}, []string{}) + assert.ElementsMatch(t, []string{}, extras) +}