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
26 changes: 26 additions & 0 deletions arc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

* synthesising attestation results in EAR (EAT Attestation Result) format,
* cryptographically verifying and displaying the contents of an EAR
* printing the EAR header and payload without verification

## Create

Expand Down Expand Up @@ -53,6 +54,10 @@ arc verify \
| `--color` | trustworthiness vector report colourises the tiers (default is B&W) |
| `<jwt-file>` | a JWT wrapping an EAR claims-set |

If the `--pkey` parameter is omitted or the default file name is specified,
the key from the file will be used if it exists, ignoring the keys in the JWT header.
Instead, if the file is missing, the public key and algorithm from the JWT header will be used.

### Output

* Validation status of the cryptographic signature.
Expand All @@ -61,3 +66,24 @@ If successful:

* The EAR claims-set is printed to stdout.
* If present, the _decoded_ trust vector is also printed to stdout (the exact format depends on `--verbose` and `--color`).

## Print

The `print` sub-command is used to print the contents of a EAR, including the header.
Neither EAR validation nor verification is executed.

```sh
arc verify <jwt-file>
```

### Parameters

| parameter | meaning |
| --- | --- |
| `<jwt-file>` | a JWT wrapping an EAR claims-set |

### Output

If EAR is successfully parsed:

* The EAR header and payload are printed to stdout.
97 changes: 97 additions & 0 deletions arc/cmd/print.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright 2025 Contributors to the Veraison project.
// SPDX-License-Identifier: Apache-2.0
package cmd

import (
"encoding/json"
"errors"
"fmt"

"github.com/spf13/afero"
"github.com/spf13/cobra"

"github.com/lestrrat-go/jwx/v3/jws"
"github.com/lestrrat-go/jwx/v3/jwt"
)

var (
printInput string
)

var printCmd = NewPrintCmd()

func NewPrintCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "print [flags] <jwt-file>",
Short: "Read an EAR from a file and print its header and payload",
Long: `Read an EAR from a file and print its header and payload

Neither EAR validation nor verification is executed.

arc print my-ear.jwt
`,
RunE: func(cmd *cobra.Command, args []string) error {
var (
data, arBytes []byte
err error
token jwt.Token
)

if err = checkPrintArgs(args); err != nil {
return fmt.Errorf("validating arguments: %w", err)
}

printInput = args[0]

if arBytes, err = afero.ReadFile(fs, printInput); err != nil {
return fmt.Errorf("reading JWT from %q: %w", printInput, err)
}

msg, err := jws.Parse(arBytes)
if err != nil {
return fmt.Errorf("failed to parse serialized JWT: %s", err)
}
// While JWT enveloped with JWS in compact format only has 1 signature,
// a generic JWS message may have multiple signatures. Therefore, we
// need to access the first element
if data, err = json.MarshalIndent(msg.Signatures()[0].ProtectedHeaders(), "", " "); err != nil {
return fmt.Errorf("unable to re-serialize the EAR claims-set: %w", err)
}
fmt.Println("[header]")
fmt.Println(string(data))

if token, err = jwt.ParseInsecure(arBytes); err != nil {
return fmt.Errorf("failed to parse JWT message: %w", err)
}

claims := make(map[string]any)
for _, k := range token.Keys() {
var v any
if err = token.Get(k, &v); err != nil {
return fmt.Errorf(`failed to get claim %s: %w`, k, err)
}
claims[k] = v
}
if data, err = json.MarshalIndent(claims, "", " "); err != nil {
return fmt.Errorf("unable to re-serialize the EAR claims-set: %w", err)
}
fmt.Println("[payload]")
fmt.Println(string(data))

return nil
},
}

return cmd
}

func checkPrintArgs(args []string) error {
if len(args) != 1 {
return errors.New("no input file supplied")
}
return nil
}

func init() {
rootCmd.AddCommand(printCmd)
}
88 changes: 88 additions & 0 deletions arc/cmd/print_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright 2025 Contributors to the Veraison project.
// SPDX-License-Identifier: Apache-2.0
package cmd

