diff --git a/scheme/psa-iot/corim_extractor.go b/scheme/psa-iot/corim_extractor.go index 185ec7da..b3dbddf6 100644 --- a/scheme/psa-iot/corim_extractor.go +++ b/scheme/psa-iot/corim_extractor.go @@ -1,164 +1,172 @@ -// Copyright 2022-2024 Contributors to the Veraison project. -// SPDX-License-Identifier: Apache-2.0 -package psa_iot - -import ( - "encoding/json" - "errors" - "fmt" - "reflect" - - "github.com/veraison/corim/comid" - "github.com/veraison/services/handler" - "github.com/veraison/services/scheme/common/arm/platform" -) - -type CorimExtractor struct { - Profile string -} - -func (o CorimExtractor) RefValExtractor(rvs comid.ValueTriples) ([]*handler.Endorsement, error) { - refVals := make([]*handler.Endorsement, 0, len(rvs.Values)) - - for _, rv := range rvs.Values { - var classAttrs platform.ClassAttributes - var refVal *handler.Endorsement - var err error - - if o.Profile != "http://arm.com/psa/iot/1" { - return nil, fmt.Errorf( - "incorrect profile: %s for Scheme PSA_IOT", - o.Profile, - ) - } - - if err := classAttrs.FromEnvironment(rv.Environment); err != nil { - return nil, fmt.Errorf("could not extract PSA class attributes: %w", err) - } - - // Each measurement is encoded in a measurement-map of a CoMID - // reference-triple-record. Since a measurement-map can encode one or more - // measurements, a single reference-triple-record can carry as many - // measurements as needed, provided they belong to the same PSA RoT - // identified in the subject of the "reference value" triple. A single - // reference-triple-record SHALL completely describe the updatable PSA RoT. - for i, m := range rv.Measurements.Values { - if m.Key == nil { - return nil, fmt.Errorf("measurement key is not present") - } - - if !m.Key.IsSet() { - return nil, fmt.Errorf("measurement key is not set at index %d ", i) - } - - // Check which MKey is present and then decide which extractor to invoke - switch m.Key.Type() { - case comid.PSARefValIDType: - var swCompAttrs platform.SwCompAttributes - refVal, err = o.extractMeas(&swCompAttrs, m, classAttrs) - if err != nil { - return nil, fmt.Errorf("unable to extract measurement at index %d, %w", i, err) - } - default: - return nil, fmt.Errorf("unknown measurement key: %T", reflect.TypeOf(m.Key)) - } - refVals = append(refVals, refVal) - } - } - - if len(refVals) == 0 { - return nil, fmt.Errorf("no software components found") - } - - return refVals, nil -} - -func (o CorimExtractor) extractMeas( - obj platform.MeasurementExtractor, - m comid.Measurement, - class platform.ClassAttributes, -) (*handler.Endorsement, error) { - if err := obj.FromMeasurement(m); err != nil { - return nil, err - } - - refAttrs, err := obj.MakeRefAttrs(class) - if err != nil { - return &handler.Endorsement{}, fmt.Errorf("failed to create software component attributes: %w", err) - } - refVal := handler.Endorsement{ - Scheme: "PSA_IOT", - Type: handler.EndorsementType_REFERENCE_VALUE, - SubType: obj.GetRefValType(), - Attributes: refAttrs, - } - return &refVal, nil -} - -func (o CorimExtractor) TaExtractor(avk comid.KeyTriple) (*handler.Endorsement, error) { - // extract implementation ID - var classAttrs platform.ClassAttributes - if err := classAttrs.FromEnvironment(avk.Environment); err != nil { - return nil, fmt.Errorf("could not extract PSA class attributes: %w", err) - } - - // extract instance ID - var instanceAttrs platform.InstanceAttributes - if err := instanceAttrs.FromEnvironment(avk.Environment); err != nil { - return nil, fmt.Errorf("could not extract PSA instance-id: %w", err) - } - - // extract IAK pub - if len(avk.VerifKeys) != 1 { - return nil, errors.New("expecting exactly one IAK public key") - } - - iakPub := avk.VerifKeys[0] - if _, ok := iakPub.Value.(*comid.TaggedPKIXBase64Key); !ok { - return nil, fmt.Errorf("IAK does not appear to be a PEM key (%T)", iakPub.Value) - } - - taAttrs, err := makeTaAttrs(instanceAttrs, classAttrs, iakPub) - if err != nil { - return nil, fmt.Errorf("failed to create trust anchor attributes: %w", err) - } - - // note we do not need a subType for TA - ta := &handler.Endorsement{ - Scheme: "PSA_IOT", - Type: handler.EndorsementType_VERIFICATION_KEY, - Attributes: taAttrs, - } - - return ta, nil -} - -func makeTaAttrs( - i platform.InstanceAttributes, - c platform.ClassAttributes, - key *comid.CryptoKey, -) (json.RawMessage, error) { - taID := map[string]interface{}{ - "impl-id": c.ImplID, - "inst-id": []byte(i.InstID), - "iak-pub": key.String(), - } - - if c.Vendor != "" { - taID["hw-vendor"] = c.Vendor - } - - if c.Model != "" { - taID["hw-model"] = c.Model - } - - msg, err := json.Marshal(taID) - if err != nil { - return nil, fmt.Errorf("unable to marshal TA attributes: %w", err) - } - return msg, nil -} - -func (o *CorimExtractor) SetProfile(profile string) { - o.Profile = profile -} +// Copyright 2022-2024 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 +package psa_iot + +import ( + "encoding/json" + "errors" + "fmt" + "reflect" + + "github.com/veraison/corim/comid" + // "github.com/veraison/corim/comid/psa" + "github.com/veraison/services/handler" + "github.com/veraison/services/scheme/common/arm/platform" +) + +type CorimExtractor struct { + Profile string +} + +func (o CorimExtractor) RefValExtractor(rvs comid.ValueTriples) ([]*handler.Endorsement, error) { + refVals := make([]*handler.Endorsement, 0, len(rvs.Values)) + + for _, rv := range rvs.Values { + var classAttrs platform.ClassAttributes + var refVal *handler.Endorsement + var err error + + if o.Profile != "http://arm.com/psa/iot/1" && o.Profile != "tag:arm.com,2025:psa#1.0.0" { + return nil, fmt.Errorf( + "incorrect profile: %s for Scheme PSA_IOT", + o.Profile, + ) + } + + if err := classAttrs.FromEnvironment(rv.Environment); err != nil { + return nil, fmt.Errorf("could not extract PSA class attributes: %w", err) + } + + // Each measurement is encoded in a measurement-map of a CoMID + // reference-triple-record. Since a measurement-map can encode one or more + // measurements, a single reference-triple-record can carry as many + // measurements as needed, provided they belong to the same PSA RoT + // identified in the subject of the "reference value" triple. A single + // reference-triple-record SHALL completely describe the updatable PSA RoT. + for i, m := range rv.Measurements.Values { + if m.Key == nil { + return nil, fmt.Errorf("measurement key is not present") + } + + if !m.Key.IsSet() { + return nil, fmt.Errorf("measurement key is not set at index %d ", i) + } + + // Check which MKey is present and then decide which extractor to invoke + switch m.Key.Type() { + case comid.PSARefValIDType: + var swCompAttrs platform.SwCompAttributes + refVal, err = o.extractMeas(&swCompAttrs, m, classAttrs) + if err != nil { + return nil, fmt.Errorf("unable to extract measurement at index %d, %w", i, err) + } + // TODO: Uncomment when PSA profile dependency is available + // case psa.PSASoftwareComponentType: + // var swCompAttrs platform.SwCompAttributes + // refVal, err = o.extractMeas(&swCompAttrs, m, classAttrs) + // if err != nil { + // return nil, fmt.Errorf("unable to extract PSA software component measurement at index %d, %w", i, err) + // } + default: + return nil, fmt.Errorf("unknown measurement key: %T", reflect.TypeOf(m.Key)) + } + refVals = append(refVals, refVal) + } + } + + if len(refVals) == 0 { + return nil, fmt.Errorf("no software components found") + } + + return refVals, nil +} + +func (o CorimExtractor) extractMeas( + obj platform.MeasurementExtractor, + m comid.Measurement, + class platform.ClassAttributes, +) (*handler.Endorsement, error) { + if err := obj.FromMeasurement(m); err != nil { + return nil, err + } + + refAttrs, err := obj.MakeRefAttrs(class) + if err != nil { + return &handler.Endorsement{}, fmt.Errorf("failed to create software component attributes: %w", err) + } + refVal := handler.Endorsement{ + Scheme: "PSA_IOT", + Type: handler.EndorsementType_REFERENCE_VALUE, + SubType: obj.GetRefValType(), + Attributes: refAttrs, + } + return &refVal, nil +} + +func (o CorimExtractor) TaExtractor(avk comid.KeyTriple) (*handler.Endorsement, error) { + // extract implementation ID + var classAttrs platform.ClassAttributes + if err := classAttrs.FromEnvironment(avk.Environment); err != nil { + return nil, fmt.Errorf("could not extract PSA class attributes: %w", err) + } + + // extract instance ID + var instanceAttrs platform.InstanceAttributes + if err := instanceAttrs.FromEnvironment(avk.Environment); err != nil { + return nil, fmt.Errorf("could not extract PSA instance-id: %w", err) + } + + // extract IAK pub + if len(avk.VerifKeys) != 1 { + return nil, errors.New("expecting exactly one IAK public key") + } + + iakPub := avk.VerifKeys[0] + if _, ok := iakPub.Value.(*comid.TaggedPKIXBase64Key); !ok { + return nil, fmt.Errorf("IAK does not appear to be a PEM key (%T)", iakPub.Value) + } + + taAttrs, err := makeTaAttrs(instanceAttrs, classAttrs, iakPub) + if err != nil { + return nil, fmt.Errorf("failed to create trust anchor attributes: %w", err) + } + + // note we do not need a subType for TA + ta := &handler.Endorsement{ + Scheme: "PSA_IOT", + Type: handler.EndorsementType_VERIFICATION_KEY, + Attributes: taAttrs, + } + + return ta, nil +} + +func makeTaAttrs( + i platform.InstanceAttributes, + c platform.ClassAttributes, + key *comid.CryptoKey, +) (json.RawMessage, error) { + taID := map[string]interface{}{ + "impl-id": c.ImplID, + "inst-id": []byte(i.InstID), + "iak-pub": key.String(), + } + + if c.Vendor != "" { + taID["hw-vendor"] = c.Vendor + } + + if c.Model != "" { + taID["hw-model"] = c.Model + } + + msg, err := json.Marshal(taID) + if err != nil { + return nil, fmt.Errorf("unable to marshal TA attributes: %w", err) + } + return msg, nil +} + +func (o *CorimExtractor) SetProfile(profile string) { + o.Profile = profile +} diff --git a/scheme/psa-iot/endorsement_handler_test.go b/scheme/psa-iot/endorsement_handler_test.go index 453276f3..cebf500b 100644 --- a/scheme/psa-iot/endorsement_handler_test.go +++ b/scheme/psa-iot/endorsement_handler_test.go @@ -1,128 +1,140 @@ -// Copyright 2022-2025 Contributors to the Veraison project. -// SPDX-License-Identifier: Apache-2.0 -package psa_iot - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestDecoder_GetAttestationScheme(t *testing.T) { - d := &EndorsementHandler{} - - expected := SchemeName - - actual := d.GetAttestationScheme() - - assert.Equal(t, expected, actual) -} - -func TestDecoder_GetSupportedMediaTypes(t *testing.T) { - d := &EndorsementHandler{} - - expected := EndorsementMediaTypes - - actual := d.GetSupportedMediaTypes() - - assert.Equal(t, expected, actual) -} - -func TestDecoder_Init(t *testing.T) { - d := &EndorsementHandler{} - - assert.Nil(t, d.Init(nil)) -} - -func TestDecoder_Close(t *testing.T) { - d := &EndorsementHandler{} - - assert.Nil(t, d.Close()) -} - -func TestDecoder_Decode_empty_data(t *testing.T) { - d := &EndorsementHandler{} - - emptyData := []byte{} - - expectedErr := `empty data` - - _, err := d.Decode(emptyData, "", nil) - - assert.EqualError(t, err, expectedErr) -} - -func TestDecoder_Decode_invalid_data(t *testing.T) { - d := &EndorsementHandler{} - - invalidCbor := []byte("invalid CBOR") - - expectedErr := `CBOR decoding failed: expected map (CBOR Major Type 5), found Major Type 3` - - _, err := d.Decode(invalidCbor, "", nil) - - assert.EqualError(t, err, expectedErr) -} - -func TestDecoder_Decode_OK(t *testing.T) { - tvs := [][]byte{ - unsignedCorimComidPsaIakPubOne, - unsignedCorimComidPsaIakPubTwo, - unsignedCorimComidPsaRefValOne, - unsignedCorimComidPsaRefValThree, - unsignedCorimComidPsaRefValOnlyMandIDAttr, - } - - d := &EndorsementHandler{} - - for _, tv := range tvs { - _, err := d.Decode(tv, "", nil) - assert.NoError(t, err) - } -} - -func TestDecoder_Decode_negative_tests(t *testing.T) { - tvs := []struct { - desc string - input []byte - expectedErr string - }{ - { - desc: "multiple verification keys for an instance", - input: unsignedCorimComidPsaMultIak, - expectedErr: `bad key in CoMID at index 0: expecting exactly one IAK public key`, - }, - { - desc: "multiple digests in the same measurement", - input: unsignedCorimComidPsaRefValMultDigest, - expectedErr: "bad software component in CoMID at index 0: unable to extract measurement at index 0, expecting exactly one digest", - }, - { - desc: "missing measurement identifier", - input: unsignedCorimComidPsaRefValNoMkey, - expectedErr: `bad software component in CoMID at index 0: measurement key is not present`, - }, - { - desc: "no implementation id specified in the measurement", - input: unsignedCorimComidPsaRefValNoImplID, - expectedErr: `bad software component in CoMID at index 0: could not extract PSA class attributes: could not extract implementation-id from class-id: class-id type is: *comid.TaggedUUID`, - }, - { - desc: "no instance id specified in the verification key triple", - input: unsignedCorimComidPsaIakPubNoUeID, - expectedErr: `bad key in CoMID at index 0: could not extract PSA instance-id: expecting instance in environment`, - }, - { - desc: "no implementation id specified in the verification key triple", - input: unsignedCorimComidPsaIakPubNoImplID, - expectedErr: `bad key in CoMID at index 0: could not extract PSA class attributes: could not extract implementation-id from class-id: class-id type is: *comid.TaggedUUID`, - }} - - for _, tv := range tvs { - t.Run(tv.desc, func(t *testing.T) { - d := &EndorsementHandler{} - _, err := d.Decode(tv.input, "", nil) - assert.EqualError(t, err, tv.expectedErr) - }) - } -} +// Copyright 2022-2025 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 +package psa_iot + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDecoder_GetAttestationScheme(t *testing.T) { + d := &EndorsementHandler{} + + expected := SchemeName + + actual := d.GetAttestationScheme() + + assert.Equal(t, expected, actual) +} + +func TestDecoder_GetSupportedMediaTypes(t *testing.T) { + d := &EndorsementHandler{} + + expected := EndorsementMediaTypes + + actual := d.GetSupportedMediaTypes() + + assert.Equal(t, expected, actual) +} + +func TestDecoder_Init(t *testing.T) { + d := &EndorsementHandler{} + + assert.Nil(t, d.Init(nil)) +} + +func TestDecoder_Close(t *testing.T) { + d := &EndorsementHandler{} + + assert.Nil(t, d.Close()) +} + +func TestDecoder_Decode_empty_data(t *testing.T) { + d := &EndorsementHandler{} + + emptyData := []byte{} + + expectedErr := `empty data` + + _, err := d.Decode(emptyData, "", nil) + + assert.EqualError(t, err, expectedErr) +} + +func TestDecoder_Decode_invalid_data(t *testing.T) { + d := &EndorsementHandler{} + + invalidCbor := []byte("invalid CBOR") + + expectedErr := `CBOR decoding failed: expected map (CBOR Major Type 5), found Major Type 3` + + _, err := d.Decode(invalidCbor, "", nil) + + assert.EqualError(t, err, expectedErr) +} + +func TestDecoder_Decode_OK(t *testing.T) { + tvs := [][]byte{ + unsignedCorimComidPsaIakPubOne, + unsignedCorimComidPsaIakPubTwo, + unsignedCorimComidPsaRefValOne, + unsignedCorimComidPsaRefValThree, + unsignedCorimComidPsaRefValOnlyMandIDAttr, + } + + d := &EndorsementHandler{} + + for _, tv := range tvs { + _, err := d.Decode(tv, "", nil) + assert.NoError(t, err) + } +} + +func TestDecoder_Decode_negative_tests(t *testing.T) { + tvs := []struct { + desc string + input []byte + expectedErr string + }{ + { + desc: "multiple verification keys for an instance", + input: unsignedCorimComidPsaMultIak, + expectedErr: `bad key in CoMID at index 0: expecting exactly one IAK public key`, + }, + { + desc: "multiple digests in the same measurement", + input: unsignedCorimComidPsaRefValMultDigest, + expectedErr: "bad software component in CoMID at index 0: unable to extract measurement at index 0, expecting exactly one digest", + }, + { + desc: "missing measurement identifier", + input: unsignedCorimComidPsaRefValNoMkey, + expectedErr: `bad software component in CoMID at index 0: measurement key is not present`, + }, + { + desc: "no implementation id specified in the measurement", + input: unsignedCorimComidPsaRefValNoImplID, + expectedErr: `bad software component in CoMID at index 0: could not extract PSA class attributes: could not extract implementation-id from class-id: class-id type is: *comid.TaggedUUID`, + }, + { + desc: "no instance id specified in the verification key triple", + input: unsignedCorimComidPsaIakPubNoUeID, + expectedErr: `bad key in CoMID at index 0: could not extract PSA instance-id: expecting instance in environment`, + }, + { + desc: "no implementation id specified in the verification key triple", + input: unsignedCorimComidPsaIakPubNoImplID, + expectedErr: `bad key in CoMID at index 0: could not extract PSA class attributes: could not extract implementation-id from class-id: class-id type is: *comid.TaggedUUID`, + }} + + for _, tv := range tvs { + t.Run(tv.desc, func(t *testing.T) { + d := &EndorsementHandler{} + _, err := d.Decode(tv.input, "", nil) + assert.EqualError(t, err, tv.expectedErr) + }) + } +} + +func TestCorimExtractor_ProfileSupport(t *testing.T) { + extractor := &CorimExtractor{} + + // Test old PSA profile + extractor.SetProfile("http://arm.com/psa/iot/1") + assert.Equal(t, "http://arm.com/psa/iot/1", extractor.Profile) + + // Test new PSA profile + extractor.SetProfile("tag:arm.com,2025:psa#1.0.0") + assert.Equal(t, "tag:arm.com,2025:psa#1.0.0", extractor.Profile) +} diff --git a/scheme/psa-iot/scheme.go b/scheme/psa-iot/scheme.go index 1bec58a4..2003764f 100644 --- a/scheme/psa-iot/scheme.go +++ b/scheme/psa-iot/scheme.go @@ -1,21 +1,25 @@ -// Copyright 2023-2025 Contributors to the Veraison project. -// SPDX-License-Identifier: Apache-2.0 -package psa_iot - -const ( - SchemeName = "PSA_IOT" -) - -var EndorsementMediaTypes = []string{ - // Unsigned CoRIM profile - `application/corim-unsigned+cbor; profile="http://arm.com/psa/iot/1"`, - // Signed CoRIM profile - `application/rim+cose; profile="http://arm.com/psa/iot/1"`, -} - -var EvidenceMediaTypes = []string{ - "application/psa-attestation-token", - `application/eat-cwt; profile="http://arm.com/psa/2.0.0"`, - `application/eat+cwt; eat_profile="tag:psacertified.org,2023:psa#tfm"`, - `application/eat+cwt; eat_profile="tag:psacertified.org,2019:psa#legacy"`, -} +// Copyright 2023-2025 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 +package psa_iot + +const ( + SchemeName = "PSA_IOT" +) + +var EndorsementMediaTypes = []string{ + // Unsigned CoRIM profile - legacy PSA profile + `application/corim-unsigned+cbor; profile="http://arm.com/psa/iot/1"`, + // Signed CoRIM profile - legacy PSA profile + `application/rim+cose; profile="http://arm.com/psa/iot/1"`, + // Unsigned CoRIM profile - new PSA profile + `application/corim-unsigned+cbor; profile="tag:arm.com,2025:psa#1.0.0"`, + // Signed CoRIM profile - new PSA profile + `application/rim+cose; profile="tag:arm.com,2025:psa#1.0.0"`, +} + +var EvidenceMediaTypes = []string{ + "application/psa-attestation-token", + `application/eat-cwt; profile="http://arm.com/psa/2.0.0"`, + `application/eat+cwt; eat_profile="tag:psacertified.org,2023:psa#tfm"`, + `application/eat+cwt; eat_profile="tag:psacertified.org,2019:psa#legacy"`, +}