Skip to content

Commit 79dc967

Browse files
committed
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 <Anton.Antonov@arm.com>
1 parent bc13686 commit 79dc967

File tree

6 files changed

+155
-25
lines changed

6 files changed

+155
-25
lines changed

arc/README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

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

89
## Create
910

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

57+
If the `--pkey` parameter is omitted or the default file name is specified,
58+
the key from the file will be used if it exists, ignoring the keys in the JWT header.
59+
Instead, if the file is missing, the public key and algorithm from the JWT header will be used.
60+
5661
### Output
5762

5863
* Validation status of the cryptographic signature.
@@ -65,7 +70,7 @@ If successful:
6570
## Print
6671

6772
The `print` sub-command is used to print the contents of a EAR, including the header.
68-
No ERA validation or veryfing are executed.
73+
Neither EAR validation nor verification is executed.
6974

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

arc/cmd/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ var (
2020
var rootCmd = &cobra.Command{
2121
Use: "arc",
2222
Short: "EAR (EAT Attestation Result) command line utility",
23-
Version: "0.0.1",
23+
Version: "1.1.3",
2424
SilenceUsage: true,
2525
SilenceErrors: true,
2626
}

arc/cmd/test_vars.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,10 @@ var (
3232
"developer": "Acme Inc."
3333
}
3434
}`)
35+
// no JWK in header
3536
testJWT = []byte(`eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJlYXIucmF3LWV2aWRlbmNlIjoiM3EyLTd3IiwiaWF0IjoxNjY2MDkxMzczLCJlYXIudmVyaWZpZXItaWQiOnsiYnVpbGQiOiJycnRyYXAtdjEuMC4wIiwiZGV2ZWxvcGVyIjoiQWNtZSBJbmMuIn0sImVhdF9wcm9maWxlIjoidGFnOmdpdGh1Yi5jb20sMjAyMzp2ZXJhaXNvbi9lYXIiLCJzdWJtb2RzIjp7InRlc3QiOnsiZWFyLnN0YXR1cyI6ImFmZmlybWluZyIsImVhci50cnVzdHdvcnRoaW5lc3MtdmVjdG9yIjp7Imluc3RhbmNlLWlkZW50aXR5IjoyLCJjb25maWd1cmF0aW9uIjoyLCJleGVjdXRhYmxlcyI6MywiZmlsZS1zeXN0ZW0iOjIsImhhcmR3YXJlIjoyLCJydW50aW1lLW9wYXF1ZSI6Miwic3RvcmFnZS1vcGFxdWUiOjIsInNvdXJjZWQtZGF0YSI6Mn0sImVhci5hcHByYWlzYWwtcG9saWN5LWlkIjoiaHR0cHM6Ly92ZXJhaXNvbi5leGFtcGxlL3BvbGljeS8xLzYwYTAwNjhkIn19fQ.8_kjzkq4nwp-LV04mK5a86FPMzllaKipboE3rg3T973lHdgsb1LG5Gndfj9R_zRAc6M4XIyt6ce8bQNVdIKtmg`) // nolint: lll
37+
// test token with JWK in header
38+
testJWT_JWK = []byte(`eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6InVzV3hISzJQbWZuSEt3WFBTNTRtMGtUY0dKOTBVaWdsV2lHYWh0YWdudjgiLCJ5IjoiSUJPTC1DM0J0dFZpdmctbFNyZUFTanBrdHRjc3otMXJiN2J0S0x2OEVYNCJ9fQ.eyJlYXIucmF3LWV2aWRlbmNlIjoiM3EyLTd3IiwiZWFyLnZlcmlmaWVyLWlkIjp7ImJ1aWxkIjoicnJ0cmFwLXYxLjAuMCIsImRldmVsb3BlciI6IkFjbWUgSW5jLiJ9LCJlYXRfcHJvZmlsZSI6InRhZzpnaXRodWIuY29tLDIwMjM6dmVyYWlzb24vZWFyIiwiaWF0IjoxNjY2MDkxMzczLCJzdWJtb2RzIjp7InRlc3QiOnsiZWFyLmFwcHJhaXNhbC1wb2xpY3ktaWQiOiJodHRwczovL3ZlcmFpc29uLmV4YW1wbGUvcG9saWN5LzEvNjBhMDA2OGQiLCJlYXIuc3RhdHVzIjoiYWZmaXJtaW5nIiwiZWFyLnRydXN0d29ydGhpbmVzcy12ZWN0b3IiOnsiY29uZmlndXJhdGlvbiI6MiwiZXhlY3V0YWJsZXMiOjMsImZpbGUtc3lzdGVtIjoyLCJoYXJkd2FyZSI6MiwiaW5zdGFuY2UtaWRlbnRpdHkiOjIsInJ1bnRpbWUtb3BhcXVlIjoyLCJzb3VyY2VkLWRhdGEiOjIsInN0b3JhZ2Utb3BhcXVlIjoyfX19fQ.kQkr_tjdajVBdDAiTFgmxtUTYosd-KA5FWzVUxWsIAXnDeuF8kthMGBv6r36sMS6APd3a5NMD7uvLaSyL_FciQ`) // nolint: lll
39+
// Trustee realm expired token
40+
testRealmJWT = []byte(`eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImp3ayI6eyJhbGciOiJFUzI1NiIsImt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiS1J1Z0YwMk9CUjdKME9wM1JhZHJ5dWtlUGhLQ1FuQVZXZ2FqUGJOV0tqRSIsInkiOiJ1dDJZT2NlUDN5cTVqdkNGNk5iZVZpQkNlQjUwUUVPUzNZWXJZTHlDeHJFIn19.eyJlYXRfcHJvZmlsZSI6InRhZzpnaXRodWIuY29tLDIwMjQ6Y29uZmlkZW50aWFsLWNvbnRhaW5lcnMvVHJ1c3RlZSIsImlhdCI6MTc1MjA1OTEzOSwiZWFyLnZlcmlmaWVyLWlkIjp7ImRldmVsb3BlciI6Imh0dHBzOi8vY29uZmlkZW50aWFsY29udGFpbmVycy5vcmciLCJidWlsZCI6ImF0dGVzdGF0aW9uLXNlcnZpY2UgMC4xLjAifSwic3VibW9kcyI6eyJjcHUwIjp7ImVhci5zdGF0dXMiOiJhZmZpcm1pbmciLCJlYXIudHJ1c3R3b3J0aGluZXNzLXZlY3RvciI6eyJjb25maWd1cmF0aW9uIjozLCJleGVjdXRhYmxlcyI6MywiaGFyZHdhcmUiOjJ9LCJlYXIuYXBwcmFpc2FsLXBvbGljeS1pZCI6ImRlZmF1bHQiLCJlYXIudmVyYWlzb24uYW5ub3RhdGVkLWV2aWRlbmNlIjp7ImNjYSI6eyJwbGF0Zm9ybSI6eyJjY2EtcGxhdGZvcm0taW1wbGVtZW50YXRpb24taWQiOiJmMFZNUmdJQkFRQUFBQUFBQUFBQUFBTUFQZ0FCQUFBQVVGZ0FBQUFBQUFBPSIsImNjYS1wbGF0Zm9ybS1pbnN0YW5jZS1pZCI6IkFRY0dCUVFEQWdFQUR3NE5EQXNLQ1FnWEZoVVVFeElSRUI4ZUhSd2JHaGtZIn0sInJlYWxtIjp7ImNjYS1yZWFsbS1jaGFsbGVuZ2UiOiJSUGs4dy9HZnRmaEg4R2U3aWFFNmFEcXdHbzNZM2RJMURnajl2NWY3dmRuR0Zld3prQWdEZEprZmJGNGZZM0l2QUFBQUFBQUFBQUFBQUFBQUFBQUFBQT09IiwiY2NhLXJlYWxtLWV4dGVuc2libGUtbWVhc3VyZW1lbnRzIjpbIkFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE9IiwiQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQT0iLCJBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBPSIsIkFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE9Il0sImNjYS1yZWFsbS1oYXNoLWFsZ28taWQiOiJzaGEtMjU2IiwiY2NhLXJlYWxtLWluaXRpYWwtbWVhc3VyZW1lbnQiOiIzcS9WUU15T3Jyakc5b1pya3JUc0VuT21TWG1JaWc1MlY4amQycVBiRkRzPSIsImNjYS1yZWFsbS1wZXJzb25hbGl6YXRpb24tdmFsdWUiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQT09In19LCJpbml0X2RhdGEiOiIwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCIsImluaXRfZGF0YV9jbGFpbXMiOm51bGwsInJlcG9ydF9kYXRhIjoiNDRmOTNjYzNmMTlmYjVmODQ3ZjA2N2JiODlhMTNhNjgzYWIwMWE4ZGQ4ZGRkMjM1MGUwOGZkYmY5N2ZiYmRkOWM2MTVlYzMzOTAwODAzNzQ5OTFmNmM1ZTFmNjM3MjJmMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJydW50aW1lX2RhdGFfY2xhaW1zIjp7ImFkZGl0aW9uYWwtZXZpZGVuY2UiOiIiLCJub25jZSI6IkhoNDU5WWI4cDNzY2VtcHRrekM3T2Q1RUJpMTZoYk5VUFRaektSb0ZIL0U9IiwidGVlLXB1YmtleSI6eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlIjoiQVFBQiIsImt0eSI6IlJTQSIsIm4iOiJ5Tk51N2huV1pWbWNWR0xqNGptQWxxX2JYc0Q5WGdTdXR3MUhiVG05cFpNeV9tSTMyNWJhOFJHdGc2X1R4ZTFWMGRVX1Q2ejBGcHE0cDRCQUVfd3laRGxCcG4xVUZMOUNaR09rNkZtR2NWLVJYR3VMOXI0Y2hzNFp1akpKXzBWRUNCM0VkNGVBOFlCY1dyVkhtbXZNVjZoeC1qWWRyeGdOWGtHaE5qR0Jyc0UifX19fX0sImV4cCI6MTc1MjA1OTQzOX0.c_9ljXdd3B6aYns5Xu-h4QjYSWhe94mSna3rvahgR3UV5mwYHlPbO0fMrDjQ9-k-KwCjhWufS5-kuGG9jepo3g`) // nolint: lll
3641
)

arc/cmd/verify.go

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@ import (
88

99
"github.com/lestrrat-go/jwx/v3/jwa"
1010
"github.com/lestrrat-go/jwx/v3/jwk"
11+
"github.com/lestrrat-go/jwx/v3/jws"
1112
"github.com/spf13/afero"
1213
"github.com/spf13/cobra"
1314
"github.com/veraison/ear"
1415
)
1516

17+
// The default value for pkey parameter
18+
const defaultPKey = "pkey.json"
19+
1620
var (
1721
verifyInput string
1822
verifyAlg string
@@ -29,19 +33,22 @@ func NewVerifyCmd() *cobra.Command {
2933
Short: "Read a signed EAR from jwt-file, verify it and pretty-print its content",
3034
Long: `Read a signed EAR from jwt-file, verify it and pretty-print its content
3135
32-
Verify the signed EAR in "my-ear.jwt" using the public key in the default key
33-
file "pkey.json". If cryptographic verification is successful, print the
36+
Verify the signed EAR in "my-ear.jwt" using the public key from a key file.
37+
If the default key file name "pkey.json" is used and file is missing then
38+
use the public key from JWT header.
39+
If cryptographic verification is successful, print the
3440
embedded EAR claims-set and present a report of the trustworthiness vector.
3541
3642
arc verify my-ear.jwt
3743
`,
3844
RunE: func(cmd *cobra.Command, args []string) error {
3945
var (
40-
claimsSet, pKey, arBytes []byte
41-
vfyK jwk.Key
42-
ar ear.AttestationResult
43-
alg jwa.KeyAlgorithm
44-
err error
46+
claimsSet, arBytes []byte
47+
vfyK jwk.Key
48+
vfyAlg jwa.KeyAlgorithm
49+
ar ear.AttestationResult
50+
err error
51+
ok bool
4552
)
4653

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

5764
// read the verification key from verifyPKey
58-
if pKey, err = afero.ReadFile(fs, verifyPKey); err != nil {
59-
return fmt.Errorf("loading verification key from %q: %w", verifyPKey, err)
60-
}
61-
62-
if vfyK, err = jwk.ParseKey(pKey); err != nil {
63-
return fmt.Errorf("parsing verification key from %q: %w", verifyPKey, err)
64-
}
65-
66-
if alg, err = jwa.KeyAlgorithmFrom(verifyAlg); err != nil {
67-
return fmt.Errorf("parsing algorithm from %q: %w", verifyAlg, err)
65+
if pKey, err := afero.ReadFile(fs, verifyPKey); err != nil {
66+
if verifyPKey != defaultPKey {
67+
return fmt.Errorf("loading verification key from %q: %w", verifyPKey, err)
68+
}
69+
fmt.Println("Using JWK key from JWT header")
70+
msg, err := jws.Parse(arBytes)
71+
if err != nil {
72+
return fmt.Errorf("failed to parse serialized JWT: %s", err)
73+
}
74+
// While JWT enveloped with JWS in compact format only has 1 signature,
75+
// a generic JWS message may have multiple signatures. Therefore, we
76+
// need to access the first element
77+
if vfyK, ok = msg.Signatures()[0].ProtectedHeaders().JWK(); !ok || vfyK == nil {
78+
return fmt.Errorf("failed to get JWK key from JWT header")
79+
}
80+
if vfyAlg, ok = msg.Signatures()[0].ProtectedHeaders().Algorithm(); !ok {
81+
return fmt.Errorf("failed to get key algorithm from JWT header")
82+
}
83+
verifyPKey = "JWK header"
84+
} else {
85+
if vfyK, err = jwk.ParseKey(pKey); err != nil {
86+
return fmt.Errorf("parsing verification key from %q: %w", verifyPKey, err)
87+
}
88+
if vfyAlg, err = jwa.KeyAlgorithmFrom(verifyAlg); err != nil {
89+
return fmt.Errorf("parsing algorithm from %q: %w", verifyAlg, err)
90+
}
6891
}
6992

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

74-
fmt.Printf(">> %q signature successfully verified using %q\n", verifyInput, verifyPKey)
97+
fmt.Printf(">> %q signature successfully verified using %q key\n", verifyInput, verifyPKey)
7598

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

96119
cmd.Flags().StringVarP(
97-
&verifyPKey, "pkey", "p", "pkey.json", "verification key in JWK format",
120+
&verifyPKey, "pkey", "p", defaultPKey, "verification key in JWK format",
98121
)
99122

100123
cmd.Flags().StringVarP(

arc/cmd/verify_test.go

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ func Test_VerifyCmd_input_file_bad_format(t *testing.T) {
107107
}
108108
cmd.SetArgs(args)
109109

110-
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`
110+
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`
111111

112112
err := cmd.Execute()
113113
assert.EqualError(t, err, expectedErr)
@@ -135,6 +135,98 @@ func Test_VerifyCmd_unknown_verification_alg(t *testing.T) {
135135
assert.EqualError(t, err, expectedErr)
136136
}
137137

138+
func Test_VerifyCmd_missing_header_key(t *testing.T) {
139+
cmd := NewVerifyCmd()
140+
141+
files := []fileEntry{
142+
{"ear.jwt", testJWT},
143+
}
144+
makeFS(t, files)
145+
146+
args := []string{
147+
"ear.jwt",
148+
}
149+
cmd.SetArgs(args)
150+
151+
expectedErr := `failed to get JWK key from JWT header`
152+
153+
err := cmd.Execute()
154+
assert.EqualError(t, err, expectedErr)
155+
}
156+
157+
func Test_VerifyCmd_incorrect_jws(t *testing.T) {
158+
cmd := NewVerifyCmd()
159+
160+
files := []fileEntry{
161+
{"ear.jwt", testJWT_JWK[1:]},
162+
}
163+
makeFS(t, files)
164+
165+
args := []string{
166+
"ear.jwt",
167+
}
168+
cmd.SetArgs(args)
169+
170+
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`
171+
172+
err := cmd.Execute()
173+
assert.EqualError(t, err, expectedErr)
174+
}
175+
176+
func Test_VerifyCmd_header_key_and_expired_token(t *testing.T) {
177+
cmd := NewVerifyCmd()
178+
179+
files := []fileEntry{
180+
{"ear.jwt", testRealmJWT},
181+
}
182+
makeFS(t, files)
183+
184+
args := []string{
185+
"ear.jwt",
186+
}
187+
cmd.SetArgs(args)
188+
189+
expectedErr := `jwt.Validate: validation failed: "exp" not satisfied: token is expired`
190+
191+
err := cmd.Execute()
192+
assert.ErrorContains(t, err, expectedErr)
193+
}
194+
195+
func Test_VerifyCmd_header_key_ignore_alg(t *testing.T) {
196+
cmd := NewVerifyCmd()
197+
198+
files := []fileEntry{
199+
{"ear.jwt", testJWT_JWK},
200+
}
201+
makeFS(t, files)
202+
203+
args := []string{
204+
"--alg=XYZ",
205+
"ear.jwt",
206+
}
207+
cmd.SetArgs(args)
208+
209+
err := cmd.Execute()
210+
assert.NoError(t, err)
211+
}
212+
213+
func Test_VerifyCmd_header_key_ok(t *testing.T) {
214+
cmd := NewVerifyCmd()
215+
216+
files := []fileEntry{
217+
{"ear.jwt", testJWT_JWK},
218+
}
219+
makeFS(t, files)
220+
221+
args := []string{
222+
"ear.jwt",
223+
}
224+
cmd.SetArgs(args)
225+
226+
err := cmd.Execute()
227+
assert.NoError(t, err)
228+
}
229+
138230
func Test_VerifyCmd_ok(t *testing.T) {
139231
cmd := NewVerifyCmd()
140232

ear.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ import (
1818
// EatProfile is the EAT profile implemented by this package
1919
const EatProfile = "tag:github.com,2023:veraison/ear"
2020

21+
// Trustee profile name which is an alias for the Veraison one.
22+
// Both names will be replaced with a neutral one:
23+
// https://github.com/ietf-rats-wg/draft-ietf-rats-ear/pull/47
24+
const EatTrusteeProfile = "tag:github.com,2024:confidential-containers/Trustee"
25+
2126
// AttestationResult represents the result of one or more evidence Appraisals
2227
// by the verifier. It is serialized to JSON and signed by the verifier using
2328
// JWT.
@@ -140,7 +145,7 @@ func (o AttestationResult) validate() error {
140145

141146
if o.Profile == nil {
142147
missing = append(missing, "'eat_profile'")
143-
} else if *o.Profile != EatProfile {
148+
} else if *o.Profile != EatProfile && *o.Profile != EatTrusteeProfile {
144149
invalid = append(invalid, fmt.Sprintf("eat_profile (%s)", *o.Profile))
145150
}
146151

0 commit comments

Comments
 (0)