import (
"testing"

"github.com/stretchr/testify/assert"
)

func Test_PrintCmd_unknown_argument(t *testing.T) {
cmd := NewPrintCmd()

args := []string{"--unknown-argument=val"}
cmd.SetArgs(args)

err := cmd.Execute()
assert.EqualError(t, err, "unknown flag: --unknown-argument")
}

func Test_PrintCmd_no_input_file(t *testing.T) {
cmd := NewPrintCmd()

cmd.SetArgs([]string{})

err := cmd.Execute()
assert.EqualError(t, err, "validating arguments: no input file supplied")
}

func Test_PrintCmd_input_file_not_found(t *testing.T) {
cmd := NewPrintCmd()

files := []fileEntry{}
makeFS(t, files)

args := []string{
"non-existent-ear.jwt",
}
cmd.SetArgs(args)

expectedErr := `reading JWT from "non-existent-ear.jwt": open non-existent-ear.jwt: file does not exist`

err := cmd.Execute()
assert.EqualError(t, err, expectedErr)
}

func Test_PrintCmd_input_file_bad_format(t *testing.T) {
cmd := NewPrintCmd()

emptiness := []byte{}

files := []fileEntry{
{"ear.jwt", emptiness},
}
makeFS(t, files)

args := []string{
"ear.jwt",
}
cmd.SetArgs(args)

expectedErr := `failed to parse serialized JWT: jws.Parse: invalid byte sequence`

err := cmd.Execute()
assert.EqualError(t, err, expectedErr)
}

