diff --git a/corim/common_test.go b/corim/common_test.go new file mode 100644 index 00000000..12ebe10d --- /dev/null +++ b/corim/common_test.go @@ -0,0 +1,115 @@ +// Copyright 2024 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 + +package corim + +import ( + _ "embed" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + // minimalist unsigned-corim that embeds comid.PSARefValJSONTemplate + //go:embed testcases/unsigned-good-corim.cbor + testGoodUnsignedCorimCBOR []byte + + // comid entity and unsigned-corim are extended + //go:embed testcases/unsigned-corim-with-extensions.cbor + testUnsignedCorimWithExtensionsCBOR []byte + + // comid entity and unsigned-corim are extended + //go:embed testcases/signed-good-corim.cbor + testGoodSignedCorimCBOR []byte + + // comid entity and unsigned-corim are extended + //go:embed testcases/signed-corim-with-extensions.cbor + testSignedCorimWithExtensionsCBOR []byte + + //go:embed testcases/corim.json + testUnsignedCorimJSON []byte + + //go:embed testcases/corim-ext.json + testUnsignedCorimWithExtensionsJSON []byte + + //go:embed testcases/comid.json + testComidJSON []byte + + //go:embed testcases/comid-ext.json + testComidWithExtensionsJSON []byte +) + +func assertCoRIMEq(t *testing.T, expected []byte, actual []byte, msgAndArgs ...interface{}) bool { + var expectedCoRIM, actualCoRIM *UnsignedCorim + + if err := dm.Unmarshal(expected, &expectedCoRIM); err != nil { + return assert.Fail(t, fmt.Sprintf( + "Expected value ('%s') is not valid UnsignedCorim: '%s'", + expected, err.Error()), msgAndArgs...) + } + + if err := dm.Unmarshal(actual, &actualCoRIM); err != nil { + return assert.Fail(t, fmt.Sprintf( + "actual value ('%s') is not valid UnsignedCorim: '%s'", + actual, err.Error()), msgAndArgs...) + } + + if !assert.EqualValues(t, expectedCoRIM.ID, actualCoRIM.ID, msgAndArgs...) { + return false + } + + if !assert.EqualValues(t, expectedCoRIM.DependentRims, + actualCoRIM.DependentRims, msgAndArgs...) { + return false + } + + if !assert.EqualValues(t, expectedCoRIM.Profile, actualCoRIM.Profile, msgAndArgs...) { + return false + } + + if !assert.EqualValues(t, expectedCoRIM.RimValidity, + actualCoRIM.RimValidity, msgAndArgs...) { + return false + } + + if !assert.EqualValues(t, expectedCoRIM.Entities, actualCoRIM.Entities, msgAndArgs...) { + return false + } + + if len(expectedCoRIM.Tags) != len(actualCoRIM.Tags) { + allMsgAndArgs := []interface{}{len(expectedCoRIM.Tags), len(actualCoRIM.Tags)} + allMsgAndArgs = append(allMsgAndArgs, msgAndArgs...) + return assert.Fail(t, fmt.Sprintf( + "Unexpected number of Tags: expected %d, actual %d", allMsgAndArgs...)) + } + + for i, expectedTag := range expectedCoRIM.Tags { + actualTag := actualCoRIM.Tags[i] + + if !assertCBOREq(t, expectedTag, actualTag, msgAndArgs...) { + return false + } + } + + return true +} + +func assertCBOREq(t *testing.T, expected []byte, actual []byte, msgAndArgs ...interface{}) bool { + var expectedCBOR, actualCBOR interface{} + + if err := dm.Unmarshal(expected, &expectedCBOR); err != nil { + return assert.Fail(t, fmt.Sprintf( + "Expected value ('%s') is not valid cbor.\nCBOR parsing error: '%s'", + expected, err.Error()), msgAndArgs...) + } + + if err := dm.Unmarshal(actual, &actualCBOR); err != nil { + return assert.Fail(t, fmt.Sprintf( + "Input ('%s') needs to be valid cbor.\nCBOR parsing error: '%s'", + actual, err.Error()), msgAndArgs...) + } + + return assert.Equal(t, expectedCBOR, actualCBOR, msgAndArgs...) +} diff --git a/corim/example_profile_test.go b/corim/example_profile_test.go new file mode 100644 index 00000000..435ba476 --- /dev/null +++ b/corim/example_profile_test.go @@ -0,0 +1,198 @@ +// Copyright 2024 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 +package corim + +import ( + "encoding/hex" + "errors" + "fmt" + "log" + "os" + "time" + + "github.com/veraison/corim/comid" + "github.com/veraison/corim/extensions" + "github.com/veraison/eat" + "github.com/veraison/swid" +) + +// ----- profile definition ----- +// The following code defines a profile with the following extensions and +// constraints: +// - Entities may contain an address field +// - Reference values may contain a Unix timestamp indicating when the +// individual measurement was taken. +// - The language claim (CoMID index 0) must be present, and must be "en-GB" + +// These extensions will be used for both CoMID and CoRIM entities. +type EntityExtensions struct { + Address *string `cbor:"-1,keyasint,omitempty" json:"address,omitempty"` +} + +type RefValExtensions struct { + Timestamp *int `cbor:"-1,keyasint,omitempty" json:"timestamp,omitempty"` +} + +// We're not defining any additional fields, however we're providing extra +// constraints that will be applied on top of standard CoMID validation. +type ComidExtensions struct{} + +func (*ComidExtensions) ConstrainComid(c *comid.Comid) error { + if c.Language == nil { + return errors.New("language not specified") + } + + if *c.Language != "en-GB" { + return fmt.Errorf(`language must be "en-GB", but found %q`, *c.Language) + } + + return nil +} + +// Registering the profile inside init() in the same file where it is defined +// ensures that the profile will always be available, and you don't need to +// remember to register it at the time you want to use it. The only potential +// danger with that is if the your profile ID clashes with another profile, +// which should not happen if it a registered PEN or a URL containing a domain +// that you own. +func init() { + profileID, err := eat.NewProfile("http://example.com/example-profile") + if err != nil { + panic(err) // will not error, as the hard-coded string above is valid + } + + extMap := extensions.NewMap(). + Add(ExtEntity, &EntityExtensions{}). + Add(comid.ExtComid, &ComidExtensions{}). + Add(comid.ExtEntity, &EntityExtensions{}). + Add(comid.ExtReferenceValue, &RefValExtensions{}) + + if err := RegisterProfile(profileID, extMap); err != nil { + panic(err) // will not error, assuming our profile ID is unique, + // and we've correctly set up the extensions Map above + } +} + +// ----- end of profile definition ----- +// The following code demonstrates how the profile might be used. + +func Example_profile_unmarshal() { + buf, err := os.ReadFile("testcases/unsigned-example-corim.cbor") + if err != nil { + log.Fatalf("could not read corim file: %v", err) + } + + // UnmarshalUnsignedCorimFromCBOR will detect the profile and ensure + // the correct extensions are loaded before unmarshalling + extractedCorim, err := UnmarshalUnsignedCorimFromCBOR(buf) + if err != nil { + log.Fatalf("could not unmarshal corim: %v", err) + } + + extractedComid, err := UnmarshalComidFromCBOR( + extractedCorim.Tags[0], + extractedCorim.Profile, + ) + if err != nil { + log.Fatalf("could not unmarshal corim: %v", err) + } + + fmt.Printf("Language: %s\n", *extractedComid.Language) + fmt.Printf("Entity: %s\n", *extractedComid.Entities.Values[0].EntityName) + fmt.Printf(" %s\n", extractedComid.Entities.Values[0]. + Extensions.MustGetString("Address")) + + fmt.Printf("Measurements:\n") + for _, m := range extractedComid.Triples.ReferenceValues.Values[0].Measurements.Values { + + val := hex.EncodeToString((*m.Val.Digests)[0].HashValue) + tsInt := m.Val.Extensions.MustGetInt64("timestamp") + ts := time.Unix(tsInt, 0).UTC() + + fmt.Printf(" %v taken at %s\n", val, ts.Format("2006-01-02T15:04:05")) + } + + // output: + // Language: en-GB + // Entity: ACME Ltd. + // 123 Fake Street + // Measurements: + // 87428fc522803d31065e7bce3cf03fe475096631e5e07bbd7a0fde60c4cf25c7 taken at 2024-07-12T11:03:10 + // 0263829989b6fd954f72baaf2fc64bc2e2f01d692d4de72986ea808f6e99813f taken at 2024-07-12T11:03:10 + // a3a5e715f0cc574a73c3f9bebb6bc24f32ffd5b67b387244c2c909da779a1478 taken at 2024-07-12T11:03:10 +} + +// note: this example is rather verbose as we're going to be constructing a +// CoMID by hand. In practice, you would typically write a JSON document and +// then unmarshal that into a CoRIM before marshaling it into CBOR (in which +// case, extensions will work as with unmarshaling example above). +func Example_profile_marshal() { + profileID, err := eat.NewProfile("http://example.com/example-profile") + if err != nil { + panic(err) + } + + profile, ok := GetProfile(profileID) + if !ok { + log.Fatalf("profile %v not found", profileID) + } + + myCorim := profile.GetUnsignedCorim() + myComid := profile.GetComid(). + SetLanguage("en-GB"). + SetTagIdentity("example", 0). + // Adding an entity to the Entities collection also registers + // profile's extensions + AddEntity("ACME Ltd.", &comid.TestRegID, comid.RoleCreator) + + address := "123 Fake Street" + err = myComid.Entities.Values[0].Extensions.Set("Address", &address) + if err != nil { + log.Fatalf("could not set entity Address: %v", err) + } + + refVal := comid.ValueTriple{ + Environment: comid.Environment{ + Class: comid.NewClassImplID(comid.TestImplID). + SetVendor("ACME Ltd."). + SetModel("RoadRunner 2.0"), + }, + Measurements: *comid.NewMeasurements(), + } + + measurement := comid.MustNewPSAMeasurement( + comid.MustCreatePSARefValID( + comid.TestSignerID, "BL", "5.0.5", + )).AddDigest(swid.Sha256_32, []byte{0xab, 0xcd, 0xef, 0x00}) + + // alternatively, we can add extensions to individual value before + // adding it to the collection. Note that because we're adding the + // extension directly to the measurement, we're using a different + // extension point, comid.ExtMval rather than comid.ExtReferenceValue, + // as a measurement doesn't know that its going to be part of reference + // value, ans so is unaware of reference value extension points. + extMap := extensions.NewMap().Add(comid.ExtMval, &RefValExtensions{}) + if err = measurement.Val.RegisterExtensions(extMap); err != nil { + log.Fatal("could not register refval extensions") + } + + refVal.Measurements.Add(measurement) + myComid.Triples.AddReferenceValue(refVal) + + err = myComid.Valid() + if err != nil { + log.Fatalf("comid validity: %v", err) + } + + myCorim.AddComid(*myComid) + + buf, err := myCorim.ToCBOR() + if err != nil { + log.Fatalf("could not encode CoRIM: %v", err) + } + + fmt.Printf("corim: %v", hex.EncodeToString(buf)) + + // output: + // corim: a300f6018158d9d901faa40065656e2d474201a100676578616d706c650281a4006941434d45204c74642e01d8207468747470733a2f2f61636d652e6578616d706c65028101206f3132332046616b652053747265657404a1008182a100a300d90258582061636d652d696d706c656d656e746174696f6e2d69642d303030303030303031016941434d45204c74642e026e526f616452756e6e657220322e3081a200d90259a30162424c0465352e302e35055820acbb11c7e4da217205523ce4ce1a245ae1a239ae3c6bfd9e7871f7e5d8bae86b01a10281820644abcdef00037822687474703a2f2f6578616d706c652e636f6d2f6578616d706c652d70726f66696c65 +} diff --git a/corim/profiles.go b/corim/profiles.go new file mode 100644 index 00000000..7a0f51e7 --- /dev/null +++ b/corim/profiles.go @@ -0,0 +1,326 @@ +// Copyright 2024 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 +package corim + +import ( + "encoding/json" + "fmt" + "reflect" + + "github.com/veraison/corim/comid" + "github.com/veraison/corim/extensions" + "github.com/veraison/eat" + "github.com/veraison/go-cose" +) + +// SignedCorimMapExtensionPoints is a list of extension.Point's valid for a +// SignedCorim. +var SignedCorimMapExtensionPoints = []extensions.Point{ + ExtSigner, + ExtUnsignedCorim, + ExtEntity, +} + +// UnsignedCorimMapExtensionPoints is a list of extension.Point's valid for a +// UnsignedCorim. +var UnsignedCorimMapExtensionPoints = []extensions.Point{ + ExtUnsignedCorim, + ExtEntity, +} + +// ComidMapExtensionPoints is a list of extension.Point's valid for a comid.Comid. +var ComidMapExtensionPoints = []extensions.Point{ + comid.ExtComid, + comid.ExtEntity, + comid.ExtTriples, + comid.ExtReferenceValue, + comid.ExtReferenceValueFlags, + comid.ExtEndorsedValue, + comid.ExtEndorsedValueFlags, +} + +// AllExtensionPoints is a list of all valid extension.Point's +var AllExtensionPoints = make(map[extensions.Point]bool) // populated inside init() below + +// UnmarshalSignedCorimFromCBOR unmarshals a SignedCorim from provided +// CBOR data. If there are extensions associated with the profile specified by +// the data, they will be registered with the UnsignedCorim before it is +// unmarshaled. +func UnmarshalSignedCorimFromCBOR(buf []byte) (*SignedCorim, error) { + message := cose.NewSign1Message() + + if err := message.UnmarshalCBOR(buf); err != nil { + return nil, fmt.Errorf("failed CBOR decoding for COSE-Sign1 signed CoRIM: %w", err) + } + + profiled := struct { + Profile *eat.Profile `cbor:"3,keyasint,omitempty"` + }{} + + if err := dm.Unmarshal(message.Payload, &profiled); err != nil { + return nil, err + } + + ret := GetSignedCorim(profiled.Profile) + if err := ret.FromCOSE(buf); err != nil { + return nil, err + } + + return ret, nil +} + +// UnmarshalUnsignedCorimFromCBOR unmarshals an UnsignedCorim from provided +// CBOR data. If there are extensions associated with the profile specified by +// the data, they will be registered with the UnsignedCorim before it is +// unmarshaled. +func UnmarshalUnsignedCorimFromCBOR(buf []byte) (*UnsignedCorim, error) { + profiled := struct { + Profile *eat.Profile `cbor:"3,keyasint,omitempty"` + }{} + + if err := dm.Unmarshal(buf, &profiled); err != nil { + return nil, err + } + + ret := GetUnsignedCorim(profiled.Profile) + if err := ret.FromCBOR(buf); err != nil { + return nil, err + } + + return ret, nil +} + +// UnmarshalUnsignedCorimFromJSON unmarshals an UnsignedCorim from provided +// JSON data. If there are extensions associated with the profile specified by +// the data, they will be registered with the UnsignedCorim before it is +// unmarshaled. +func UnmarshalUnsignedCorimFromJSON(buf []byte) (*UnsignedCorim, error) { + profiled := struct { + Profile *eat.Profile `cbor:"3,keyasint,omitempty"` + }{} + + if err := json.Unmarshal(buf, &profiled); err != nil { + return nil, err + } + + ret := GetUnsignedCorim(profiled.Profile) + if err := ret.FromJSON(buf); err != nil { + return nil, err + } + + return ret, nil +} + +// UnmarshalComidFromCBOR unmarshals a comid.Comid from provided CBOR data. If +// there are extensions associated with the profile specified by the data, they +// will be registered with the comid.Comid before it is unmarshaled. +func UnmarshalComidFromCBOR(buf []byte, profileID *eat.Profile) (*comid.Comid, error) { + var ret *comid.Comid + + profile, ok := GetProfile(profileID) + if ok { + ret = profile.GetComid() + } else { + ret = comid.NewComid() + } + + if err := ret.FromCBOR(buf); err != nil { + return nil, err + } + + return ret, nil +} + +// GetSingedCorim returns a pointer to a new SingedCorim instance. If there +// are extensions associated with the provided profileID, they will be +// registered with the instance. +func GetSignedCorim(profileID *eat.Profile) *SignedCorim { + var ret *SignedCorim + + if profileID == nil { + ret = NewSignedCorim() + } else { + profile, ok := GetProfile(profileID) + if !ok { + // unknown profile -- treat here like an unprofiled + // CoRIM. While the CoRIM spec states that unknown + // profiles should be rejected, we're not actually + // validating the profile here, just trying to identify + // any extensions we may need to load. Profile + // validation is left up to the calling code, as a + // profile only needs to be registered here if it + // defines extensions. Profiles that do not add any + // additional fields may not be registered. + ret = NewSignedCorim() + } else { + ret = profile.GetSignedCorim() + } + } + + return ret +} + +// GetUnsignedCorim returns a pointer to a new UnsignedCorim instance. If there +// are extensions associated with the provided profileID, they will be +// registered with the instance. +func GetUnsignedCorim(profileID *eat.Profile) *UnsignedCorim { + var ret *UnsignedCorim + + if profileID == nil { + ret = NewUnsignedCorim() + } else { + profile, ok := GetProfile(profileID) + if !ok { + // unknown profile -- treat here like an unprofiled + // CoRIM. While the CoRIM spec states that unknown + // profiles should be rejected, we're not actually + // validating the profile here, just trying to identify + // any extensions we may need to load. Profile + // validation is left up to the calling code, as a + // profile only needs to be registered here if it + // defines extensions. Profiles that do not add any + // additional fields may not be registered. + ret = NewUnsignedCorim() + } else { + ret = profile.GetUnsignedCorim() + } + } + + return ret +} + +// Profile associates an EAT profile ID with a set of extensions. It allows +// obtaining new CoRIM and CoMID structures that had associated extensions +// registered. +type Profile struct { + ID *eat.Profile + MapExtensions extensions.Map +} + +// GetComid returns a pointer to a new comid.Comid that had the Profile's +// extensions (if any) registered. +func (o *Profile) GetComid() *comid.Comid { + ret := comid.NewComid() + o.registerExtensions(ret, ComidMapExtensionPoints) + return ret +} + +// GetUnsignedCorim returns a pointer to a new UnsignedCorim that had the +// Profile's extensions (if any) registered. +func (o *Profile) GetUnsignedCorim() *UnsignedCorim { + ret := NewUnsignedCorim() + ret.Profile = o.ID + o.registerExtensions(ret, UnsignedCorimMapExtensionPoints) + return ret +} + +// GetSignedCorim returns a pointer to a new SignedCorim that had the +// Profile's extensions (if any) registered. +func (o *Profile) GetSignedCorim() *SignedCorim { + ret := NewSignedCorim() + ret.UnsignedCorim.Profile = o.ID + o.registerExtensions(ret, SignedCorimMapExtensionPoints) + return ret +} + +func (o *Profile) registerExtensions(e iextensible, points []extensions.Point) { + exts := extensions.NewMap() + for _, p := range points { + if v, ok := o.MapExtensions[p]; ok { + exts[p] = v + } + } + + if err := e.RegisterExtensions(exts); err != nil { + // exts is a subset of o.MapExtensions which have been + // validated when the profile was registered, so we should never + // get here. + panic(err) + } +} + +// RegisterProfile registers a set of extensions with the specified profile. If +// the profile has already been registered, or if the extensions are invalid, +// an error is returned. +func RegisterProfile(id *eat.Profile, exts extensions.Map) error { + strID, err := id.Get() + if err != nil { + return err + } + + if _, ok := profilesRegister[strID]; ok { + return fmt.Errorf("profile with id %q already registered", strID) + } + + for p, v := range exts { + if _, ok := AllExtensionPoints[p]; !ok { + return fmt.Errorf("%w: %q", extensions.ErrUnexpectedPoint, p) + } + + if reflect.TypeOf(v).Kind() != reflect.Pointer { + return fmt.Errorf("attempting to register a non-pointer IValue for %q", p) + } + } + + profilesRegister[strID] = Profile{ID: id, MapExtensions: exts} + + return nil +} + +// UnregisterProfile ensures there are no extensions registered for the +// specified profile ID. Returns true if extensions were previously registered +// and have been removed, and false otherwise. +func UnregisterProfile(id *eat.Profile) bool { + if id == nil { + return false + } + + strID, err := id.Get() + if err != nil { + return false + } + + if _, ok := profilesRegister[strID]; ok { + delete(profilesRegister, strID) + return true + } + + return false +} + +// GetProfile returns the Profile associated with the specified ID, or an empty +// profile if no Profile has been registered for the id. The second return +// value indicates whether a profile for the ID has been found. +func GetProfile(id *eat.Profile) (Profile, bool) { + if id == nil { + return Profile{}, false + } + + strID, err := id.Get() + if err != nil { + return Profile{}, false + } + + prof, ok := profilesRegister[strID] + return prof, ok +} + +type iextensible interface { + RegisterExtensions(exts extensions.Map) error +} + +var profilesRegister = make(map[string]Profile) + +func init() { + for _, p := range SignedCorimMapExtensionPoints { + AllExtensionPoints[p] = true + } + + for _, p := range UnsignedCorimMapExtensionPoints { + AllExtensionPoints[p] = true + } + + for _, p := range ComidMapExtensionPoints { + AllExtensionPoints[p] = true + } +} diff --git a/corim/profiles_test.go b/corim/profiles_test.go new file mode 100644 index 00000000..c8f41aab --- /dev/null +++ b/corim/profiles_test.go @@ -0,0 +1,187 @@ +// Copyright 2024 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 +package corim + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/veraison/corim/comid" + "github.com/veraison/corim/extensions" + "github.com/veraison/eat" +) + +func TestProfile_registration(t *testing.T) { + exts := extensions.NewMap() + + err := RegisterProfile(&eat.Profile{}, exts) + assert.EqualError(t, err, "no valid EAT profile") + + p1, err := eat.NewProfile("1.2.3") + require.NoError(t, err) + + err = RegisterProfile(p1, exts) + assert.NoError(t, err) + + p2, err := eat.NewProfile("1.2.3") + require.NoError(t, err) + + err = RegisterProfile(p2, exts) + assert.EqualError(t, err, `profile with id "1.2.3" already registered`) + + ret := UnregisterProfile(p2) + assert.True(t, ret) + ret = UnregisterProfile(p2) + assert.False(t, ret) + ret = UnregisterProfile(nil) + assert.False(t, ret) + + err = RegisterProfile(p2, exts) + assert.NoError(t, err) + + prof, ok := GetProfile(p1) + assert.True(t, ok) + assert.Equal(t, exts, prof.MapExtensions) + + _, ok = GetProfile(&eat.Profile{}) + assert.False(t, ok) + + p3, err := eat.NewProfile("2.3.4") + require.NoError(t, err) + + exts2 := extensions.NewMap().Add(extensions.Point("test"), &struct{}{}) + err = RegisterProfile(p3, exts2) + assert.EqualError(t, err, `unexpected extension point: "test"`) + + exts3 := extensions.NewMap().Add(ExtEntity, struct{}{}) + err = RegisterProfile(p3, exts3) + assert.EqualError(t, err, `attempting to register a non-pointer IValue for "CorimEntity"`) + + UnregisterProfile(p1) +} + +func TestProfile_getters(t *testing.T) { + id, err := eat.NewProfile("1.2.3") + require.NoError(t, err) + + profile := Profile{ + ID: id, + MapExtensions: extensions.NewMap(). + Add(comid.ExtComid, &struct{}{}). + Add(ExtUnsignedCorim, &struct{}{}). + Add(ExtSigner, &struct{}{}), + } + + c := profile.GetComid() + assert.NotNil(t, c.Extensions.IValue) + + u := profile.GetUnsignedCorim() + assert.NotNil(t, u.Extensions.IValue) + + s := profile.GetSignedCorim() + assert.NotNil(t, s.UnsignedCorim.Extensions.IValue) + assert.NotNil(t, s.Meta.Signer.Extensions.IValue) +} + +func TestProfile_marshaling(t *testing.T) { + type corimExtensions struct { + Extension1 *string `cbor:"-1,keyasint,omitempty" json:"ext1,omitempty"` + } + + type entityExtensions struct { + Address *string `cbor:"-1,keyasint,omitempty" json:"address,omitempty"` + } + + type refValExtensions struct { + Timestamp *int `cbor:"-1,keyasint,omitempty" json:"timestamp,omitempty"` + } + + profID, err := eat.NewProfile("http://example.com/test-profile") + require.NoError(t, err) + + extMap := extensions.NewMap(). + Add(ExtUnsignedCorim, &corimExtensions{}). + Add(comid.ExtEntity, &entityExtensions{}). + Add(comid.ExtReferenceValue, &refValExtensions{}) + err = RegisterProfile(profID, extMap) + require.NoError(t, err) + + c, err := UnmarshalUnsignedCorimFromCBOR(testGoodUnsignedCorimCBOR) + assert.NoError(t, err) + assert.Nil(t, c.Profile) + + c, err = UnmarshalUnsignedCorimFromCBOR(testUnsignedCorimWithExtensionsCBOR) + assert.NoError(t, err) + + assert.Equal(t, profID, c.Profile) + assert.Equal(t, "foo", c.Extensions.MustGetString("Extension1")) + + profile, ok := GetProfile(c.Profile) + assert.True(t, ok) + + cmd, err := UnmarshalComidFromCBOR(c.Tags[0], c.Profile) + assert.NoError(t, err) + + address := cmd.Entities.Values[0].Extensions.MustGetString("Address") + assert.Equal(t, "123 Fake Street", address) + + ts := cmd.Triples.ReferenceValues.Values[0].Measurements.Values[0]. + Val.Extensions.MustGetInt("timestamp") + assert.Equal(t, 1720782190, ts) + + unregProfID, err := eat.NewProfile("http://example.com") + require.NoError(t, err) + + cmdNoExt, err := UnmarshalComidFromCBOR(c.Tags[0], unregProfID) + assert.NoError(t, err) + + address = cmdNoExt.Entities.Values[0].Extensions.MustGetString("Address") + assert.Equal(t, "", address) + + out, err := c.ToCBOR() + assert.NoError(t, err) + assertCoRIMEq(t, testUnsignedCorimWithExtensionsCBOR, out) + + out, err = cmd.ToCBOR() + assert.NoError(t, err) + // the first 3 bytes in Tags[0] is the tag indicating CoRIM + assertCBOREq(t, c.Tags[0][3:], out) + + c, err = UnmarshalUnsignedCorimFromJSON(testUnsignedCorimJSON) + assert.NoError(t, err) + assert.Nil(t, c.Profile) + + c, err = UnmarshalUnsignedCorimFromJSON(testUnsignedCorimWithExtensionsJSON) + assert.NoError(t, err) + + assert.Equal(t, profID, c.Profile) + assert.Equal(t, "foo", c.Extensions.MustGetString("Extension1")) + + cmd = profile.GetComid() + err = cmd.FromJSON(testComidJSON) + assert.NoError(t, err) + + cmd = profile.GetComid() + err = cmd.FromJSON(testComidWithExtensionsJSON) + assert.NoError(t, err) + + address = cmd.Entities.Values[0].Extensions.MustGetString("Address") + assert.Equal(t, "123 Fake Street", address) + + ts = cmd.Triples.ReferenceValues.Values[0].Measurements.Values[0]. + Val.Extensions.MustGetInt("timestamp") + assert.Equal(t, 1720782190, ts) + + s, err := UnmarshalSignedCorimFromCBOR(testGoodSignedCorimCBOR) + assert.NoError(t, err) + assert.Nil(t, s.UnsignedCorim.Profile) + + s, err = UnmarshalSignedCorimFromCBOR(testSignedCorimWithExtensionsCBOR) + assert.NoError(t, err) + + assert.Equal(t, profID, s.UnsignedCorim.Profile) + assert.Equal(t, "foo", s.UnsignedCorim.Extensions.MustGetString("Extension1")) + + UnregisterProfile(profID) +} diff --git a/corim/signedcorim.go b/corim/signedcorim.go index 32d2acfc..3d7b2158 100644 --- a/corim/signedcorim.go +++ b/corim/signedcorim.go @@ -28,6 +28,11 @@ type SignedCorim struct { message *cose.Sign1Message } +// NewSignedCorim instantiates an empty SignedCorim +func NewSignedCorim() *SignedCorim { + return &SignedCorim{} +} + func (o *SignedCorim) RegisterExtensions(exts extensions.Map) error { unsignedExts := extensions.NewMap() diff --git a/corim/signedcorim_test.go b/corim/signedcorim_test.go index e6ba9e8d..290555e2 100644 --- a/corim/signedcorim_test.go +++ b/corim/signedcorim_test.go @@ -346,7 +346,7 @@ func TestSignedCorim_SignVerify_ok(t *testing.T) { var SignedCorimIn SignedCorim - SignedCorimIn.UnsignedCorim = *unsignedCorimFromCBOR(t, testGoodUnsignedCorim) + SignedCorimIn.UnsignedCorim = *unsignedCorimFromCBOR(t, testGoodUnsignedCorimCBOR) SignedCorimIn.Meta = *metaGood(t) cbor, err := SignedCorimIn.Sign(signer) @@ -373,7 +373,7 @@ func TestSignedCorim_SignVerify_fail_tampered(t *testing.T) { var SignedCorimIn SignedCorim - SignedCorimIn.UnsignedCorim = *unsignedCorimFromCBOR(t, testGoodUnsignedCorim) + SignedCorimIn.UnsignedCorim = *unsignedCorimFromCBOR(t, testGoodUnsignedCorimCBOR) cbor, err := SignedCorimIn.Sign(signer) assert.Nil(t, err) diff --git a/corim/testcases/comid-ext.json b/corim/testcases/comid-ext.json new file mode 100644 index 00000000..541113ec --- /dev/null +++ b/corim/testcases/comid-ext.json @@ -0,0 +1,51 @@ +{ + "lang": "en-GB", + "tag-identity": { + "id": "43BBE37F-2E61-4B33-AED3-53CFF1428B16", + "version": 0 + }, + "entities": [ + { + "name": "ACME Ltd.", + "regid": "https://acme.example", + "address": "123 Fake Street", + "roles": [ + "tagCreator", + "creator", + "maintainer" + ] + } + ], + "triples": { + "reference-values": [ + { + "environment": { + "class": { + "id": { + "type": "psa.impl-id", + "value": "YWNtZS1pbXBsZW1lbnRhdGlvbi1pZC0wMDAwMDAwMDE=" + }, + "vendor": "ACME", + "model": "RoadRunner" + } + }, + "measurements": [ + { + "key": { + "type": "cca.platform-config-id", + "value": "cfg v1.0.0" + }, + "value": { + "timestamp": 1720782190, + "raw-value": { + "type": "bytes", + "value": "cmF3dmFsdWUKcmF3dmFsdWUK" + } + } + } + ] + } + ] + } +} + diff --git a/corim/testcases/comid.json b/corim/testcases/comid.json new file mode 100644 index 00000000..974a473b --- /dev/null +++ b/corim/testcases/comid.json @@ -0,0 +1,49 @@ +{ + "lang": "en-GB", + "tag-identity": { + "id": "43BBE37F-2E61-4B33-AED3-53CFF1428B16", + "version": 0 + }, + "entities": [ + { + "name": "ACME Ltd.", + "regid": "https://acme.example", + "roles": [ + "tagCreator", + "creator", + "maintainer" + ] + } + ], + "triples": { + "reference-values": [ + { + "environment": { + "class": { + "id": { + "type": "psa.impl-id", + "value": "YWNtZS1pbXBsZW1lbnRhdGlvbi1pZC0wMDAwMDAwMDE=" + }, + "vendor": "ACME", + "model": "RoadRunner" + } + }, + "measurements": [ + { + "key": { + "type": "cca.platform-config-id", + "value": "cfg v1.0.0" + }, + "value": { + "raw-value": { + "type": "bytes", + "value": "cmF3dmFsdWUKcmF3dmFsdWUK" + } + } + } + ] + } + ] + } +} + diff --git a/corim/testcases/corim-ext.json b/corim/testcases/corim-ext.json new file mode 100644 index 00000000..a04fbe7c --- /dev/null +++ b/corim/testcases/corim-ext.json @@ -0,0 +1,18 @@ +{ + "corim-id": "5c57e8f4-46cd-421b-91c9-08cf93e13cfc", + "profile": "http://example.com/test-profile", + "ext1": "foo", + "validity": { + "not-before": "2021-12-31T00:00:00Z", + "not-after": "2025-12-31T00:00:00Z" + }, + "entities": [ + { + "name": "ACME Ltd.", + "regid": "acme.example", + "roles": [ + "manifestCreator" + ] + } + ] +} diff --git a/corim/testcases/corim.json b/corim/testcases/corim.json new file mode 100644 index 00000000..dfdc9801 --- /dev/null +++ b/corim/testcases/corim.json @@ -0,0 +1,16 @@ +{ + "corim-id": "5c57e8f4-46cd-421b-91c9-08cf93e13cfc", + "validity": { + "not-before": "2021-12-31T00:00:00Z", + "not-after": "2025-12-31T00:00:00Z" + }, + "entities": [ + { + "name": "ACME Ltd.", + "regid": "acme.example", + "roles": [ + "manifestCreator" + ] + } + ] +} diff --git a/corim/testcases/regen-from-src.sh b/corim/testcases/regen-from-src.sh new file mode 100644 index 00000000..5d6501f2 --- /dev/null +++ b/corim/testcases/regen-from-src.sh @@ -0,0 +1,30 @@ +#!/usr/bin/bash +# Copyright 2024 Contributors to the Veraison project. +# SPDX-License-Identifier: Apache-2.0 +set -e + +GEN_TESTCASE=$(go env GOPATH)/bin/gen-testcase + +if [[ ! -f ${GEN_TESTCASE} ]]; then + echo "installing gen-testcase" + go install github.com/setrofim/gen-testcase@v0.0.2 +fi + +testcases=( + good-corim + example-corim + corim-with-extensions +) + +for case in "${testcases[@]}"; do + echo "generating unsigned-${case}.cbor" + + ${GEN_TESTCASE} "src/${case}.yaml" -o "unsigned-${case}.cbor" + + echo "generating signed-${case}.cbor" + + ${GEN_TESTCASE} -s src/ec-p256.jwk -m src/meta.yaml "src/${case}.yaml" \ + -o "signed-${case}.cbor" +done + +echo "done." diff --git a/corim/testcases/signed-corim-with-extensions.cbor b/corim/testcases/signed-corim-with-extensions.cbor new file mode 100644 index 00000000..4bfca682 Binary files /dev/null and b/corim/testcases/signed-corim-with-extensions.cbor differ diff --git a/corim/testcases/signed-example-corim.cbor b/corim/testcases/signed-example-corim.cbor new file mode 100644 index 00000000..f6c4f461 Binary files /dev/null and b/corim/testcases/signed-example-corim.cbor differ diff --git a/corim/testcases/signed-good-corim.cbor b/corim/testcases/signed-good-corim.cbor new file mode 100644 index 00000000..50dae691 Binary files /dev/null and b/corim/testcases/signed-good-corim.cbor differ diff --git a/corim/testcases/src/corim-with-extensions.yaml b/corim/testcases/src/corim-with-extensions.yaml new file mode 100644 index 00000000..2f1a45f1 --- /dev/null +++ b/corim/testcases/src/corim-with-extensions.yaml @@ -0,0 +1,75 @@ +# minimalist unsigned-corim that embeds comid.PSARefValJSONTemplate +# - profile (claim 3) has been set to "http://example.com/test-profile" +# - extension claim -1 has been added to corim +# - extension claim -1 has been added to comid entity +--- +0: test corim id +3: http://example.com/test-profile +-1: foo +1: +- encodedCBOR: + tag: 506 + value: + 0: en-GB + 1: + 0: !!binary |- + Q7vjfy5hSzOu01PP8UKLFg== + 2: + - 0: ACME Ltd. + 1: + tag: 32 + value: https://acme.example + 2: + - 0 + - 1 + - 2 + -1: 123 Fake Street + 4: + 0: + - - 0: + 0: + tag: 600 + value: !!binary |- + YWNtZS1pbXBsZW1lbnRhdGlvbi1pZC0wMDAwMDAwMDE= + 1: ACME + 2: RoadRunner + - - 0: + tag: 601 + value: + 1: BL + 4: 2.1.0 + 5: !!binary |- + rLsRx+TaIXIFUjzkzhokWuGiOa48a/2eeHH35di66Gs= + 1: + 2: + - - 1 + - !!binary |- + h0KPxSKAPTEGXnvOPPA/5HUJZjHl4Hu9eg/eYMTPJcc= + -1: 1720782190 + - 0: + tag: 601 + value: + 1: PRoT + 4: 1.3.5 + 5: !!binary |- + rLsRx+TaIXIFUjzkzhokWuGiOa48a/2eeHH35di66Gs= + 1: + 2: + - - 1 + - !!binary |- + AmOCmYm2/ZVPcrqvL8ZLwuLwHWktTecphuqAj26ZgT8= + -1: 1720782190 + - 0: + tag: 601 + value: + 1: ARoT + 4: 0.1.4 + 5: !!binary |- + rLsRx+TaIXIFUjzkzhokWuGiOa48a/2eeHH35di66Gs= + 1: + 2: + - - 1 + - !!binary |- + o6XnFfDMV0pzw/m+u2vCTzL/1bZ7OHJEwskJ2neaFHg= + -1: 1720782190 + diff --git a/corim/testcases/src/ec-p256.jwk b/corim/testcases/src/ec-p256.jwk new file mode 100644 index 00000000..e3c07719 --- /dev/null +++ b/corim/testcases/src/ec-p256.jwk @@ -0,0 +1,9 @@ +{ + "kty": "EC", + "crv": "P-256", + "x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", + "y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", + "d": "870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE", + "use": "enc", + "kid": "1" +} diff --git a/corim/testcases/src/example-corim.yaml b/corim/testcases/src/example-corim.yaml new file mode 100644 index 00000000..55d45847 --- /dev/null +++ b/corim/testcases/src/example-corim.yaml @@ -0,0 +1,78 @@ +# minimalist unsigned-corim that embeds comid.PSARefValJSONTemplate +# - profile (claim 3) has been set to "http://example.com/example-profile" +# - extension claim -1 has been added to corim +# - extension claim -1 has been added to comid entity +# NOTE: this is the as corim-with-extensions.yaml, except for the profile +# string, which is differentiated so that the example (which uses init() +# to register the profile) doesn't clash with the tests. +--- +0: test corim id +3: http://example.com/example-profile +-1: foo +1: +- encodedCBOR: + tag: 506 + value: + 0: en-GB + 1: + 0: !!binary |- + Q7vjfy5hSzOu01PP8UKLFg== + 2: + - 0: ACME Ltd. + 1: + tag: 32 + value: https://acme.example + 2: + - 0 + - 1 + - 2 + -1: 123 Fake Street + 4: + 0: + - - 0: + 0: + tag: 600 + value: !!binary |- + YWNtZS1pbXBsZW1lbnRhdGlvbi1pZC0wMDAwMDAwMDE= + 1: ACME + 2: RoadRunner + - - 0: + tag: 601 + value: + 1: BL + 4: 2.1.0 + 5: !!binary |- + rLsRx+TaIXIFUjzkzhokWuGiOa48a/2eeHH35di66Gs= + 1: + 2: + - - 1 + - !!binary |- + h0KPxSKAPTEGXnvOPPA/5HUJZjHl4Hu9eg/eYMTPJcc= + -1: 1720782190 + - 0: + tag: 601 + value: + 1: PRoT + 4: 1.3.5 + 5: !!binary |- + rLsRx+TaIXIFUjzkzhokWuGiOa48a/2eeHH35di66Gs= + 1: + 2: + - - 1 + - !!binary |- + AmOCmYm2/ZVPcrqvL8ZLwuLwHWktTecphuqAj26ZgT8= + -1: 1720782190 + - 0: + tag: 601 + value: + 1: ARoT + 4: 0.1.4 + 5: !!binary |- + rLsRx+TaIXIFUjzkzhokWuGiOa48a/2eeHH35di66Gs= + 1: + 2: + - - 1 + - !!binary |- + o6XnFfDMV0pzw/m+u2vCTzL/1bZ7OHJEwskJ2neaFHg= + -1: 1720782190 + diff --git a/corim/testcases/src/good-corim.yaml b/corim/testcases/src/good-corim.yaml new file mode 100644 index 00000000..4e61fee6 --- /dev/null +++ b/corim/testcases/src/good-corim.yaml @@ -0,0 +1,66 @@ +# minimalist unsigned-corim that embeds comid.PSARefValJSONTemplate +--- +0: test corim id +1: +- encodedCBOR: + tag: 506 + value: + 0: en-GB + 1: + 0: !!binary |- + Q7vjfy5hSzOu01PP8UKLFg== + 2: + - 0: ACME Ltd. + 1: + tag: 32 + value: https://acme.example + 2: + - 0 + - 1 + - 2 + 4: + 0: + - - 0: + 0: + tag: 600 + value: !!binary |- + YWNtZS1pbXBsZW1lbnRhdGlvbi1pZC0wMDAwMDAwMDE= + 1: ACME + 2: RoadRunner + - - 0: + tag: 601 + value: + 1: BL + 4: 2.1.0 + 5: !!binary |- + rLsRx+TaIXIFUjzkzhokWuGiOa48a/2eeHH35di66Gs= + 1: + 2: + - - 1 + - !!binary |- + h0KPxSKAPTEGXnvOPPA/5HUJZjHl4Hu9eg/eYMTPJcc= + - 0: + tag: 601 + value: + 1: PRoT + 4: 1.3.5 + 5: !!binary |- + rLsRx+TaIXIFUjzkzhokWuGiOa48a/2eeHH35di66Gs= + 1: + 2: + - - 1 + - !!binary |- + AmOCmYm2/ZVPcrqvL8ZLwuLwHWktTecphuqAj26ZgT8= + - 0: + tag: 601 + value: + 1: ARoT + 4: 0.1.4 + 5: !!binary |- + rLsRx+TaIXIFUjzkzhokWuGiOa48a/2eeHH35di66Gs= + 1: + 2: + - - 1 + - !!binary |- + o6XnFfDMV0pzw/m+u2vCTzL/1bZ7OHJEwskJ2neaFHg= + diff --git a/corim/testcases/src/meta.yaml b/corim/testcases/src/meta.yaml new file mode 100644 index 00000000..abffebe3 --- /dev/null +++ b/corim/testcases/src/meta.yaml @@ -0,0 +1,13 @@ +--- +1: + 0: + tag: 1 + value: 1640908800 + 1: + tag: 1 + value: 1767139200 +0: + 1: + tag: 32 + value: https://acme.example + 0: ACME Ltd signing key diff --git a/corim/testcases/unsigned-corim-with-extensions.cbor b/corim/testcases/unsigned-corim-with-extensions.cbor new file mode 100644 index 00000000..0e056df9 Binary files /dev/null and b/corim/testcases/unsigned-corim-with-extensions.cbor differ diff --git a/corim/testcases/unsigned-example-corim.cbor b/corim/testcases/unsigned-example-corim.cbor new file mode 100644 index 00000000..0c715c8f Binary files /dev/null and b/corim/testcases/unsigned-example-corim.cbor differ diff --git a/corim/testcases/unsigned-good-corim.cbor b/corim/testcases/unsigned-good-corim.cbor new file mode 100644 index 00000000..3c1fcb6c Binary files /dev/null and b/corim/testcases/unsigned-good-corim.cbor differ diff --git a/corim/unsignedcorim_test.go b/corim/unsignedcorim_test.go index 1e0b14dc..ff3ca173 100644 --- a/corim/unsignedcorim_test.go +++ b/corim/unsignedcorim_test.go @@ -16,11 +16,6 @@ import ( "github.com/veraison/swid" ) -var ( - // minimalist unsigned-corim that embeds comid.PSARefValJSONTemplate - testGoodUnsignedCorim = comid.MustHexDecode(nil, "a2006d7465737420636f72696d20696401815901a3d901faa40065656e2d474201a1005043bbe37f2e614b33aed353cff1428b160281a3006941434d45204c74642e01d8207468747470733a2f2f61636d652e6578616d706c65028300010204a1008182a100a300d90258582061636d652d696d706c656d656e746174696f6e2d69642d303030303030303031016441434d45026a526f616452756e6e657283a200d90259a30162424c0465322e312e30055820acbb11c7e4da217205523ce4ce1a245ae1a239ae3c6bfd9e7871f7e5d8bae86b01a102818201582087428fc522803d31065e7bce3cf03fe475096631e5e07bbd7a0fde60c4cf25c7a200d90259a3016450526f540465312e332e35055820acbb11c7e4da217205523ce4ce1a245ae1a239ae3c6bfd9e7871f7e5d8bae86b01a10281820158200263829989b6fd954f72baaf2fc64bc2e2f01d692d4de72986ea808f6e99813fa200d90259a3016441526f540465302e312e34055820acbb11c7e4da217205523ce4ce1a245ae1a239ae3c6bfd9e7871f7e5d8bae86b01a1028182015820a3a5e715f0cc574a73c3f9bebb6bc24f32ffd5b67b387244c2c909da779a1478") -) - func TestUnsignedCorim_id_string(t *testing.T) { testIDString := "test string" @@ -72,9 +67,9 @@ func TestUnsignedCorim_AddComid_and_marshal(t *testing.T) { fmt.Printf("CBOR: %x", actual) - expected := testGoodUnsignedCorim + expected := testGoodUnsignedCorimCBOR - assert.Equal(t, expected, actual) + assertCoRIMEq(t, expected, actual) } func TestUnsignedCorim_AddCots_and_marshal(t *testing.T) { @@ -120,7 +115,7 @@ func TestUnsignedCorim_AddCoswid_and_marshal(t *testing.T) { } func TestUnsignedCorim_unmarshal(t *testing.T) { - tv := testGoodUnsignedCorim + tv := testGoodUnsignedCorimCBOR var unsignedCorim UnsignedCorim