Skip to content

Commit

Permalink
feat: implement CoRIM profiles
Browse files Browse the repository at this point in the history
- 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
setrofim committed Jul 16, 2024
1 parent bd68db6 commit b89d933
Show file tree
Hide file tree
Showing 24 changed files with 1,309 additions and 10 deletions.
115 changes: 115 additions & 0 deletions corim/common_test.go
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...)
}
199 changes: 199 additions & 0 deletions corim/example_profile_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
// 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 {
// will not error, assuming our profile ID is unique, and we've
// correctly set up the extensions Map above
panic(err)
}
}

// ----- 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
}
Loading

0 comments on commit b89d933

Please sign in to comment.