From 792bda18eef51654d0eaebffaa2272dfd9ab29d4 Mon Sep 17 00:00:00 2001 From: Anton Antonov Date: Fri, 11 Jul 2025 13:29:51 +0100 Subject: [PATCH 1/2] feat: arc: Add print sub-command The print command: - reads a JWT file - parses EAR - prints its header and payload without validation and veryfing Signed-off-by: Anton Antonov --- arc/README.md | 21 ++++++++++ arc/cmd/print.go | 97 +++++++++++++++++++++++++++++++++++++++++++ arc/cmd/print_test.go | 88 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 206 insertions(+) create mode 100644 arc/cmd/print.go create mode 100644 arc/cmd/print_test.go diff --git a/arc/README.md b/arc/README.md index 511bcf2..29e888f 100644 --- a/arc/README.md +++ b/arc/README.md @@ -61,3 +61,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. +No ERA validation or veryfing are executed. + +```sh +arc verify +``` + +### Parameters + +| parameter | meaning | +| --- | --- | +| `` | a JWT wrapping an EAR claims-set | + +### Output + +If EAR is successfully parsed: + +* The EAR header and payload are printed to stdout. diff --git a/arc/cmd/print.go b/arc/cmd/print.go new file mode 100644 index 0000000..3bcc2dc --- /dev/null +++ b/arc/cmd/print.go @@ -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] ", + 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) +} diff --git a/arc/cmd/print_test.go b/arc/cmd/print_test.go new file mode 100644 index 0000000..3bd7852 --- /dev/null +++ b/arc/cmd/print_test.go @@ -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) + } +} From a5821ceb068915955a9238501e90d39b1130840b Mon Sep 17 00:00:00 2001 From: Anton Antonov Date: Fri, 11 Jul 2025 17:40:26 +0100 Subject: [PATCH 2/2] feat: arc: verify command supports public key in JWT header If `--pkey` parameter is omitted or the default file name is specified then - key from the file will be used if exists ignoring keys in JWT header - the public key and algorithm from JWT header will be used if the file is missing Signed-off-by: Anton Antonov --- arc/README.md | 7 +++- arc/cmd/root.go | 2 +- arc/cmd/test_vars.go | 5 +++ arc/cmd/verify.go | 65 +++++++++++++++++++---------- arc/cmd/verify_test.go | 94 +++++++++++++++++++++++++++++++++++++++++- ear.go | 7 +++- 6 files changed, 155 insertions(+), 25 deletions(-) diff --git a/arc/README.md b/arc/README.md index 29e888f..80c257c 100644 --- a/arc/README.md +++ b/arc/README.md @@ -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 @@ -53,6 +54,10 @@ arc verify \ | `--color` | trustworthiness vector report colourises the tiers (default is B&W) | | `` | 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. @@ -65,7 +70,7 @@ If successful: ## Print The `print` sub-command is used to print the contents of a EAR, including the header. -No ERA validation or veryfing are executed. +Neither EAR validation nor verification is executed. ```sh arc verify diff --git a/arc/cmd/root.go b/arc/cmd/root.go index 3b57758..ccd777c 100644 --- a/arc/cmd/root.go +++ b/arc/cmd/root.go @@ -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, } diff --git a/arc/cmd/test_vars.go b/arc/cmd/test_vars.go index e674267..8a0569f 100644 --- a/arc/cmd/test_vars.go +++ b/arc/cmd/test_vars.go @@ -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 ) diff --git a/arc/cmd/verify.go b/arc/cmd/verify.go index 432e050..d5cbc35 100644 --- a/arc/cmd/verify.go +++ b/arc/cmd/verify.go @@ -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 @@ -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 { @@ -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 { @@ -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( diff --git a/arc/cmd/verify_test.go b/arc/cmd/verify_test.go index bcca3b5..862b67f 100644 --- a/arc/cmd/verify_test.go +++ b/arc/cmd/verify_test.go @@ -107,7 +107,7 @@ func Test_VerifyCmd_input_file_bad_format(t *testing.T) { } cmd.SetArgs(args) - expectedErr := `verifying signed EAR from ear.jwt: failed verifying JWT message: jwt.Parse: failed to parse token: jwt.verifyFast: failed to split compact: jwsbb: invalid number of segments` + expectedErr := `verifying signed EAR from "ear.jwt" using "pkey.json" key: failed verifying JWT message: jwt.Parse: failed to parse token: jwt.verifyFast: failed to split compact: jwsbb: invalid number of segments` err := cmd.Execute() assert.EqualError(t, err, expectedErr) @@ -135,6 +135,98 @@ func Test_VerifyCmd_unknown_verification_alg(t *testing.T) { assert.EqualError(t, err, expectedErr) } +func Test_VerifyCmd_missing_header_key(t *testing.T) { + cmd := NewVerifyCmd() + + files := []fileEntry{ + {"ear.jwt", testJWT}, + } + makeFS(t, files) + + args := []string{ + "ear.jwt", + } + cmd.SetArgs(args) + + expectedErr := `failed to get JWK key from JWT header` + + err := cmd.Execute() + assert.EqualError(t, err, expectedErr) +} + +func Test_VerifyCmd_incorrect_jws(t *testing.T) { + cmd := NewVerifyCmd() + + files := []fileEntry{ + {"ear.jwt", testJWT_JWK[1:]}, + } + makeFS(t, files) + + args := []string{ + "ear.jwt", + } + cmd.SetArgs(args) + + expectedErr := `failed to parse serialized JWT: jws.Parse: failed to parse compact format: failed to decode protected headers: failed to decode source: illegal base64 data at input byte 212` + + err := cmd.Execute() + assert.EqualError(t, err, expectedErr) +} + +func Test_VerifyCmd_header_key_and_expired_token(t *testing.T) { + cmd := NewVerifyCmd() + + files := []fileEntry{ + {"ear.jwt", testRealmJWT}, + } + makeFS(t, files) + + args := []string{ + "ear.jwt", + } + cmd.SetArgs(args) + + expectedErr := `jwt.Validate: validation failed: "exp" not satisfied: token is expired` + + err := cmd.Execute() + assert.ErrorContains(t, err, expectedErr) +} + +func Test_VerifyCmd_header_key_ignore_alg(t *testing.T) { + cmd := NewVerifyCmd() + + files := []fileEntry{ + {"ear.jwt", testJWT_JWK}, + } + makeFS(t, files) + + args := []string{ + "--alg=XYZ", + "ear.jwt", + } + cmd.SetArgs(args) + + err := cmd.Execute() + assert.NoError(t, err) +} + +func Test_VerifyCmd_header_key_ok(t *testing.T) { + cmd := NewVerifyCmd() + + files := []fileEntry{ + {"ear.jwt", testJWT_JWK}, + } + makeFS(t, files) + + args := []string{ + "ear.jwt", + } + cmd.SetArgs(args) + + err := cmd.Execute() + assert.NoError(t, err) +} + func Test_VerifyCmd_ok(t *testing.T) { cmd := NewVerifyCmd() diff --git a/ear.go b/ear.go index 165f343..583250a 100644 --- a/ear.go +++ b/ear.go @@ -18,6 +18,11 @@ import ( // EatProfile is the EAT profile implemented by this package const EatProfile = "tag:github.com,2023:veraison/ear" +// Trustee profile name which is an alias for the Veraison one. +// Both names will be replaced with a neutral one: +// https://github.com/ietf-rats-wg/draft-ietf-rats-ear/pull/47 +const EatTrusteeProfile = "tag:github.com,2024:confidential-containers/Trustee" + // AttestationResult represents the result of one or more evidence Appraisals // by the verifier. It is serialized to JSON and signed by the verifier using // JWT. @@ -140,7 +145,7 @@ func (o AttestationResult) validate() error { if o.Profile == nil { missing = append(missing, "'eat_profile'") - } else if *o.Profile != EatProfile { + } else if *o.Profile != EatProfile && *o.Profile != EatTrusteeProfile { invalid = append(invalid, fmt.Sprintf("eat_profile (%s)", *o.Profile)) }