func Test_PrintCmd_ok(t *testing.T) {
cmd := NewPrintCmd()

args := []string{
"ear.jwt",
}
cmd.SetArgs(args)

// all test JWTs should be printed without errors
test_JWTs := [][]byte{testJWT, testJWT_JWK, testRealmJWT}

for _, jwt := range test_JWTs {
files := []fileEntry{
{"ear.jwt", jwt},
}
makeFS(t, files)

err := cmd.Execute()
assert.NoError(t, err)
}
}
2 changes: 1 addition & 1 deletion arc/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ var (
var rootCmd = &cobra.Command{
Use: "arc",
Short: "EAR (EAT Attestation Result) command line utility",
Version: "0.0.1",
Version: "1.1.3",
SilenceUsage: true,
SilenceErrors: true,
}
Expand Down
5 changes: 5 additions & 0 deletions arc/cmd/test_vars.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,10 @@ var (
"developer": "Acme Inc."
}
}`)
// no JWK in header
testJWT = []byte(`eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJlYXIucmF3LWV2aWRlbmNlIjoiM3EyLTd3IiwiaWF0IjoxNjY2MDkxMzczLCJlYXIudmVyaWZpZXItaWQiOnsiYnVpbGQiOiJycnRyYXAtdjEuMC4wIiwiZGV2ZWxvcGVyIjoiQWNtZSBJbmMuIn0sImVhdF9wcm9maWxlIjoidGFnOmdpdGh1Yi5jb20sMjAyMzp2ZXJhaXNvbi9lYXIiLCJzdWJtb2RzIjp7InRlc3QiOnsiZWFyLnN0YXR1cyI6ImFmZmlybWluZyIsImVhci50cnVzdHdvcnRoaW5lc3MtdmVjdG9yIjp7Imluc3RhbmNlLWlkZW50aXR5IjoyLCJjb25maWd1cmF0aW9uIjoyLCJleGVjdXRhYmxlcyI6MywiZmlsZS1zeXN0ZW0iOjIsImhhcmR3YXJlIjoyLCJydW50aW1lLW9wYXF1ZSI6Miwic3RvcmFnZS1vcGFxdWUiOjIsInNvdXJjZWQtZGF0YSI6Mn0sImVhci5hcHByYWlzYWwtcG9saWN5LWlkIjoiaHR0cHM6Ly92ZXJhaXNvbi5leGFtcGxlL3BvbGljeS8xLzYwYTAwNjhkIn19fQ.8_kjzkq4nwp-LV04mK5a86FPMzllaKipboE3rg3T973lHdgsb1LG5Gndfj9R_zRAc6M4XIyt6ce8bQNVdIKtmg`) // nolint: lll
// test token with JWK in header
testJWT_JWK = []byte(`eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6InVzV3hISzJQbWZuSEt3WFBTNTRtMGtUY0dKOTBVaWdsV2lHYWh0YWdudjgiLCJ5IjoiSUJPTC1DM0J0dFZpdmctbFNyZUFTanBrdHRjc3otMXJiN2J0S0x2OEVYNCJ9fQ.eyJlYXIucmF3LWV2aWRlbmNlIjoiM3EyLTd3IiwiZWFyLnZlcmlmaWVyLWlkIjp7ImJ1aWxkIjoicnJ0cmFwLXYxLjAuMCIsImRldmVsb3BlciI6IkFjbWUgSW5jLiJ9LCJlYXRfcHJvZmlsZSI6InRhZzpnaXRodWIuY29tLDIwMjM6dmVyYWlzb24vZWFyIiwiaWF0IjoxNjY2MDkxMzczLCJzdWJtb2RzIjp7InRlc3QiOnsiZWFyLmFwcHJhaXNhbC1wb2xpY3ktaWQiOiJodHRwczovL3ZlcmFpc29uLmV4YW1wbGUvcG9saWN5LzEvNjBhMDA2OGQiLCJlYXIuc3RhdHVzIjoiYWZmaXJtaW5nIiwiZWFyLnRydXN0d29ydGhpbmVzcy12ZWN0b3IiOnsiY29uZmlndXJhdGlvbiI6MiwiZXhlY3V0YWJsZXMiOjMsImZpbGUtc3lzdGVtIjoyLCJoYXJkd2FyZSI6MiwiaW5zdGFuY2UtaWRlbnRpdHkiOjIsInJ1bnRpbWUtb3BhcXVlIjoyLCJzb3VyY2VkLWRhdGEiOjIsInN0b3JhZ2Utb3BhcXVlIjoyfX19fQ.kQkr_tjdajVBdDAiTFgmxtUTYosd-KA5FWzVUxWsIAXnDeuF8kthMGBv6r36sMS6APd3a5NMD7uvLaSyL_FciQ`) // nolint: lll
// Trustee realm expired token
testRealmJWT = []byte(`eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImp3ayI6eyJhbGciOiJFUzI1NiIsImt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiS1J1Z0YwMk9CUjdKME9wM1JhZHJ5dWtlUGhLQ1FuQVZXZ2FqUGJOV0tqRSIsInkiOiJ1dDJZT2NlUDN5cTVqdkNGNk5iZVZpQkNlQjUwUUVPUzNZWXJZTHlDeHJFIn19.eyJlYXRfcHJvZmlsZSI6InRhZzpnaXRodWIuY29tLDIwMjQ6Y29uZmlkZW50aWFsLWNvbnRhaW5lcnMvVHJ1c3RlZSIsImlhdCI6MTc1MjA1OTEzOSwiZWFyLnZlcmlmaWVyLWlkIjp7ImRldmVsb3BlciI6Imh0dHBzOi8vY29uZmlkZW50aWFsY29udGFpbmVycy5vcmciLCJidWlsZCI6ImF0dGVzdGF0aW9uLXNlcnZpY2UgMC4xLjAifSwic3VibW9kcyI6eyJjcHUwIjp7ImVhci5zdGF0dXMiOiJhZmZpcm1pbmciLCJlYXIudHJ1c3R3b3J0aGluZXNzLXZlY3RvciI6eyJjb25maWd1cmF0aW9uIjozLCJleGVjdXRhYmxlcyI6MywiaGFyZHdhcmUiOjJ9LCJlYXIuYXBwcmFpc2FsLXBvbGljeS1pZCI6ImRlZmF1bHQiLCJlYXIudmVyYWlzb24uYW5ub3RhdGVkLWV2aWRlbmNlIjp7ImNjYSI6eyJwbGF0Zm9ybSI6eyJjY2EtcGxhdGZvcm0taW1wbGVtZW50YXRpb24taWQiOiJmMFZNUmdJQkFRQUFBQUFBQUFBQUFBTUFQZ0FCQUFBQVVGZ0FBQUFBQUFBPSIsImNjYS1wbGF0Zm9ybS1pbnN0YW5jZS1pZCI6IkFRY0dCUVFEQWdFQUR3NE5EQXNLQ1FnWEZoVVVFeElSRUI4ZUhSd2JHaGtZIn0sInJlYWxtIjp7ImNjYS1yZWFsbS1jaGFsbGVuZ2UiOiJSUGs4dy9HZnRmaEg4R2U3aWFFNmFEcXdHbzNZM2RJMURnajl2NWY3dmRuR0Zld3prQWdEZEprZmJGNGZZM0l2QUFBQUFBQUFBQUFBQUFBQUFBQUFBQT09IiwiY2NhLXJlYWxtLWV4dGVuc2libGUtbWVhc3VyZW1lbnRzIjpbIkFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE9IiwiQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQT0iLCJBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBPSIsIkFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE9Il0sImNjYS1yZWFsbS1oYXNoLWFsZ28taWQiOiJzaGEtMjU2IiwiY2NhLXJlYWxtLWluaXRpYWwtbWVhc3VyZW1lbnQiOiIzcS9WUU15T3Jyakc5b1pya3JUc0VuT21TWG1JaWc1MlY4amQycVBiRkRzPSIsImNjYS1yZWFsbS1wZXJzb25hbGl6YXRpb24tdmFsdWUiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQT09In19LCJpbml0X2RhdGEiOiIwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCIsImluaXRfZGF0YV9jbGFpbXMiOm51bGwsInJlcG9ydF9kYXRhIjoiNDRmOTNjYzNmMTlmYjVmODQ3ZjA2N2JiODlhMTNhNjgzYWIwMWE4ZGQ4ZGRkMjM1MGUwOGZkYmY5N2ZiYmRkOWM2MTVlYzMzOTAwODAzNzQ5OTFmNmM1ZTFmNjM3MjJmMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJydW50aW1lX2RhdGFfY2xhaW1zIjp7ImFkZGl0aW9uYWwtZXZpZGVuY2UiOiIiLCJub25jZSI6IkhoNDU5WWI4cDNzY2VtcHRrekM3T2Q1RUJpMTZoYk5VUFRaektSb0ZIL0U9IiwidGVlLXB1YmtleSI6eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlIjoiQVFBQiIsImt0eSI6IlJTQSIsIm4iOiJ5Tk51N2huV1pWbWNWR0xqNGptQWxxX2JYc0Q5WGdTdXR3MUhiVG05cFpNeV9tSTMyNWJhOFJHdGc2X1R4ZTFWMGRVX1Q2ejBGcHE0cDRCQUVfd3laRGxCcG4xVUZMOUNaR09rNkZtR2NWLVJYR3VMOXI0Y2hzNFp1akpKXzBWRUNCM0VkNGVBOFlCY1dyVkhtbXZNVjZoeC1qWWRyeGdOWGtHaE5qR0Jyc0UifX19fX0sImV4cCI6MTc1MjA1OTQzOX0.c_9ljXdd3B6aYns5Xu-h4QjYSWhe94mSna3rvahgR3UV5mwYHlPbO0fMrDjQ9-k-KwCjhWufS5-kuGG9jepo3g`) // nolint: lll
)
65 changes: 44 additions & 21 deletions arc/cmd/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ import (

"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v3/jwk"
"github.com/lestrrat-go/jwx/v3/jws"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/veraison/ear"
)

// The default value for pkey parameter
const defaultPKey = "pkey.json"

var (
verifyInput string
verifyAlg string
Expand All @@ -29,19 +33,22 @@ func NewVerifyCmd() *cobra.Command {
Short: "Read a signed EAR from jwt-file, verify it and pretty-print its content",
Long: `Read a signed EAR from jwt-file, verify it and pretty-print its content

