diff --git a/README.md b/README.md index c775840..d9cb057 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ This is a compliant implementation of ~~three~~two specifications: * [draft-tschofenig-rats-psa-token-07](https://datatracker.ietf.org/doc/html/draft-tschofenig-rats-psa-token-07) (`PSA_IOT_PROFILE_1`), and * [draft-tschofenig-rats-psa-token-09](https://datatracker.ietf.org/doc/html/draft-tschofenig-rats-psa-token-09) (`http://arm.com/psa/2.0.0`) +* [RFC9783](https://datatracker.ietf.org/doc/html/RFC9783) (`tag:psacertified.org,2023:psa#tfm`) * ~~Realm Management Monitor Specificiation [RMM Spec](https://developer.arm.com/documentation/den0137/latest)~~ > [!Note] @@ -22,7 +23,7 @@ The package exposes the following functionalities: # Implementing new profiles -It is possible to support PSA-derived profiles other than profiles 1 and 2 +It is possible to support PSA-derived profiles other than profiles 1, 2 and tfm implemented here. To do this you need to provide an implementation of `IClaims` and an implementation of `IProfile` that associates your `IClaims` implementation with an `eat.Profile` value, and then register the `IProfile` diff --git a/claims_9783.go b/claims_9783.go new file mode 100644 index 0000000..1d2ce36 --- /dev/null +++ b/claims_9783.go @@ -0,0 +1,134 @@ +// Copyright 2025 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 + +package psatoken + +import ( + "fmt" + + "github.com/veraison/eat" + "github.com/veraison/psatoken/encoding" +) + +type RFC9783Claims struct { + // embed P2Claims to inherit existing implementation + P2Claims + + // Override BootSeed to use different CBOR key as per RFC 9783 (268 vs 2397) + BootSeed *[]byte `cbor:"268,keyasint,omitempty" json:"psa-boot-seed,omitempty"` +} + +func (o *RFC9783Claims) Validate() error { + // "baseline" validation of P2 claims + if err := ValidateClaims(o); err != nil { + return err + } + + // additional validation: ensure bootseed is between 8 and 32 bytes if present + if _, err := o.GetBootSeed(); err != nil && err != ErrOptionalClaimMissing { + return err + } + + return nil +} + +func (o *RFC9783Claims) SetBootSeed(v []byte) error { + l := len(v) + if l < 8 || l > 32 { + return fmt.Errorf( + "%w: invalid length %d (MUST be between 8 and 32 bytes)", + ErrWrongSyntax, l, + ) + } + + o.BootSeed = &v + + return nil +} + +func (o *RFC9783Claims) GetBootSeed() ([]byte, error) { + if o.BootSeed == nil { + return nil, ErrOptionalClaimMissing + } + + l := len(*o.BootSeed) + if l < 8 || l > 32 { + return nil, fmt.Errorf( + "%w: invalid length %d (MUST be between 8 and 32 bytes)", + ErrWrongSyntax, l, + ) + } + + return *o.BootSeed, nil +} + +// To ensure embedding is handled correctly during marshaling, we need to use +// custom encoding functions, which means implementing the four marshaling +// methods defined by IClaims. + +func (o RFC9783Claims) MarshalCBOR() ([]byte, error) { //nolint:gocritic + return encoding.SerializeStructToCBOR(em, &o) +} + +func (o *RFC9783Claims) UnmarshalCBOR(data []byte) error { + return encoding.PopulateStructFromCBOR(dm, data, o) +} + +func (o RFC9783Claims) MarshalJSON() ([]byte, error) { //nolint:gocritic + return encoding.SerializeStructToJSON(&o) + +} + +func (o *RFC9783Claims) UnmarshalJSON(data []byte) error { + return encoding.PopulateStructFromJSON(data, o) +} + +// Name of the profile associated with RFC9783Claims +const RFC9783ProfileName = "tag:psacertified.org,2023:psa#tfm" + +// factory function for RFC9783Claims +func NewRFC9783Claims() *RFC9783Claims { + p := eat.Profile{} + if err := p.Set(RFC9783ProfileName); err != nil { + // should never get here as using known good constant as input + panic(err) + } + + return &RFC9783Claims{ + P2Claims: P2Claims{ + Profile: &p, + + // We need to provide an implementation of + // ISwComponent; as we're not extending software + // components, we're using the default implmentation + SwComponents: &SwComponents[*SwComponent]{}, + + // setting CanonicalProfile to our profile name, as we will be + // relying on the P2Claims's implementation to validate + // Profile claim, + CanonicalProfile: RFC9783ProfileName, + }, + } +} + +// Implementation of IProfile. This is used to register the new IClaims +// implementation and associated it with the profile name. +type RFC9783Profile struct{} + +func (o RFC9783Profile) GetName() string { + return RFC9783ProfileName +} + +func (o RFC9783Profile) GetClaims() IClaims { + return NewRFC9783Claims() +} + +// Registering the profile inside init() to ensure that it it is available to +// the general NewClaims() and DecodeClaims() functions, and the IClaims +// implementation associated with the profile will automatically be used when +// the profile in the data matches the registered name. +func init() { + if err := RegisterProfile(RFC9783Profile{}); err != nil { + panic(err) + } +} diff --git a/claims_9783_test.go b/claims_9783_test.go new file mode 100644 index 0000000..7460b7e --- /dev/null +++ b/claims_9783_test.go @@ -0,0 +1,158 @@ +// Copyright 2025 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 + +package psatoken + +import ( + "fmt" + "log" +) + +func ExampleRFC9783Claims_unmarshalCBOR() { + input := mustHexDecode(nil, testEncodedRFC9783ClaimsAll) + + claims := NewRFC9783Claims() + + if err := claims.UnmarshalCBOR(input); err != nil { + log.Fatalf("could not decode claims: %v", err) + } + + if err := claims.Validate(); err != nil { + log.Fatalf("could not validate claims: %v", err) + } + + profileName, err := claims.GetProfile() + if err != nil { + log.Fatalf("could not get profile: %v", err) + } + fmt.Printf("Profile: %s\n", profileName) + + // output: + // Profile: tag:psacertified.org,2023:psa#tfm +} + +func ExampleRFC9783Claims_marshalCBOR() { + rfc9783Claims := claims9783ExampleSetup() + + out, err := rfc9783Claims.MarshalCBOR() + if err != nil { + log.Fatalf("could not marshal claims: %v", err) + } + + fmt.Printf("marshaled claims: %x", out) + + // output: + // marshaled claims: a819010c48626f6f747365656419010978217461673a7073616365727469666965642e6f72672c323032333a7073612374666d19095a0119095b19300019095c58200102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2019095f81a20258200102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200558200102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200a58300102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200102030405060708090a0b0c0d0e0f1019010058210102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021 +} + +func ExampleRFC9783Claims_unmarshalJSON() { + input := []byte(` + { + "eat-profile": "tag:psacertified.org,2023:psa#tfm", + "psa-client-id": 1, + "psa-security-lifecycle": 12288, + "psa-implementation-id": "AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA=", + "psa-boot-seed": "AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA=", + "psa-hwver": "1234567890123", + "psa-software-components": [ + { + "measurement-type": "BL", + "measurement-value": "AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA=", + "signer-id": "AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA=" + } + ], + "psa-nonce": "AQIDBAUGBwgJCgsMDQ4PEAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8g", + "psa-instance-id": "AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyAh", + "psa-verification-service-indicator": "https://psa-verifier.org", + "psa-certification-reference": "1234567890123-12345", + "timestamp": 1721138454 + } + `) + + claims := NewRFC9783Claims() + + if err := claims.UnmarshalJSON(input); err != nil { + log.Fatalf("could not decode claims: %v", err) + } + + if err := claims.Validate(); err != nil { + log.Fatalf("could not validate claims: %v", err) + } + + profileName, err := claims.GetProfile() + if err != nil { + log.Fatalf("could not get profile: %v", err) + } + fmt.Printf("Profile: %s\n", profileName) + + // output: + // Profile: tag:psacertified.org,2023:psa#tfm +} + +func ExampleRFC9783Claims_marshalJSON() { + rfc9783Claims := claims9783ExampleSetup() + + out, err := rfc9783Claims.MarshalJSON() + if err != nil { + log.Fatalf("could not marshal claims: %v", err) + } + + fmt.Printf("marshaled claims: %s", string(out)) + + // output: + // marshaled claims: {"psa-boot-seed":"Ym9vdHNlZWQ=","eat-profile":"tag:psacertified.org,2023:psa#tfm","psa-client-id":1,"psa-security-lifecycle":12288,"psa-implementation-id":"AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA=","psa-software-components":[{"measurement-value":"AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA=","signer-id":"AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA="}],"psa-nonce":"AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyABAgMEBQYHCAkKCwwNDg8Q","psa-instance-id":"AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyAh"} +} + +func claims9783ExampleSetup() *RFC9783Claims { + exampleBytes := []byte{ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, + 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, + 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, + 0x1f, 0x20, + } + + // instance id must be 33 bytes + instIDBytes := append(exampleBytes, 0x21) // nolint:gocritic + + // as per our profile, nonce must be 48 bytes + nonceBytes := append(exampleBytes, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, // nolint:gocritic + 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10) + + claims := NewRFC9783Claims() + + if err := claims.SetClientID(1); err != nil { + log.Fatalf("could not set client ID: %v", err) + } + + if err := claims.SetSecurityLifeCycle(12288); err != nil { + log.Fatalf("could not set security life cycle: %v", err) + } + + if err := claims.SetImplID(exampleBytes); err != nil { + log.Fatalf("could not set implementation ID: %v", err) + } + + if err := claims.SetInstID(instIDBytes); err != nil { + log.Fatalf("could not set instance ID: %v", err) + } + + if err := claims.SetNonce(nonceBytes); err != nil { + log.Fatalf("could not set nonce: %v", err) + } + + if err := claims.SetBootSeed([]byte("bootseed")); err != nil { + log.Fatalf("could not set boot seed: %v", err) + } + + swComponents := []ISwComponent{ + &SwComponent{ + MeasurementValue: &exampleBytes, + SignerID: &exampleBytes, + }, + } + if err := claims.SetSoftwareComponents(swComponents); err != nil { + log.Fatalf("could not set software components: %v", err) + } + + return claims +} diff --git a/pretty_test_vectors.go b/pretty_test_vectors.go index c60afb4..0c430ab 100644 --- a/pretty_test_vectors.go +++ b/pretty_test_vectors.go @@ -1,6 +1,3 @@ -// Copyright 2021-2024 Contributors to the Veraison project. -// SPDX-License-Identifier: Apache-2.0 - package psatoken // automatically generated from P1ClaimsAll.diag @@ -165,3 +162,23 @@ aaaaaaaaaaaabbbbbbbbbbbbbbbbccccccccccccccccdddddddddddddddd 6d2e636f6d2f7073612f322e302e3019095e733036303435363532373238 32392d3130303130 ` + +// automatically generated from RFC9783ClaimsAll.diag +var testEncodedRFC9783ClaimsAll = ` +a919010978217461673a7073616365727469666965642e6f72672c323032 +333a7073612374666d19095a0119095b19300019095c582061636d652d69 +6d706c656d656e746174696f6e2d69642d30303030303030303119010c58 +20deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefde +adbeef19095f83a40162424c02582087428fc522803d31065e7bce3cf03f +e475096631e5e07bbd7a0fde60c4cf25c70465322e312e30055820acbb11 +c7e4da217205523ce4ce1a245ae1a239ae3c6bfd9e7871f7e5d8bae86ba4 +016450526f540258200263829989b6fd954f72baaf2fc64bc2e2f01d692d +4de72986ea808f6e99813f0465312e332e35055820acbb11c7e4da217205 +523ce4ce1a245ae1a239ae3c6bfd9e7871f7e5d8bae86ba4016441526f54 +025820a3a5e715f0cc574a73c3f9bebb6bc24f32ffd5b67b387244c2c909 +da779a14780465302e312e34055820acbb11c7e4da217205523ce4ce1a24 +5ae1a239ae3c6bfd9e7871f7e5d8bae86b0a5820414a7c174141b3d0e9a1 +d28af31520f0d42299feac4007ded89d68ae6cd92f19190100582101ceeb +ae7b8927a3227e5303cf5e0f1f7b34bb542ad7250ac03fbcde36ec2f1508 +190960781868747470733a2f2f7073612d76657269666965722e6f7267 +` diff --git a/testvectors/cbor/RFC9783ClaimsAll.diag b/testvectors/cbor/RFC9783ClaimsAll.diag new file mode 100644 index 0000000..fafcf7f --- /dev/null +++ b/testvectors/cbor/RFC9783ClaimsAll.diag @@ -0,0 +1,26 @@ +{ + / eat_profile / 265 : "tag:psacertified.org,2023:psa#tfm", + / psa-client-id / 2394 : 1, + / psa-lifecycle / 2395 : 12288, + / psa-implementation-id / 2396 : 'acme-implementation-id-000000001', + / psa-boot-seed / 268 : h'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef', + / psa-software-components / 2399 : [ { + / measurement-type / 1 : "BL", + / measurement-value / 2 : h'87428fc522803d31065e7bce3cf03fe475096631e5e07bbd7a0fde60c4cf25c7', + / version / 4 : "2.1.0", + / signer-id / 5 : h'acbb11c7e4da217205523ce4ce1a245ae1a239ae3c6bfd9e7871f7e5d8bae86b' + }, { + / measurement-type / 1 : "PRoT", + / measurement-value / 2 : h'0263829989b6fd954f72baaf2fc64bc2e2f01d692d4de72986ea808f6e99813f', + / version / 4 : "1.3.5", + / signer-id / 5 : h'acbb11c7e4da217205523ce4ce1a245ae1a239ae3c6bfd9e7871f7e5d8bae86b' + }, { + / measurement-type / 1 : "ARoT", + / measurement-value / 2 : h'a3a5e715f0cc574a73c3f9bebb6bc24f32ffd5b67b387244c2c909da779a1478', + / version / 4 : "0.1.4", + / signer-id / 5 : h'acbb11c7e4da217205523ce4ce1a245ae1a239ae3c6bfd9e7871f7e5d8bae86b' + } ], + / psa-nonce / 10 : h'414a7c174141b3d0e9a1d28af31520f0d42299feac4007ded89d68ae6cd92f19', + / psa-instance-id / 256 : h'01ceebae7b8927a3227e5303cf5e0f1f7b34bb542ad7250ac03fbcde36ec2f1508', + / psa-verification-service-indicator / 2400 : "https://psa-verifier.org" +} diff --git a/testvectors/cbor/build-test-vectors.sh b/testvectors/cbor/build-test-vectors.sh index 6a8e94d..f45e772 100755 --- a/testvectors/cbor/build-test-vectors.sh +++ b/testvectors/cbor/build-test-vectors.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2022 Contributors to the Veraison project. +# Copyright 2022-2025 Contributors to the Veraison project. # SPDX-License-Identifier: Apache-2.0 set -eu @@ -17,6 +17,7 @@ DIAG_FILES="${DIAG_FILES} P2ClaimsMissingMandatoryNonce" DIAG_FILES="${DIAG_FILES} P2ClaimsInvalidMultiNonce" DIAG_FILES="${DIAG_FILES} P1ClaimsTFM" DIAG_FILES="${DIAG_FILES} P2ClaimsTFM" +DIAG_FILES="${DIAG_FILES} RFC9783ClaimsAll" TV_DOT_GO=${TV_DOT_GO?must be set in the environment.}