extensions

package
v1.7.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 2, 2024 License: Apache-2.0 Imports: 8 Imported by: 0

README

CoRIM specification may be extended by CoRIM Profiles documented in other specifications at well defined points identified in the base CoRIM spec using CDDL extension sockets. CoRIM profiles may

  1. Introduce new data fields to certain objects.
  2. Further constrain allowed values for existing fields.
  3. Introduce new type choices for fields whose values can be one of several types.

This implementation, likewise, allows dependent code to register extension types. This is done via three distinct extension mechanisms:

  1. Structures that allow extensions embed an Extensions object, which allows registering a user-provided struct. The user-provided struct can extend their containing structures in two ways:

    • the fields of the user-provided struct become additional fields in the containing structure.
    • the user-provided struct may define additional constraints on the containing structure by defining an appropriate validation method for it.

    This corresponds to the map-extension sockets in the spec.

  2. Some fields may have values chosen from a set of pre-determined types (e.g., an instance ID may be either a UUID or UEID). These (mostly) correspond to type-choice sockets in the CoRIM spec. The set of allowed types for a field may be extended by registering a factory function for a new type, using the registration function associated with the type choice.

  3. A couple of type-choice sockets ($tag-rel-type-choice, $corim-role-type-choice and $comid-role-type-choice) define what, in effect, are extensible enums. They allow providing additional values, rather than types. This implementation provides registration functions for new values for those types.

[!NOTE] CoRIM also "imports" CDDL from the CoSWID spec. Some of these CoSWID CDDL definitions also feature extension sockets. However, as they are defined in a different spec and are implemented in the veraison/swid library, they cannot be extended using the extension feature provided in the CoRIM library. The extension support in CoRIM library is applicable ONLY to CoRIM and CoMID maps and type choices

Map Extensions

Map extensions allow extending CoRIM and CoMID maps with additional keys, effectively defining new fields for the corresponding structures. In the code base, these can be identified by the embedded Extensions struct. Each extensible type has a corresponding extensions.Point. These are:

type extension point
comid.Comid comid.ExtComid
comid.Entity comid.ExtEntity
comid.FlagsMap comid.ExtReferenceValueFlags, comid.ExtEndorsedValueFlags
comid.Mval comid.ExtReferenceValue, comid.ExtEndorsedValue
comid.Triples comid.ExtTriples
corim.Entity corim.ExtEntity
corim.Signer corim.ExtSigner
corim.UnsignedCorim corim.ExtUnsignedCorim

