From f28ec55b5a281f4aaae4e4f13164a26035175e1f Mon Sep 17 00:00:00 2001 From: Ian Chin Wang Date: Mon, 29 Jun 2026 11:33:25 -0400 Subject: [PATCH 1/6] Return URL-safe nonce in challenge sessions Signed-off-by: Ian Chin Wang --- integration-tests/utils/generators.py | 16 +++++--- verification/api/challengeresponsesession.go | 41 +++++++++++++++++++- verification/api/handler_test.go | 28 +++++++++---- 3 files changed, 72 insertions(+), 13 deletions(-) diff --git a/integration-tests/utils/generators.py b/integration-tests/utils/generators.py index 513f06ef..6ec930e6 100644 --- a/integration-tests/utils/generators.py +++ b/integration-tests/utils/generators.py @@ -91,6 +91,10 @@ def generate_artefacts_from_response(response, scheme, evidence, signing, keys, generate_expected_result_from_response(response, scheme, expected) +def base64url_to_base64(value): + return value.replace('-', '+').replace('_', '/') + + def generate_expected_result_from_response(response, scheme, expected): os.makedirs(f'{GENDIR}/expected', exist_ok=True) @@ -99,15 +103,17 @@ def generate_expected_result_from_response(response, scheme, expected): nonce = response.json()["nonce"] if scheme == 'psa' and nonce: + translated_nonce = base64url_to_base64(nonce) update_json( infile, - {"PSA_IOT": {'ear.veraison.annotated-evidence': {f'psa-nonce': nonce}}}, + {"PSA_IOT": {'ear.veraison.annotated-evidence': {f'psa-nonce': translated_nonce}}}, outfile, ) elif scheme == 'cca' and nonce: + translated_nonce = base64url_to_base64(nonce) update_json( infile, - {"CCA_REALM": {'ear.veraison.annotated-evidence': {f'cca-realm-challenge': nonce}}}, + {"CCA_REALM": {'ear.veraison.annotated-evidence': {f'cca-realm-challenge': translated_nonce}}}, outfile, ) else: @@ -138,15 +144,16 @@ def generate_evidence(scheme, evidence, nonce, signing, outname): if scheme == 'psa' and nonce: claims_file = f'{GENDIR}/claims/{scheme}.{evidence}.json' + translated_nonce = base64url_to_base64(nonce) update_json( f'data/claims/{scheme}.{evidence}.json', - {f'{scheme}-nonce': nonce}, + {f'{scheme}-nonce': translated_nonce}, claims_file, ) elif scheme == 'cca' and nonce: claims_file = f'{GENDIR}/claims/{scheme}.{evidence}.json' # convert nonce from base64url to base64 - translated_nonce = nonce.replace('-', '+').replace('_', '/') + translated_nonce = base64url_to_base64(nonce) update_json( f'data/claims/{scheme}.{evidence}.json', {'cca-realm-delegated-token': {f'cca-realm-challenge': translated_nonce}}, @@ -306,4 +313,3 @@ def substitute_random_corim_id(path): with tempfile.NamedTemporaryFile(delete=False, mode='w') as tf: json.dump(data, tf) return tf.name - diff --git a/verification/api/challengeresponsesession.go b/verification/api/challengeresponsesession.go index 33e7035e..1b814360 100644 --- a/verification/api/challengeresponsesession.go +++ b/verification/api/challengeresponsesession.go @@ -6,6 +6,7 @@ package api import ( + "encoding/base64" "encoding/json" "fmt" "time" @@ -69,16 +70,54 @@ type EvidenceBlob struct { Value []byte `json:"value"` } +type nonce []byte + +func (o nonce) MarshalJSON() ([]byte, error) { + return json.Marshal(base64.URLEncoding.EncodeToString(o)) +} + +func (o *nonce) UnmarshalJSON(b []byte) error { + var v string + + if err := json.Unmarshal(b, &v); err != nil { + return err + } + + nonce, err := decodeSessionNonce(v) + if err != nil { + return fmt.Errorf("nonce must be valid base64url: %w", err) + } + + *o = nonce + + return nil +} + type ChallengeResponseSession struct { id string Status Status `json:"status"` - Nonce []byte `json:"nonce"` + Nonce nonce `json:"nonce"` Expiry time.Time `json:"expiry"` Accept []string `json:"accept"` Evidence *EvidenceBlob `json:"evidence,omitempty"` Result *string `json:"result,omitempty"` } +func decodeSessionNonce(v string) ([]byte, error) { + nonce, err := base64.URLEncoding.DecodeString(v) + if err == nil { + return nonce, nil + } + + // Keep reading sessions created before the nonce wire format switched + // from standard base64 to base64url. + if nonce, stdErr := base64.StdEncoding.DecodeString(v); stdErr == nil { + return nonce, nil + } + + return nil, err +} + func (o *ChallengeResponseSession) SetEvidence(mt string, evidence []byte) { o.Evidence = &EvidenceBlob{Type: mt, Value: evidence} } diff --git a/verification/api/handler_test.go b/verification/api/handler_test.go index f2b44447..2d0fd1c2 100644 --- a/verification/api/handler_test.go +++ b/verification/api/handler_test.go @@ -4,6 +4,7 @@ package api import ( "bytes" + "encoding/base64" "encoding/json" "errors" "fmt" @@ -45,7 +46,7 @@ var ( testJSONBody = `{ "k": "v" }` testSession = `{ "status": "waiting", - "nonce": "mVubqtg3Wa5GSrx3L/2B99cQU2bMQFVYUI9aTmDYi64=", + "nonce": "mVubqtg3Wa5GSrx3L_2B99cQU2bMQFVYUI9aTmDYi64=", "expiry": "2022-07-13T13:50:24.520525+01:00", "accept": [ "application/eat_cwt;profile=\"http://arm.com/psa/2.0.0\"", @@ -61,7 +62,7 @@ var ( }` testProcessingSession = `{ "status": "processing", - "nonce": "mVubqtg3Wa5GSrx3L/2B99cQU2bMQFVYUI9aTmDYi64=", + "nonce": "mVubqtg3Wa5GSrx3L_2B99cQU2bMQFVYUI9aTmDYi64=", "expiry": "2022-07-13T13:50:24.520525+01:00", "accept": [ "application/eat_cwt;profile=\"http://arm.com/psa/2.0.0\"", @@ -75,7 +76,7 @@ var ( }` testCompleteSession = `{ "status": "complete", - "nonce": "mVubqtg3Wa5GSrx3L/2B99cQU2bMQFVYUI9aTmDYi64=", + "nonce": "mVubqtg3Wa5GSrx3L_2B99cQU2bMQFVYUI9aTmDYi64=", "expiry": "2022-07-13T13:50:24.520525+01:00", "accept": [ "application/eat_cwt;profile=\"http://arm.com/psa/2.0.0\"", @@ -117,6 +118,17 @@ var ( } ) +func responseNonce(t *testing.T, response []byte) string { + t.Helper() + + var body struct { + Nonce string `json:"nonce"` + } + require.NoError(t, json.Unmarshal(response, &body)) + + return body.Nonce +} + func TestHandler_NewChallengeResponse_UnsupportedAccept(t *testing.T) { h := &Handler{} @@ -269,11 +281,11 @@ func TestHandler_NewChallengeResponse_NonceParameter(t *testing.T) { expectedType := ChallengeResponseSessionMediaType expectedLocationRE := sessionURIRegexp expectedSessionStatus := StatusWaiting - expectedNonce := []byte("nonce-value") + expectedNonce := testNonce + expectedNonceURLSafe := base64.URLEncoding.EncodeToString(expectedNonce) qParams := url.Values{} - // b64("nonce-value") => "bm9uY2UtdmFsdWU=" - qParams.Add("nonce", "bm9uY2UtdmFsdWU=") + qParams.Add("nonce", expectedNonceURLSafe) w := httptest.NewRecorder() @@ -289,7 +301,8 @@ func TestHandler_NewChallengeResponse_NonceParameter(t *testing.T) { assert.Equal(t, expectedCode, w.Code) assert.Equal(t, expectedType, w.Result().Header.Get("Content-Type")) assert.Regexp(t, expectedLocationRE, w.Result().Header.Get("Location")) - assert.Equal(t, expectedNonce, body.Nonce) + assert.Equal(t, expectedNonceURLSafe, responseNonce(t, w.Body.Bytes())) + assert.Equal(t, expectedNonce, []byte(body.Nonce)) assert.Nil(t, body.Evidence) assert.Nil(t, body.Result) assert.Equal(t, expectedSessionStatus, body.Status) @@ -334,6 +347,7 @@ func TestHandler_NewChallengeResponse_NonceSizeParameter(t *testing.T) { assert.Equal(t, expectedCode, w.Code) assert.Equal(t, expectedType, w.Result().Header.Get("Content-Type")) assert.Regexp(t, expectedLocationRE, w.Result().Header.Get("Location")) + assert.Regexp(t, `^[A-Za-z0-9_-]+={0,2}$`, responseNonce(t, w.Body.Bytes())) assert.Len(t, body.Nonce, expectedNonceSize) assert.Nil(t, body.Evidence) assert.Nil(t, body.Result) From ee344e3cc61457bdc54e76aeb45caa31e9d8139f Mon Sep 17 00:00:00 2001 From: Ian Chin Wang Date: Mon, 29 Jun 2026 13:38:37 -0400 Subject: [PATCH 2/6] Use decoder for nonce test conversion Signed-off-by: Ian Chin Wang --- integration-tests/utils/generators.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/integration-tests/utils/generators.py b/integration-tests/utils/generators.py index 6ec930e6..e96560be 100644 --- a/integration-tests/utils/generators.py +++ b/integration-tests/utils/generators.py @@ -1,6 +1,7 @@ # Copyright 2023-2026 Contributors to the Veraison project. # SPDX-License-Identifier: Apache-2.0 import ast +import base64 import json import os import shutil @@ -92,7 +93,9 @@ def generate_artefacts_from_response(response, scheme, evidence, signing, keys, def base64url_to_base64(value): - return value.replace('-', '+').replace('_', '/') + # evcli claim JSON uses standard base64, while session nonces are base64url. + decoded = base64.urlsafe_b64decode(value) + return base64.b64encode(decoded).decode('ascii') def generate_expected_result_from_response(response, scheme, expected): From 061f2f53f4278b1059d2716af127ed522cc96463 Mon Sep 17 00:00:00 2001 From: Ian Chin Wang Date: Wed, 1 Jul 2026 13:43:45 -0400 Subject: [PATCH 3/6] Remove legacy nonce decode fallback Signed-off-by: Ian Chin Wang --- verification/api/challengeresponsesession.go | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/verification/api/challengeresponsesession.go b/verification/api/challengeresponsesession.go index 1b814360..395270a7 100644 --- a/verification/api/challengeresponsesession.go +++ b/verification/api/challengeresponsesession.go @@ -83,7 +83,7 @@ func (o *nonce) UnmarshalJSON(b []byte) error { return err } - nonce, err := decodeSessionNonce(v) + nonce, err := base64.URLEncoding.DecodeString(v) if err != nil { return fmt.Errorf("nonce must be valid base64url: %w", err) } @@ -103,21 +103,6 @@ type ChallengeResponseSession struct { Result *string `json:"result,omitempty"` } -func decodeSessionNonce(v string) ([]byte, error) { - nonce, err := base64.URLEncoding.DecodeString(v) - if err == nil { - return nonce, nil - } - - // Keep reading sessions created before the nonce wire format switched - // from standard base64 to base64url. - if nonce, stdErr := base64.StdEncoding.DecodeString(v); stdErr == nil { - return nonce, nil - } - - return nil, err -} - func (o *ChallengeResponseSession) SetEvidence(mt string, evidence []byte) { o.Evidence = &EvidenceBlob{Type: mt, Value: evidence} } From 92bf0866469543bf9e703c181e8f6a651a07e2d0 Mon Sep 17 00:00:00 2001 From: Ian Chin Wang Date: Wed, 1 Jul 2026 13:59:06 -0400 Subject: [PATCH 4/6] Use nonce type for new session Signed-off-by: Ian Chin Wang --- verification/api/handler.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/verification/api/handler.go b/verification/api/handler.go index 7427d004..e4cc4ba4 100644 --- a/verification/api/handler.go +++ b/verification/api/handler.go @@ -164,7 +164,7 @@ func parseNonceRequest(nonceParam, nonceSizeParam string) ([]byte, error) { return nonce, nil } -func newSession(nonce []byte, supportedMediaTypes []string, ttl time.Duration) (uuid.UUID, []byte, error) { +func newSession(sessionNonce nonce, supportedMediaTypes []string, ttl time.Duration) (uuid.UUID, []byte, error) { id, err := mintSessionID() if err != nil { return uuid.UUID{}, nil, err @@ -173,7 +173,7 @@ func newSession(nonce []byte, supportedMediaTypes []string, ttl time.Duration) ( session := &ChallengeResponseSession{ id: id.String(), Status: StatusWaiting, // start in waiting status - Nonce: nonce, + Nonce: sessionNonce, Expiry: time.Now().Add(ttl), // RFC3339 format, with sub-second precision added if present Accept: supportedMediaTypes, } @@ -460,7 +460,7 @@ func (o *Handler) NewChallengeResponse(c *gin.Context) { } // parse query to devise the nonce - nonce, err := parseNonceRequest(c.Query("nonce"), c.Query("nonceSize")) + sessionNonce, err := parseNonceRequest(c.Query("nonce"), c.Query("nonceSize")) if err != nil { status := http.StatusBadRequest @@ -484,7 +484,7 @@ func (o *Handler) NewChallengeResponse(c *gin.Context) { return } - id, session, err := newSession(nonce, supportedMediaTypes, ConfigSessionTTL) + id, session, err := newSession(nonce(sessionNonce), supportedMediaTypes, ConfigSessionTTL) if err != nil { ReportProblem(c, http.StatusInternalServerError, From 99a889ccd28777f5eff627cecf71fa74adefdb02 Mon Sep 17 00:00:00 2001 From: Ian Chin Wang Date: Fri, 3 Jul 2026 10:08:12 -0400 Subject: [PATCH 5/6] Update integration-tests/utils/generators.py Signed-off-by: Ian Chin Wang Co-authored-by: setrofim --- integration-tests/utils/generators.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/integration-tests/utils/generators.py b/integration-tests/utils/generators.py index e96560be..9500cbed 100644 --- a/integration-tests/utils/generators.py +++ b/integration-tests/utils/generators.py @@ -94,8 +94,12 @@ def generate_artefacts_from_response(response, scheme, evidence, signing, keys, def base64url_to_base64(value): # evcli claim JSON uses standard base64, while session nonces are base64url. - decoded = base64.urlsafe_b64decode(value) - return base64.b64encode(decoded).decode('ascii') + replacer := strings.NewReplacer( + "-", "+", + "_", "/", + ) + + return replacer.Replace(value) def generate_expected_result_from_response(response, scheme, expected): From bb19d9f98ec010bdca9531d1f6f6e1dce3015d7f Mon Sep 17 00:00:00 2001 From: Ian Chin Wang Date: Tue, 14 Jul 2026 14:13:42 -0400 Subject: [PATCH 6/6] Use unpadded base64url nonces Signed-off-by: Ian Chin Wang --- verification/api/challengeresponsesession.go | 13 +++++++++++-- verification/api/handler.go | 12 +++--------- verification/api/handler_test.go | 10 +++++----- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/verification/api/challengeresponsesession.go b/verification/api/challengeresponsesession.go index 395270a7..ad7746d1 100644 --- a/verification/api/challengeresponsesession.go +++ b/verification/api/challengeresponsesession.go @@ -72,8 +72,17 @@ type EvidenceBlob struct { type nonce []byte +func base64URLToBytes(v string) ([]byte, error) { + nonce, err := base64.RawURLEncoding.DecodeString(v) + if err == nil { + return nonce, nil + } + + return base64.URLEncoding.DecodeString(v) +} + func (o nonce) MarshalJSON() ([]byte, error) { - return json.Marshal(base64.URLEncoding.EncodeToString(o)) + return json.Marshal(base64.RawURLEncoding.EncodeToString(o)) } func (o *nonce) UnmarshalJSON(b []byte) error { @@ -83,7 +92,7 @@ func (o *nonce) UnmarshalJSON(b []byte) error { return err } - nonce, err := base64.URLEncoding.DecodeString(v) + nonce, err := base64URLToBytes(v) if err != nil { return fmt.Errorf("nonce must be valid base64url: %w", err) } diff --git a/verification/api/handler.go b/verification/api/handler.go index e4cc4ba4..56d3c199 100644 --- a/verification/api/handler.go +++ b/verification/api/handler.go @@ -4,7 +4,6 @@ package api import ( "crypto/rand" - "encoding/base64" "encoding/json" "errors" "fmt" @@ -113,15 +112,10 @@ func aToU8(v string) (uint8, error) { return uint8(u8), nil } -// b64ToBytes attempts at converting the supplied b64-encoded string into a byte -// slice +// b64ToBytes attempts at converting the supplied base64url-encoded string into +// a byte slice. func b64ToBytes(v string) ([]byte, error) { - b, err := base64.URLEncoding.DecodeString(v) - if err != nil { - return nil, err - } - - return b, nil + return base64URLToBytes(v) } // parseNonceRequest tries to devise the nonce value to be used for the session diff --git a/verification/api/handler_test.go b/verification/api/handler_test.go index 2d0fd1c2..a2de04e5 100644 --- a/verification/api/handler_test.go +++ b/verification/api/handler_test.go @@ -46,7 +46,7 @@ var ( testJSONBody = `{ "k": "v" }` testSession = `{ "status": "waiting", - "nonce": "mVubqtg3Wa5GSrx3L_2B99cQU2bMQFVYUI9aTmDYi64=", + "nonce": "mVubqtg3Wa5GSrx3L_2B99cQU2bMQFVYUI9aTmDYi64", "expiry": "2022-07-13T13:50:24.520525+01:00", "accept": [ "application/eat_cwt;profile=\"http://arm.com/psa/2.0.0\"", @@ -62,7 +62,7 @@ var ( }` testProcessingSession = `{ "status": "processing", - "nonce": "mVubqtg3Wa5GSrx3L_2B99cQU2bMQFVYUI9aTmDYi64=", + "nonce": "mVubqtg3Wa5GSrx3L_2B99cQU2bMQFVYUI9aTmDYi64", "expiry": "2022-07-13T13:50:24.520525+01:00", "accept": [ "application/eat_cwt;profile=\"http://arm.com/psa/2.0.0\"", @@ -76,7 +76,7 @@ var ( }` testCompleteSession = `{ "status": "complete", - "nonce": "mVubqtg3Wa5GSrx3L_2B99cQU2bMQFVYUI9aTmDYi64=", + "nonce": "mVubqtg3Wa5GSrx3L_2B99cQU2bMQFVYUI9aTmDYi64", "expiry": "2022-07-13T13:50:24.520525+01:00", "accept": [ "application/eat_cwt;profile=\"http://arm.com/psa/2.0.0\"", @@ -282,7 +282,7 @@ func TestHandler_NewChallengeResponse_NonceParameter(t *testing.T) { expectedLocationRE := sessionURIRegexp expectedSessionStatus := StatusWaiting expectedNonce := testNonce - expectedNonceURLSafe := base64.URLEncoding.EncodeToString(expectedNonce) + expectedNonceURLSafe := base64.RawURLEncoding.EncodeToString(expectedNonce) qParams := url.Values{} qParams.Add("nonce", expectedNonceURLSafe) @@ -347,7 +347,7 @@ func TestHandler_NewChallengeResponse_NonceSizeParameter(t *testing.T) { assert.Equal(t, expectedCode, w.Code) assert.Equal(t, expectedType, w.Result().Header.Get("Content-Type")) assert.Regexp(t, expectedLocationRE, w.Result().Header.Get("Location")) - assert.Regexp(t, `^[A-Za-z0-9_-]+={0,2}$`, responseNonce(t, w.Body.Bytes())) + assert.Regexp(t, `^[A-Za-z0-9_-]+$`, responseNonce(t, w.Body.Bytes())) assert.Len(t, body.Nonce, expectedNonceSize) assert.Nil(t, body.Evidence) assert.Nil(t, body.Result)