Verify the signed EAR in "my-ear.jwt" using the public key in the default key
file "pkey.json". If cryptographic verification is successful, print the
Verify the signed EAR in "my-ear.jwt" using the public key from a key file.
If the default key file name "pkey.json" is used and file is missing then
use the public key from JWT header.
If cryptographic verification is successful, print the
embedded EAR claims-set and present a report of the trustworthiness vector.

arc verify my-ear.jwt
`,
RunE: func(cmd *cobra.Command, args []string) error {
var (
claimsSet, pKey, arBytes []byte
vfyK jwk.Key
ar ear.AttestationResult
alg jwa.KeyAlgorithm
err error
claimsSet, arBytes []byte
vfyK jwk.Key
vfyAlg jwa.KeyAlgorithm
ar ear.AttestationResult
err error
ok bool
)

if err = checkVerifyArgs(args); err != nil {
Expand All @@ -55,23 +62,39 @@ embedded EAR claims-set and present a report of the trustworthiness vector.
}

// read the verification key from verifyPKey
if pKey, err = afero.ReadFile(fs, verifyPKey); err != nil {
return fmt.Errorf("loading verification key from %q: %w", verifyPKey, err)
}

if vfyK, err = jwk.ParseKey(pKey); err != nil {
return fmt.Errorf("parsing verification key from %q: %w", verifyPKey, err)
}

if alg, err = jwa.KeyAlgorithmFrom(verifyAlg); err != nil {
return fmt.Errorf("parsing algorithm from %q: %w", verifyAlg, err)
if pKey, err := afero.ReadFile(fs, verifyPKey); err != nil {
if verifyPKey != defaultPKey {
return fmt.Errorf("loading verification key from %q: %w", verifyPKey, err)
}
fmt.Println("Using JWK key from JWT header")
msg, err := jws.Parse(arBytes)
if err != nil {
return fmt.Errorf("failed to parse serialized JWT: %s", err)
}
// While JWT enveloped with JWS in compact format only has 1 signature,
// a generic JWS message may have multiple signatures. Therefore, we
// need to access the first element
if vfyK, ok = msg.Signatures()[0].ProtectedHeaders().JWK(); !ok || vfyK == nil {
return fmt.Errorf("failed to get JWK key from JWT header")
}
if vfyAlg, ok = msg.Signatures()[0].ProtectedHeaders().Algorithm(); !ok {
return fmt.Errorf("failed to get key algorithm from JWT header")
}
verifyPKey = "JWK header"
} else {
if vfyK, err = jwk.ParseKey(pKey); err != nil {
return fmt.Errorf("parsing verification key from %q: %w", verifyPKey, err)
}
if vfyAlg, err = jwa.KeyAlgorithmFrom(verifyAlg); err != nil {
return fmt.Errorf("parsing algorithm from %q: %w", verifyAlg, err)
}
}

if err = ar.Verify(arBytes, alg, vfyK); err != nil {
return fmt.Errorf("verifying signed EAR from %s: %w", verifyInput, err)
if err = ar.Verify(arBytes, vfyAlg, vfyK); err != nil {
return fmt.Errorf("verifying signed EAR from %q using %q key: %w", verifyInput, verifyPKey, err)
}

fmt.Printf(">> %q signature successfully verified using %q\n", verifyInput, verifyPKey)
fmt.Printf(">> %q signature successfully verified using %q key\n", verifyInput, verifyPKey)

fmt.Println("[claims-set]")
if claimsSet, err = ar.MarshalJSONIndent("", " "); err != nil {
Expand All @@ -94,7 +117,7 @@ embedded EAR claims-set and present a report of the trustworthiness vector.
}

cmd.Flags().StringVarP(
&verifyPKey, "pkey", "p", "pkey.json", "verification key in JWK format",
&verifyPKey, "pkey", "p", defaultPKey, "verification key in JWK format",
)

cmd.Flags().StringVarP(
Expand Down
Loading