Note that comid.Mval and comid.FlagsMap are used for both reference values and endorsed values, which may be extended separately. This is why there are two extension points associated with each. Additionally, comid.ExtMval and comid.ExtFlags also exist when you want to register extensions with a comid.Mval or comid.Measurment (and so don't have the context of whether it will be going into a reference or an endorsed value).

The diagram below shows a visual representation of where these extension points originate in the struct hierarchy, and which CoRIM object are "aware" of which extension points:

map extensions

(note: the diagram shows the logical relationships between structures and does not meant to accurately reflect the code -- e.g. it omits the container types, representing them as slices)

To extend the above types, you need to define a struct containing your extensions for each extension point that you want to extend. You then need to create an extensions.Map that maps the extension points to a pointer to an instance of the corresponding struct. Finally, you need to pass the map to the RegisterExtensions() method of an instance of the top-most type that is being extended. For example, if you want to extend the comid, reference values, and entities, you would create a struct defining the extensions for each, call extensions.NewMap() to create a new map, call Add() on the map to add mappings from the three extension points to pointers to empty instances of your structs, and, finally, pass the map to comid.Comid.RegisterExtensions(). This should be done as early as possible, before any marshaling is performed (see the example below).

These types can be extended in two ways: by adding additional fields, and by introducing additional constraints over existing fields.

Adding new fields

To add new fields, simply add them to your extensions struct, ensuring that the cbor and json tags on those fields are set correctly. As CoRIM mandates integer keys, you must use the keyasint option for the cbor tag.

To access the values of those fields, you can call the extended type instance's Extensions.Get() passing in the name of the field you want to access. The name can be either the Go struct's field name, the name specified in the json tag, or (a string containing) the integer specified in the cbor tag.

Get() returns an interface{}. There are equivalent GetInt(), GetString(), etc. methods that perform the required conversions, and return the value of the indicated type, along with possible errors. ("Must" versions of these also exist, e.g. MustGetString(), that do not return an error and instead call panic()).

You can also get the pointer to your extension's instance itself by calling the extended type instance's GetExtensions(). This returns an interface{}, so you will need to type assert to be able to access the fields directly.

Introducing additional constraints

To introduce new constraints, add a method called Constrain<TYPE>(v *<TYPE>) to your extensions struct, where <TYPE> is the name of the type being extended (one of the ones listed above) -- e.g. ConstrainComid(v *comid.Comid) when extending comid.Comid. This method, if it exists, will be invoked inside the extended type instance's Valid() method, passing itself as the parameter.

You do not need to define this method unless you actually want to enforce some constraints (i.e., if you just want to define additional fields).

Example

The following example illustrates how to implement a map extension by extending comid.Entity with the following features:

  1. an optional "email" field
  2. additional constraint on the existing "name" field that it contains a valid UUID (note: since NameEntry is a type choice extensible, this can also be done by defining a new value type for NameEntry -- see the following section).
package main

import (
	"encoding/json"
	"fmt"
	"log"

	"github.com/google/uuid"
	"github.com/jraman567/corim/comid"
	"github.com/jraman567/corim/extensions"
)

// the struct containing the extensions
type EntityExtensions struct {
    // a string field extension
	Email string `cbor:"-1,keyasint,omitempty" json:"email,omitempty"`
}

// custom constraints for Entity
func (o EntityExtensions) ConstrainEntity(val *comid.Entity) error {
	_, err := uuid.Parse(val.EntityName.String())
	if err != nil {
		return fmt.Errorf("invalid UUID: %w", err)
	}

	return nil
}

var sampleText = `
{
      "name": "31fb5abf-023e-4992-aa4e-95f9c1503bfa",
      "regid": "https://acme.example",
      "email": "info@acme.com",
      "roles": [
        "tagCreator",
        "creator",
        "maintainer"
      ]
}
`

func main() {
	var entity comid.Entity
	extMap := extensions.NewMap().Add(comid.ExtEntity, &EntityExtensions{})
	entity.RegisterExtensions(extMap)

	if err := json.Unmarshal([]byte(sampleText), &entity); err != nil {
		log.Fatalf("ERROR: %s", err.Error())
	}

	if err := entity.Valid(); err != nil {
		log.Fatalf("failed to validate: %s", err.Error())
	} else {
		fmt.Println("validation succeeded")
	}

	// obtain the extension field value via a generic getter
	email := entity.Extensions.MustGetString("email")
	fmt.Printf("entity email: %s\n", email)

	// retrive the extensions struct and get value via its field.
	exts := entity.GetExtensions().(*EntityExtensions)
	fmt.Printf("also entity email: %s\n", exts.Email)
}

Profiles

Map extensions may be grouped into profiles. A profile is registered, associating an eat.Profile with an extensions.Map. A registered profile can be obtained by calling corim.GetProfile(), which returns a corim.Profile object which can be used to obtain corim.UnsignedCorim, corim.SignedCorim and comid.Comid instances that have the associated extensions registered. corim.UnmarshalUnsignedCorimFromCBOR() will automatically look up a registered profile based on the eat.Profile in the provided data (there are corresponding functions for JSON and signed CoRIM).

// define extensions
type EntityExtensions struct {
	Address *string `cbor:"-1,keyasint,omitempty" json:"address,omitempty"`
}

type RefValExtensions struct {
	Timestamp *int `cbor:"-1,keyasint,omitempty" json:"timestamp,omitempty"`
}


// register profile
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(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)
	}
}


// use the profile
func main() {
	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)
	}

    // ...
}

Please see example_profile_test.go for the complete example of creating and using CoRIM profiles.

[!NOTE] Currently, only map extensions can be associated with profiles. Type choice and enum value extensions (described below) can only be registered globally.

Type Choice Extensions

Type Choice extensions allow specifying alternative types for existing CoRIM fields by defining a type that implements an appropriate interface and registering it with a CBOR tag.

A type choice struct contains a single field, Value, that contains the actual object represented by the type choice. The Value implements an interface that is specific to the type choice and is derived from ITypeChoiceValue:

type ITypeChoiceValue interface {
	// String returns the string representation of the ITypeChoiceValue.
	String() string
	// Valid returns an error if validation of the ITypeChoiceValue fails,
	// or nil if it succeeds.
	Valid() error
	// Type returns the type name of this ITypeChoiceValue implementation.
	Type() string
}

