Skip to content
Open
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
19 changes: 14 additions & 5 deletions integration-tests/utils/generators.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -91,6 +92,12 @@ 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.
decoded = base64.urlsafe_b64decode(value)
return base64.b64encode(decoded).decode('ascii')


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

Expand All @@ -99,15 +106,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 +147,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 @@ -306,4 +316,3 @@ def substitute_random_corim_id(path):
with tempfile.NamedTemporaryFile(delete=False, mode='w') as tf:
json.dump(data, tf)
return tf.name

58 changes: 58 additions & 0 deletions 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 @@ -79,6 +80,63 @@ type ChallengeResponseSession struct {
Result *string `json:"result,omitempty"`
}

type challengeResponseSessionJSON struct {
Status Status `json:"status"`
Nonce string `json:"nonce"`
Expiry time.Time `json:"expiry"`
Accept []string `json:"accept"`
Evidence *EvidenceBlob `json:"evidence,omitempty"`
Result *string `json:"result,omitempty"`
}

func (o ChallengeResponseSession) MarshalJSON() ([]byte, error) {
return json.Marshal(challengeResponseSessionJSON{
Status: o.Status,
Nonce: base64.URLEncoding.EncodeToString(o.Nonce),
Expiry: o.Expiry,
Accept: o.Accept,
Evidence: o.Evidence,
Result: o.Result,
})
}

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) UnmarshalJSON(b []byte) error {
var session challengeResponseSessionJSON

if err := json.Unmarshal(b, &session); err != nil {
return err
}

nonce, err := decodeSessionNonce(session.Nonce)
if err != nil {
return fmt.Errorf("nonce must be valid base64url: %w", err)
}

o.Status = session.Status
o.Nonce = nonce
o.Expiry = session.Expiry
o.Accept = session.Accept
o.Evidence = session.Evidence
o.Result = session.Result

return nil
}

func (o *ChallengeResponseSession) SetEvidence(mt string, evidence []byte) {
o.Evidence = &EvidenceBlob{Type: mt, Value: evidence}
}
Expand Down
26 changes: 20 additions & 6 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.URLEncoding.EncodeToString(expectedNonce)

qParams := url.Values{}
// b64("nonce-value") => "bm9uY2UtdmFsdWU="
qParams.Add("nonce", "bm9uY2UtdmFsdWU=")
qParams.Add("nonce", expectedNonceURLSafe)

w := httptest.NewRecorder()

Expand All @@ -289,6 +301,7 @@ 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, expectedNonceURLSafe, responseNonce(t, w.Body.Bytes()))
assert.Equal(t, expectedNonce, body.Nonce)
assert.Nil(t, body.Evidence)
assert.Nil(t, body.Result)
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_-]+={0,2}$`, responseNonce(t, w.Body.Bytes()))
assert.Len(t, body.Nonce, expectedNonceSize)
assert.Nil(t, body.Evidence)
assert.Nil(t, body.Result)
Expand Down
Loading