Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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`
Expand Down
134 changes: 134 additions & 0 deletions claims_9783.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
158 changes: 158 additions & 0 deletions claims_9783_test.go
Original file line number Diff line number Diff line change
@@ -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
}
23 changes: 20 additions & 3 deletions pretty_test_vectors.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
`
26 changes: 26 additions & 0 deletions testvectors/cbor/RFC9783ClaimsAll.diag
Original file line number Diff line number Diff line change
@@ -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"
}
3 changes: 2 additions & 1 deletion testvectors/cbor/build-test-vectors.sh
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.}

Expand Down