The following is the full list of type choice structs:

  • comid.ClassID
  • comid.CryptoKey
  • comid.EntityName
  • comid.Group
  • comid.Instance
  • comid.Mkey
  • comid.SVN
  • corim.EntityName

To provide a new value type, the following is required:

  1. Define a type that implements the value interface for the type choice you want to extend. This interface is called I<NAME>Value, where <NAME> is the name of the type choice type(e.g. IClassIDValue). These interfaces always embed ITypeChoiceValue and possibly define additional methods.
  2. Create a factory function for your type, with the signature func (any) (*<NAME>, error), where <NAME> is the name of the type choice type that will contain your value. (Note that the function must return a pointer to the container type choice struct, not to the value type you define.) This function should create an instance of your value type from the provided input and return a new type choice struct instance containing it. The range of valid inputs is up to you, however it must handle null, returning the zero-value for your type in that case.
  3. Register your factory function with the CBOR tag for your new type by passing it to the registration function corresponding to the type choice struct. It will have the name Register<NAME>Type, where <NAME> is the name of the type choice struct that will contain your value (e.g. RegisterClassIDType).

Example

The following example illustrates how to add a new type choice value implementation by extending the CryptoKey type to support DER values.

package main

import (
	"crypto"
	"crypto/x509"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"log"

	"github.com/jraman567/corim/comid"
)

// the CBOR tag to be used for the new type
var DerKeyTag = uint64(9999)

// new implementation of ICryptoKeyValue type
type TaggedDerKey []byte

// The factory function for the new type
func NewTaggedDerKey(k any) (*comid.CryptoKey, error) {
	var b []byte
	var err error

	if k == nil {
		k = *new([]byte)
	}

	switch t := k.(type) {
	case []byte:
		b = t
	case string:
		b, err = base64.StdEncoding.DecodeString(t)
		if err != nil {
			return nil, err
		}
	default:
		return nil, fmt.Errorf("value must be a []byte; found %T", k)
	}

	key := TaggedDerKey(b)

	return &comid.CryptoKey{Value: key}, nil
}

func (o TaggedDerKey) String() string {
	return base64.StdEncoding.EncodeToString(o)
}

func (o TaggedDerKey) Valid() error {
	_, err := o.PublicKey()
	return err
}

func (o TaggedDerKey) Type() string {
	return "pkix-der-key"
}

func (o TaggedDerKey) PublicKey() (crypto.PublicKey, error) {
	if len(o) == 0 {
		return nil, errors.New("key value not set")
	}

	key, err := x509.ParsePKIXPublicKey(o)
	if err != nil {
		return nil, fmt.Errorf("unable to parse public key: %w", err)
	}

	return key, nil
}

var testKeyJSON = `
{
	"type": "pkix-der-key",
	"value": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEW1BvqF+/ry8BWa7ZEMU1xYYHEQ8BlLT4MFHOaO+ICTtIvrEeEpr/sfTAP66H2hCHdb5HEXKtRKod6QLcOLPA1Q=="
}
`

func main() {
    // register the factory function under the CBOR tag.
	if err := comid.RegisterCryptoKeyType(DerKeyTag, NewTaggedDerKey); err != nil {
		log.Fatal(err)
	}

	var key comid.CryptoKey

	if err := json.Unmarshal([]byte(testKeyJSON), &key); err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Decoded DER key: %x\n", key)
}

Enum extensions

The following enum types may be extended with additional values:

  • comid.Rel
  • comid.Role
  • corim.Role

This can be done by calling RegisterRel or RegisterRole, as appropriate, and providing a new uint64 value and corresponding string name.

Example

package main

import (
	"encoding/json"
	"fmt"
	"log"

	"github.com/jraman567/corim/comid"
)

var sampleText = `
{
      "name": "Acme Ltd.",
      "regid": "https://acme.example",
      "roles": [
        "tagCreator",
        "owner"
      ]
}
`

func main() {
    // associate role value 4 with the name "owner"
	comid.RegisterRole(4, "owner")

	var entity comid.Entity

	if err := json.Unmarshal([]byte(sampleText), &entity); err != nil {
		log.Fatalf("ERROR: %s", err.Error())
	}

	if err := entity.Valid(); err != nil {
		log.Fatalf("failed to validate: %s", err.Error())
	} else {
		fmt.Println("validation succeeded")
	}

	fmt.Println("roles:")
	for _, role := range entity.Roles {
		fmt.Printf("\t%s\n", role.String())
	}
}

