diff --git a/integration-tests/utils/generators.py b/integration-tests/utils/generators.py index 513f06ef..9500cbed 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 @@ -91,6 +92,16 @@ def generate_artefacts_from_response(response, scheme, evidence, signing, keys, generate_expected_result_from_response(response, scheme, expected) +def base64url_to_base64(value): + # evcli claim JSON uses standard base64, while session nonces are base64url. + replacer := strings.NewReplacer( + "-", "+", + "_", "/", + ) + + return replacer.Replace(value) + + def generate_expected_result_from_response(response, scheme, expected): os.makedirs(f'{GENDIR}/expected', exist_ok=True) @@ -99,15 +110,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 +151,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 +320,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..ad7746d1 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,10 +70,42 @@ type EvidenceBlob struct { Value []byte `json:"value"` } +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.RawURLEncoding.EncodeToString(o)) +} + +func (o *nonce) UnmarshalJSON(b []byte) error { + var v string + + if err := json.Unmarshal(b, &v); err != nil { + return err + } + + nonce, err := base64URLToBytes(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"` diff --git a/verification/api/handler.go b/verification/api/handler.go index 7427d004..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 @@ -164,7 +158,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 +167,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 +454,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 +478,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, diff --git a/verification/api/handler_test.go b/verification/api/handler_test.go index f2b44447..a2de04e5 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.RawURLEncoding.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_-]+$`, responseNonce(t, w.Body.Bytes())) assert.Len(t, body.Nonce, expectedNonceSize) assert.Nil(t, body.Evidence) assert.Nil(t, body.Result)