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
21 changes: 16 additions & 5 deletions integration-tests/utils/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ 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.
translated = value.replace('-', '+').replace('_', '/')

# Session nonces use unpadded base64url, while evcli claim JSON requires
# padded standard base64.
return translated + '=' * (-len(translated) % 4)


def generate_expected_result_from_response(response, scheme, expected):
os.makedirs(f'{GENDIR}/expected', exist_ok=True)

Expand All @@ -99,15 +108,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:
Expand Down Expand Up @@ -138,15 +149,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)
Comment thread
cowbon marked this conversation as resolved.
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}},
Expand Down Expand Up @@ -307,4 +319,3 @@ def substitute_random_corim_id(path):
with tempfile.NamedTemporaryFile(delete=False, mode='w') as tf:
json.dump(data, tf)
return tf.name

12 changes: 12 additions & 0 deletions integration-tests/utils/test_generators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Copyright 2026 Contributors to the Veraison project.
# SPDX-License-Identifier: Apache-2.0

from generators import base64url_to_base64


def test_base64url_to_base64_replaces_characters_and_restores_padding():
assert base64url_to_base64('-_8') == '+/8='


def test_base64url_to_base64_does_not_add_unnecessary_padding():
assert base64url_to_base64('YWJj') == 'YWJj'
35 changes: 34 additions & 1 deletion verification/api/challengeresponsesession.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package api

import (
"encoding/base64"
"encoding/json"
"fmt"
"time"
Expand Down Expand Up @@ -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"`
Expand Down
20 changes: 7 additions & 13 deletions verification/api/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ package api

import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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,
}
Expand Down Expand Up @@ -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

Expand All @@ -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,
Expand Down
28 changes: 21 additions & 7 deletions verification/api/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package api

import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -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\"",
Expand All @@ -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\"",
Expand All @@ -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\"",
Expand Down Expand Up @@ -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{}

Expand Down Expand Up @@ -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()

Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Loading