Documentation

Overview

Copyright 2023-2024 Contributors to the Veraison project. SPDX-License-Identifier: Apache-2.0

Copyright 2024 Contributors to the Veraison project. SPDX-License-Identifier: Apache-2.0

Index

Constants

This section is empty.

Variables

View Source
var ErrExtensionNotFound = errors.New("extension not found")
View Source
var ErrUnexpectedPoint = errors.New("unexpected extension point")
View Source
var StringType = "string"

Functions

func TypeChoiceValueMarshalJSON

func TypeChoiceValueMarshalJSON(v ITypeChoiceValue) ([]byte, error)

Types

type Collection added in v1.6.2

type Collection[P any, I IExtensible[P]] struct {
	Values []P
	// contains filtered or unexported fields
}

Collection is a generic container for objects who's pointers implement IExtensible. In addition of containing instances of extensible objects, it is capable of registering extensions for those instances.

func NewCollection added in v1.6.2

func NewCollection[P any, I IExtensible[P]]() *Collection[P, I]

NewCollection returns a pointer to a new Collections instance.

func (*Collection[P, I]) Add added in v1.6.2

func (o *Collection[P, I]) Add(p *P) *Collection[P, I]

Add a new element to the collection.

func (*Collection[P, I]) Clear added in v1.6.2

func (o *Collection[P, I]) Clear()

Clear empties the collection of its values. This does _not_ unregister extensions.

func (*Collection[P, I]) GetExtensions added in v1.6.2

func (o *Collection[P, I]) GetExtensions() IMapValue

GetExtensions returns the extensions IMapValue that has been registered with the collection.

func (Collection[P, I]) IsEmpty added in v1.6.2

func (o Collection[P, I]) IsEmpty() bool

IsEmpty return true if the collection is empty, i.e. if it does not contain any values. This does not indicate whether or not any extensions have been registered with the collection.

func (Collection[P, I]) MarshalCBOR added in v1.6.2

func (o Collection[P, I]) MarshalCBOR() ([]byte, error)

func (Collection[P, I]) MarshalJSON added in v1.6.2

func (o Collection[P, I]) MarshalJSON() ([]byte, error)

func (*Collection[P, I]) RegisterExtensions added in v1.6.2

func (o *Collection[P, I]) RegisterExtensions(exts Map) error

RegisterExtensions register extensions for the collection's values. An error is returned if the provided map contains extension points not supported by the collection's values.

func (*Collection[P, I]) UnmarshalCBOR added in v1.6.2

func (o *Collection[P, I]) UnmarshalCBOR(data []byte) error

func (*Collection[P, I]) UnmarshalJSON added in v1.6.2

func (o *Collection[P, I]) UnmarshalJSON(data []byte) error

func (Collection[P, I]) Valid added in v1.6.2

func (o Collection[P, I]) Valid() error

Valid returns an error if the collection is invalid, i.e. if it is empty or if any of its contents are invalid.

type Extensions

type Extensions struct {
	IMapValue `json:"extensions,omitempty"`
}

func (*Extensions) Get

func (o *Extensions) Get(name string) (any, error)

func (*Extensions) GetBool

func (o *Extensions) GetBool(name string) (bool, error)

func (*Extensions) GetFloat32

func (o *Extensions) GetFloat32(name string) (float32, error)

func (*Extensions) GetFloat64

func (o *Extensions) GetFloat64(name string) (float64, error)

func (*Extensions) GetInt

func (o *Extensions) GetInt(name string) (int, error)

func (*Extensions) GetInt8

func (o *Extensions) GetInt8(name string) (int8, error)

func (*Extensions) GetInt16

func (o *Extensions) GetInt16(name string) (int16, error)

func (*Extensions) GetInt32

func (o *Extensions) GetInt32(name string) (int32, error)

func (*Extensions) GetInt64

func (o *Extensions) GetInt64(name string) (int64, error)

func (*Extensions) GetIntSlice

func (o *Extensions) GetIntSlice(name string) ([]int, error)

func (*Extensions) GetSlice

func (o *Extensions) GetSlice(name string) ([]any, error)

func (*Extensions) GetString

func (o *Extensions) GetString(name string) (string, error)

func (*Extensions) GetStringMap

func (o *Extensions) GetStringMap(name string) (map[string]any, error)

func (*Extensions) GetStringMapString

func (o *Extensions) GetStringMapString(name string) (map[string]string, error)

