Skip to content
Merged
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
81 changes: 66 additions & 15 deletions appraisal.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ import (
// other metadata that are relevant to establish the appraisal context - the
// evidence itself, the appraisal policy used, the time of appraisal.
type Appraisal struct {
Profile *eat.Profile `json:"eat_profile,omitempty"`
Status TrustTier `json:"ear_status"`
TrustVector *TrustVector `json:"ear_trustworthiness_vector,omitzero"`
AppraisalPolicyIDs []string `json:"ear_appraisal_policy_ids,omitempty"`
Nonce *eat.Nonce `json:"eat_nonce,omitempty"`
AttesterClaims map[eat.IntOrString]any `json:"ear_attester_claims,omitempty"`
VerifierClaims map[eat.IntOrString]any `json:"ear_verifier_claims,omitempty"`
Extensions *Extensions `json:"-"`
Profile *eat.Profile `cbor:"265,keyasint,omitempty" json:"eat_profile,omitempty"`
Status TrustTier `cbor:"1000,keyasint" json:"ear_status"`
TrustVector *TrustVector `cbor:"1001,keyasint,omitempty" json:"ear_trustworthiness_vector,omitzero"`
AppraisalPolicyIDs []string `cbor:"1003,keyasint,omitempty" json:"ear_appraisal_policy_ids,omitempty"`
Nonce *eat.Nonce `cbor:"10,keyasint,omitempty" json:"eat_nonce,omitempty"`
AttesterClaims map[eat.IntOrString]any `cbor:"1005,keyasint,omitempty" json:"ear_attester_claims,omitempty"`
VerifierClaims map[eat.IntOrString]any `cbor:"1006,keyasint,omitempty" json:"ear_verifier_claims,omitempty"`
Extensions *Extensions `cbor:"-" json:"-"`
}

// NewAppraisal returns a new Appraisal with its status set to TrustTierNone
Expand Down Expand Up @@ -58,7 +58,7 @@ func AppraisalFromAny(input any) (*Appraisal, error) {

for key, value := range claims {
switch key {
case eat.IntOrStringFromString("eat_profile"):
case eat.IntOrStringFromString("eat_profile"), eat.IntOrStringFromInt(265):
profileText, ok := value.(string)
if !ok {
return nil, fmt.Errorf("eat_profile: expected a string, got %T", value)
Expand All @@ -70,21 +70,21 @@ func AppraisalFromAny(input any) (*Appraisal, error) {
}

appraisal.Profile = profile
case eat.IntOrStringFromString("ear_status"):
case eat.IntOrStringFromString("ear_status"), eat.IntOrStringFromInt(1000):
tier, err := TrustTierFromAny(value)
if err != nil {
return nil, fmt.Errorf("ear_status: %w", err)
}

appraisal.Status = tier
case eat.IntOrStringFromString("ear_trustworthiness_vector"):
case eat.IntOrStringFromString("ear_trustworthiness_vector"), eat.IntOrStringFromInt(1001):
vector, err := TrustVectorFromAny(value)
if err != nil {
return nil, fmt.Errorf("ear_trustworthiness_vector: %w", err)
}

appraisal.TrustVector = vector
case eat.IntOrStringFromString("ear_appraisal_policy_ids"):
case eat.IntOrStringFromString("ear_appraisal_policy_ids"), eat.IntOrStringFromInt(1003):
rawIds, ok := value.([]any)
if !ok {
return nil, fmt.Errorf(
Expand All @@ -105,21 +105,21 @@ func AppraisalFromAny(input any) (*Appraisal, error) {
}

appraisal.AppraisalPolicyIDs = ids
case eat.IntOrStringFromString("eat_nonce"):
case eat.IntOrStringFromString("eat_nonce"), eat.IntOrStringFromInt(10):
nonce, err := eat.NonceFromAny(value)
if err != nil {
return nil, fmt.Errorf("eat_nonce: %w", err)
}

appraisal.Nonce = nonce
case eat.IntOrStringFromString("ear_attester_claims"):
case eat.IntOrStringFromString("ear_attester_claims"), eat.IntOrStringFromInt(1005):
claims, err := eat.IntOrStringMapFromAny(value)
if err != nil {
return nil, fmt.Errorf("ear_attester_claims: %w", err)
}

appraisal.AttesterClaims = claims
case eat.IntOrStringFromString("ear_verifier_claims"):
case eat.IntOrStringFromString("ear_verifier_claims"), eat.IntOrStringFromInt(1006):
claims, err := eat.IntOrStringMapFromAny(value)
if err != nil {
return nil, fmt.Errorf("ear_verifier_claims: %w", err)
Expand Down Expand Up @@ -226,3 +226,54 @@ func (o *Appraisal) UnmarshalJSON(data []byte) error { // nolint:dupl

return nil
}

func (o *Appraisal) MarshalCBOR() ([]byte, error) {
if err := o.Validate(); err != nil {
return nil, err
}

claims, err := structWithExtensionsToMap(o, "cbor")
if err != nil {
return nil, err
}

return em.Marshal(claims)
}

func (o *Appraisal) UnmarshalCBOR(data []byte) error { // nolint:dupl
type appraisalUnmarshalHelper Appraisal

if err := dm.Unmarshal(data, (*appraisalUnmarshalHelper)(o)); err != nil {
return err
}

var claims map[eat.IntOrString]any
if err := dm.Unmarshal(data, &claims); err != nil {
return err
}

var extensions Extensions
seenStatus := false
knownLabels := getTagLabels(o, "cbor")
for label, value := range claims {
if label == eat.IntOrStringFromInt(1000) {
seenStatus = true
}

if _, isKnown := knownLabels[label]; !isKnown {
extensions.Set(label, value)
}
}

if !seenStatus {
return errors.New(`missing mandatory claim 1000 (ear_status)`)
}

o.Extensions = &extensions

if err := o.Validate(); err != nil {
return err
}

return nil
}
79 changes: 79 additions & 0 deletions appraisal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/veraison/eat"
)

Expand Down Expand Up @@ -104,6 +105,7 @@ func TestAppraisal_round_trip(t *testing.T) {
title string
appraisal Appraisal
expectedJSON string
expectedCBOR []byte
}{
{
title: "ok minimal",
Expand All @@ -112,6 +114,11 @@ func TestAppraisal_round_trip(t *testing.T) {
Extensions: NewExtensions(),
},
expectedJSON: `{"ear_status": "affirming"}`,
expectedCBOR: []byte{
0xa1, // map(1)
0x19, 0x03, 0xe8, // . key: 1000 [ear_status]
0x02, // . value: 2 [affirming]
},
},
{
title: "ok with extensions",
Expand All @@ -120,6 +127,14 @@ func TestAppraisal_round_trip(t *testing.T) {
Extensions: NewExtensions().StringSet("foo", true),
},
expectedJSON: `{"ear_status": "affirming", "foo": true}`,
expectedCBOR: []byte{
0xa2, // map(2)
0x19, 0x03, 0xe8, // . key: 1000 [ear_status]
0x02, // . value: 2 [affirming]
0x63, // . key: tstr(3)
0x66, 0x6f, 0x6f, // . . "foo"
0xf5, // . value: true
},
},
{
title: "ok full",
Expand Down Expand Up @@ -147,6 +162,45 @@ func TestAppraisal_round_trip(t *testing.T) {
),
},
expectedJSON: `{"ear_appraisal_policy_ids":["test-policy"],"ear_attester_claims":{"ac-1":"foo"},"ear_status":"affirming","ear_trustworthiness_vector":{"instance-identity":2},"ear_verifier_claims":{"1":"foo"},"eat_nonce":"3q2-796tvu8","eat_profile":"http://test-profile","foo":{"bar":true}}`,
expectedCBOR: []byte{
0xa8, // map(8)
0x0a, // . key: 10 [eat_nonce]
0x48, // . value: bstr(8)
0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef,
0x19, 0x01, 0x09, // . key: 265 [eat_profile]
0x73, // . value: tstr(19)
0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x74, // . . "http://t"
0x65, 0x73, 0x74, 0x2d, 0x70, 0x72, 0x6f, 0x66, // . . "est-prof"
0x69, 0x6c, 0x65, // . . "ile"
0x19, 0x03, 0xe8, // . key: 1000 [ear_status]
0x02, // . value: 2 [affirming]
0x19, 0x03, 0xe9, // . key: 1001 [ear_trustworthiness_vector]
0xa1, // . value: map(1) [TrustVector]
0x00, // . . key: 0 [instance-identity]
0x02, // . . value: 2 [recognized_instance]
0x19, 0x03, 0xeb, // . key: 1003 [ear_appraisal_policy_ids]
0x81, // . value: array(1)
0x6b, // . . [0]tstr(11)
0x74, 0x65, 0x73, 0x74, 0x2d, 0x70, 0x6f, 0x6c, // . . . "test-pol"
0x69, 0x63, 0x79, // . . . "icy"
0x19, 0x03, 0xed, // . key: 1005 [ear_attester_claims]
0xa1, // . value: map(1)
0x64, // . . key: tstr(4)
0x61, 0x63, 0x2d, 0x31, // . . . "ac-1"
0x63, // . . value: tstr(3)
0x66, 0x6f, 0x6f, // . . . "foo"
0x19, 0x03, 0xee, // . key: 1006 [ear_verifier_claims]
0xa1, // . value: map(1)
0x01, // . . key: 1
0x63, // . . value: tstr(3)
0x66, 0x6f, 0x6f, // . . . "foo"
0x63, // . key: tstr(3) [extension]
0x66, 0x6f, 0x6f, // . . "foo"
0xa1, // . value: map(1)
0x63, // . . key: tstr(3)
0x62, 0x61, 0x72, // . . . "bar"
0xf5, // . . value: true
},
},
}

Expand All @@ -160,6 +214,31 @@ func TestAppraisal_round_trip(t *testing.T) {
err = json.Unmarshal(encodedJSON, &decoded)
assert.NoError(t, err)
assert.EqualValues(t, tc.appraisal, decoded)

encodedCBOR, err := tc.appraisal.MarshalCBOR()
assert.NoError(t, err)
assert.Equal(t, tc.expectedCBOR, encodedCBOR)

err = decoded.UnmarshalCBOR(encodedCBOR)
assert.NoError(t, err)

if tc.title == "ok full" {
// the map inside extension value gets
// unmarshaled as map[any]any in CBOR (but as
// map[string]any in JSON). So we need to
// manually adjust that here.
extMap, err := decoded.Extensions.GetMap("foo")
require.NoError(t, err)

adjusted := make(map[string]any)
for key, value := range extMap {
adjusted[key.(string)] = value
}

decoded.Extensions.StringSet("foo", adjusted)
}

assert.EqualValues(t, tc.appraisal, decoded)
})
}
}
Expand Down
41 changes: 29 additions & 12 deletions arc/cmd/create.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2022 Contributors to the Veraison project.
// Copyright 2022-2026 Contributors to the Veraison project.
// SPDX-License-Identifier: Apache-2.0
package cmd

Expand All @@ -18,19 +18,21 @@ var (
createSKey string
createAlg string
createOutput string
createCWT bool
)

var createCmd = NewCreateCmd()

func NewCreateCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "create [flags] <jwt-file>",
Short: "Read the EAR claims-set from a JSON file, sign it and save the resulting JWT to jwt-file",
Long: `Read the EAR claims-set from a JSON file, sign it and save the resulting JWT to jwt-file

Use: "create [flags] <outfile>",
Short: "Read the EAR claims-set from a JSON file, sign it and save the resulting JWT (or CWT) to outfile",
Long: `Read the EAR claims-set from a JSON file, sign it and save the resulting JWT (or CWT) to outfile
Create an EAR from the default claims-set file "ear-claims.json". Sign it with
the key in the default key file "skey.json", and save the result to "my-ear.jwt".

Note: for CWT, only ES256, ES384, and ES512 signature algorithms are supported.

arc create my-ear.jwt
`,
RunE: func(cmd *cobra.Command, args []string) error {
Expand Down Expand Up @@ -65,12 +67,23 @@ the key in the default key file "skey.json", and save the result to "my-ear.jwt"
return fmt.Errorf("parsing signing key from %q: %w", createSKey, err)
}

if alg, err = jwa.KeyAlgorithmFrom(createAlg); err != nil {
return fmt.Errorf("parsing algorithm from %q: %w", createAlg, err)
}

if arBytes, err = ar.SignJWT(alg, sigK); err != nil {
return fmt.Errorf("signing EAR: %w", err)
if createCWT {
signerAlg, signer, err := ear.SignerFromJWK(sigK)
if err != nil {
return fmt.Errorf("obtaining signer from key: %w", err)
}

if arBytes, err = ar.SignCWT(signerAlg, signer, nil); err != nil {
return fmt.Errorf("signing EAR: %w", err)
}
} else {
if alg, err = jwa.KeyAlgorithmFrom(createAlg); err != nil {
return fmt.Errorf("parsing algorithm from %q: %w", createAlg, err)
}

if arBytes, err = ar.SignJWT(alg, sigK); err != nil {
return fmt.Errorf("signing EAR: %w", err)
}
}

// save to createOutput
Expand All @@ -93,7 +106,11 @@ the key in the default key file "skey.json", and save the result to "my-ear.jwt"
)

cmd.Flags().StringVarP(
&createAlg, "alg", "a", "ES256", "signing algorithm ("+algList()+")",
&createAlg, "alg", "a", "ES256", "JWT signing algorithm ("+algList()+"); (this is not used for CWT)",
)

cmd.Flags().BoolVarP(
&createCWT, "cwt", "C", false, "Create a CWT (JWT is created by default)",
)

return cmd
Expand Down
27 changes: 26 additions & 1 deletion arc/cmd/create_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2022 Contributors to the Veraison project.
// Copyright 2022-2026 Contributors to the Veraison project.
// SPDX-License-Identifier: Apache-2.0
package cmd

Expand Down Expand Up @@ -183,3 +183,28 @@ func Test_CreateCmd_ok(t *testing.T) {
_, err = fs.Stat("ear.jwt")
assert.NoError(t, err)
}

func Test_CreateCmd_CWT_ok(t *testing.T) {
cmd := NewCreateCmd()

files := []fileEntry{
{"skey.json", testSKey},
{"ear-claims.json", testMiniClaimsSet},
}
makeFS(t, files)

args := []string{
"--skey=skey.json",
"--claims=ear-claims.json",
"--alg=ES256",
"--cwt",
"ear.cwt",
}
cmd.SetArgs(args)

err := cmd.Execute()
assert.NoError(t, err)

_, err = fs.Stat("ear.cwt")
assert.NoError(t, err)
}
23 changes: 23 additions & 0 deletions arc/cmd/root_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright 2026 Contributors to the Veraison project.

// SPDX-License-Identifier: Apache-2.0
package cmd

import (
"testing"
)

func Test_RootCmd_config(t *testing.T) {
files := []fileEntry{
{"test.yaml", []byte("")},
}
makeFS(t, files)

args := []string{
"--config=test.yaml",
"-h",
}
rootCmd.SetArgs(args)

Execute()
}
Loading
Loading