-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Add RegisterProfile and UnregisterProfile to associate an extensions.Map with an eat.Profile inside a new Profile structure. - Profile is able to instantiate new signed and unsigned CoRIMs, and CoMIDs with appropriate extensions registered to them. - Add unmarshaling functions that attempt to identify the eat.Profile within the raw data, and retrieve an appropriate Profile, which is then used to get an instance with appropriate extensions registered, which can then be unmarshaled fully. Signed-off-by: Sergei Trofimov <[email protected]>
- Loading branch information
Showing
23 changed files
with
1,241 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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...) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
Oops, something went wrong.