func (*Extensions) GetStringSlice

func (o *Extensions) GetStringSlice(name string) ([]string, error)

func (*Extensions) GetUint

func (o *Extensions) GetUint(name string) (uint, error)

func (*Extensions) GetUint8

func (o *Extensions) GetUint8(name string) (uint8, error)

func (*Extensions) GetUint16

func (o *Extensions) GetUint16(name string) (uint16, error)

func (*Extensions) GetUint32

func (o *Extensions) GetUint32(name string) (uint32, error)

func (*Extensions) GetUint64

func (o *Extensions) GetUint64(name string) (uint64, error)

func (*Extensions) HaveExtensions

func (o *Extensions) HaveExtensions() bool

func (*Extensions) IsEmpty added in v1.6.2

func (o *Extensions) IsEmpty() bool

func (*Extensions) MustGetBool

func (o *Extensions) MustGetBool(name string) bool

func (*Extensions) MustGetFloat32

func (o *Extensions) MustGetFloat32(name string) float32

func (*Extensions) MustGetFloat64

func (o *Extensions) MustGetFloat64(name string) float64

func (*Extensions) MustGetInt

func (o *Extensions) MustGetInt(name string) int

func (*Extensions) MustGetInt8

func (o *Extensions) MustGetInt8(name string) int8

func (*Extensions) MustGetInt16

func (o *Extensions) MustGetInt16(name string) int16

func (*Extensions) MustGetInt32

func (o *Extensions) MustGetInt32(name string) int32

func (*Extensions) MustGetInt64

func (o *Extensions) MustGetInt64(name string) int64

func (*Extensions) MustGetIntSlice

func (o *Extensions) MustGetIntSlice(name string) []int

func (*Extensions) MustGetSlice

func (o *Extensions) MustGetSlice(name string) []any

func (*Extensions) MustGetString

func (o *Extensions) MustGetString(name string) string

func (*Extensions) MustGetStringMap

func (o *Extensions) MustGetStringMap(name string) map[string]any

func (*Extensions) MustGetStringMapString

func (o *Extensions) MustGetStringMapString(name string) map[string]string

func (*Extensions) MustGetStringSlice

func (o *Extensions) MustGetStringSlice(name string) []string

func (*Extensions) MustGetUint

func (o *Extensions) MustGetUint(name string) uint

func (*Extensions) MustGetUint8

func (o *Extensions) MustGetUint8(name string) uint8

func (*Extensions) MustGetUint16

func (o *Extensions) MustGetUint16(name string) uint16

func (*Extensions) MustGetUint32

func (o *Extensions) MustGetUint32(name string) uint32

func (*Extensions) MustGetUint64

func (o *Extensions) MustGetUint64(name string) uint64

func (Extensions) New added in v1.6.2

func (o Extensions) New() IMapValue

func (*Extensions) Register

func (o *Extensions) Register(exts IMapValue)

func (*Extensions) Set

func (o *Extensions) Set(name string, value any) error

type IExtensible added in v1.6.2

type IExtensible[P any] interface {
	RegisterExtensions(exts Map) error
	GetExtensions() IMapValue
	Valid() error

	*P // this interface must be implemented by a pointer to P
}

IExtensible defines an interface for extensible objects (those that can register extensions.

type IMapValue added in v1.6.2

type IMapValue any

type ITypeChoiceValue

type ITypeChoiceValue interface {
	// String returns the string representation of the ITypeChoiceValue.
	String() string
	// Valid returns an error if validation of the ITypeChoiceValue fails,
	// or nil if it succeeds.
	Valid() error
	// Type returns the type name of this ITypeChoiceValue implementation.
	Type() string
}

ITypeChoiceValue is the interface that is implemented by all concrete type choice value types. Specific type choices define their own value interfaces that embed this one (and possibly include additional methods).

type Map added in v1.6.2

type Map map[Point]IMapValue

Map is a mapping of extension Point's to IMapValue's (pointers to structs that define extension fields.

func NewMap added in v1.6.2

func NewMap() Map

NewMap instantiates an empty Map

func (Map) Add added in v1.6.2

func (o Map) Add(p Point, v IMapValue) Map

Add a Point-IMapValue entry to the map. If there is already an IMapValue associated with the Point, it will be replaced.

type Point added in v1.6.2

type Point string

Point defines an extension point -- a place where a struct defining extensions may be attached. There is at least one Point corresponding to each struct that embeds Extensions.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL