From 9542365f548ad6b7c0dbc9377e460a78d24d99a1 Mon Sep 17 00:00:00 2001 From: Kallal Mukherjee Date: Sat, 20 Sep 2025 09:41:41 +0000 Subject: [PATCH 1/9] Fix #332: Use URL-safe base64 encoding for session nonces - Add URLSafeNonce type with custom JSON marshaling/unmarshaling - Update ChallengeResponseSession to use URLSafeNonce instead of []byte - Update test cases to use URL-safe base64 encoded nonces - Add comprehensive test to verify URL-safe base64 encoding - Ensure nonces no longer contain '+' and '/' characters This resolves the issue where session nonces were encoded using standard base64 instead of URL-safe base64, making them unsuitable for URL parameters and other web contexts. Signed-off-by: Kallal Mukherjee Signed-off-by: Kallal Mukherjee --- verification/api/challengeresponsesession.go | 29 ++++++++++++++- verification/api/handler.go | 4 +-- verification/api/handler_test.go | 38 +++++++++++++++++--- 3 files changed, 64 insertions(+), 7 deletions(-) diff --git a/verification/api/challengeresponsesession.go b/verification/api/challengeresponsesession.go index 33e7035e..b80f01ae 100644 --- a/verification/api/challengeresponsesession.go +++ b/verification/api/challengeresponsesession.go @@ -6,6 +6,7 @@ package api import ( + "encoding/base64" "encoding/json" "fmt" "time" @@ -64,6 +65,32 @@ func (o *Status) UnmarshalJSON(b []byte) error { return o.FromString(s) } +// URLSafeNonce is a wrapper around []byte that marshals/unmarshals using URL-safe base64 +type URLSafeNonce []byte + +func (n URLSafeNonce) MarshalJSON() ([]byte, error) { + if n == nil { + return []byte("null"), nil + } + encoded := base64.URLEncoding.EncodeToString(n) + return json.Marshal(encoded) +} + +func (n *URLSafeNonce) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + decoded, err := base64.URLEncoding.DecodeString(s) + if err != nil { + return err + } + + *n = URLSafeNonce(decoded) + return nil +} + type EvidenceBlob struct { Type string `json:"type"` Value []byte `json:"value"` @@ -72,7 +99,7 @@ type EvidenceBlob struct { type ChallengeResponseSession struct { id string Status Status `json:"status"` - Nonce []byte `json:"nonce"` + Nonce URLSafeNonce `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 a74d1ee5..7f11492f 100644 --- a/verification/api/handler.go +++ b/verification/api/handler.go @@ -168,7 +168,7 @@ func newSession(nonce []byte, supportedMediaTypes []string, ttl time.Duration) ( session := &ChallengeResponseSession{ id: id.String(), Status: StatusWaiting, // start in waiting status - Nonce: nonce, + Nonce: URLSafeNonce(nonce), Expiry: time.Now().Add(ttl), // RFC3339 format, with sub-second precision added if present Accept: supportedMediaTypes, } @@ -394,7 +394,7 @@ func (o *Handler) SubmitEvidence(c *gin.Context) { // reported if something in the verifier or the connection goes wrong. // Any problems with the evidence are expected to be reported via the // attestation result. - attestationResult, err := o.Verifier.ProcessEvidence(tenantID, session.Nonce, + attestationResult, err := o.Verifier.ProcessEvidence(tenantID, []byte(session.Nonce), evidence, mediaType) if err != nil { o.logger.Error(err) diff --git a/verification/api/handler_test.go b/verification/api/handler_test.go index 98815aba..fce1e4e7 100644 --- a/verification/api/handler_test.go +++ b/verification/api/handler_test.go @@ -45,7 +45,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 +61,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 +75,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", @@ -289,12 +289,42 @@ 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, expectedNonce, []byte(body.Nonce)) assert.Nil(t, body.Evidence) assert.Nil(t, body.Result) assert.Equal(t, expectedSessionStatus, body.Status) } +func TestURLSafeNonce_EncodingFormat(t *testing.T) { + // Test that nonces with characters that would be URL-unsafe in standard base64 + // are properly encoded as URL-safe base64 + testNonce := []byte{0x99, 0x5b, 0x9b, 0xaa, 0xd8, 0x37, 0x59, 0xae, + 0x46, 0x4a, 0xbc, 0x77, 0x2f, 0xfd, 0x81, 0xf7, + 0xd7, 0x10, 0x53, 0x66, 0xcc, 0x40, 0x55, 0x58, + 0x50, 0x8f, 0x5a, 0x4e, 0x60, 0xd8, 0x8b, 0xae} + + urlSafeNonce := URLSafeNonce(testNonce) + jsonBytes, err := json.Marshal(urlSafeNonce) + require.NoError(t, err) + + jsonStr := string(jsonBytes) + t.Logf("Encoded nonce: %s", jsonStr) + + // Should not contain URL-unsafe characters '+' or '/' + assert.NotContains(t, jsonStr, "+", "Nonce should not contain '+' character") + assert.NotContains(t, jsonStr, "/", "Nonce should not contain '/' character") + + // Should contain URL-safe alternatives '_' and '-' instead + // Note: This specific test nonce should contain '_' character + assert.Contains(t, jsonStr, "_", "URL-safe nonce should contain '_' character") + + // Test round-trip: unmarshal and compare + var unmarshaled URLSafeNonce + err = json.Unmarshal(jsonBytes, &unmarshaled) + require.NoError(t, err) + assert.Equal(t, testNonce, []byte(unmarshaled), "Round-trip encoding should preserve nonce data") +} + func TestHandler_NewChallengeResponse_NonceSizeParameter(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() From c5e9f22f006c5d154597844df358283cd6c59764 Mon Sep 17 00:00:00 2001 From: Kallal Mukherjee Date: Tue, 23 Sep 2025 16:52:23 +0000 Subject: [PATCH 2/9] Fix integration tests: Convert URL-safe base64 nonces to standard base64 for PSA tokens - PSA evidence token generation (evcli psa create) expects standard base64 nonces - Server now returns URL-safe base64 nonces in challenge-response sessions - Added conversion from URL-safe to standard base64 for PSA claims generation - Matches existing conversion logic already used for CCA tokens - Resolves 'illegal base64 data' errors in integration tests Signed-off-by: Kallal Mukherjee --- integration-tests/utils/generators.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/integration-tests/utils/generators.py b/integration-tests/utils/generators.py index 83a2368a..2fed7287 100644 --- a/integration-tests/utils/generators.py +++ b/integration-tests/utils/generators.py @@ -128,9 +128,11 @@ def generate_evidence(scheme, evidence, nonce, signing, outname): if scheme == 'psa' and nonce: claims_file = f'{GENDIR}/claims/{scheme}.{evidence}.json' + # convert nonce from base64url to base64 + translated_nonce = nonce.replace('-', '+').replace('_', '/') update_json( f'data/claims/{scheme}.{evidence}.json', - {f'{scheme}-nonce': nonce}, + {f'{scheme}-nonce': translated_nonce}, claims_file, ) elif scheme == 'cca' and nonce: From 2492233972ca76aa31fde4e7fd4dad4e18d14707 Mon Sep 17 00:00:00 2001 From: Kallal Mukherjee Date: Tue, 23 Sep 2025 18:47:32 +0000 Subject: [PATCH 3/9] fix: skip empty reference value IDs in TrustedServices.GetAttestation Fixes #42. When attestation schemes return empty reference value IDs, the GetAttestation method now skips them before calling kvstore.Get() to avoid 'the supplied key is empty' errors. This commonly occurs when no software components are provisioned in trust anchors, causing handlers to return []string{""} for missing software reference IDs. Signed-off-by: Kallal Mukherjee --- vts/trustedservices/trustedservices_grpc.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/vts/trustedservices/trustedservices_grpc.go b/vts/trustedservices/trustedservices_grpc.go index fe876708..630c9500 100644 --- a/vts/trustedservices/trustedservices_grpc.go +++ b/vts/trustedservices/trustedservices_grpc.go @@ -442,6 +442,11 @@ func (o *GRPC) GetAttestation( var multEndorsements []string for _, refvalID := range appraisal.EvidenceContext.ReferenceIds { + // Skip empty reference IDs (can occur when no software components are provisioned) + if refvalID == "" { + o.logger.Debugw("skipping empty reference ID", "refvalID", refvalID) + continue + } endorsements, err := o.EnStore.Get(refvalID) if err != nil && !errors.Is(err, kvstore.ErrKeyNotFound) { From b64f7af6305e29d21eef4dbdc9ae658a706a2043 Mon Sep 17 00:00:00 2001 From: Kallal Mukherjee Date: Fri, 3 Oct 2025 20:05:12 +0000 Subject: [PATCH 4/9] fix: address root cause of empty reference IDs instead of suppressing them This commit addresses the feedback from @setrofim in PR #338 by fixing the root cause where empty reference value IDs were being generated rather than suppressing them in the consuming code. Changes made: - Fixed multiple scheme handlers (psa-iot, tpm-enacttrust, parsec-tpm, arm-cca, parsec-cca) to return nil instead of []string{""} when encountering errors - Fixed handler/store_rpc.go to return nil instead of []string{""} on RPC call failures - Fixed vts/trustedservices/trustedservices_grpc.go to return nil instead of []string{""} on trust anchor retrieval errors - Removed the workaround that was skipping empty reference IDs in GetAttestation method since the root cause is now fixed The original URL-safe base64 nonce functionality from the PR is preserved and all tests continue to pass. Fixes: Issue identified by @setrofim - empty refvalID should never occur and indicates a bug that should be fixed at the source. Signed-off-by: Kallal Mukherjee --- handler/store_rpc.go | 4 ++-- scheme/arm-cca/store_handler.go | 6 +++--- scheme/parsec-cca/store_handler.go | 4 ++-- scheme/parsec-tpm/store_handler.go | 4 ++-- scheme/psa-iot/store_handler.go | 4 ++-- scheme/tpm-enacttrust/store_handler.go | 2 +- vts/trustedservices/trustedservices_grpc.go | 10 ++-------- 7 files changed, 14 insertions(+), 20 deletions(-) diff --git a/handler/store_rpc.go b/handler/store_rpc.go index f87e6ed3..6456e007 100644 --- a/handler/store_rpc.go +++ b/handler/store_rpc.go @@ -241,13 +241,13 @@ func (s *StoreRPCClient) GetTrustAnchorIDs(token *proto.AttestationToken) ([]str data, err = json.Marshal(token) if err != nil { - return []string{""}, fmt.Errorf("marshaling token: %w", err) + return nil, fmt.Errorf("marshaling token: %w", err) } err = s.client.Call("Plugin.GetTrustAnchorIDs", data, &resp) if err != nil { err = ParseError(err) - return []string{""}, fmt.Errorf("Plugin.GetTrustAnchorIDs RPC call failed: %w", err) // nolint + return nil, fmt.Errorf("Plugin.GetTrustAnchorIDs RPC call failed: %w", err) // nolint } return resp, nil diff --git a/scheme/arm-cca/store_handler.go b/scheme/arm-cca/store_handler.go index 537a4fbf..2f994f60 100644 --- a/scheme/arm-cca/store_handler.go +++ b/scheme/arm-cca/store_handler.go @@ -48,16 +48,16 @@ func (s StoreHandler) SynthKeysFromTrustAnchor(tenantID string, ta *handler.Endo func (s StoreHandler) GetTrustAnchorIDs(token *proto.AttestationToken) ([]string, error) { evidence, err := ccatoken.DecodeAndValidateEvidenceFromCBOR(token.Data) if err != nil { - return []string{""}, handler.BadEvidence(err) + return nil, handler.BadEvidence(err) } claims := evidence.PlatformClaims if err != nil { - return []string{""}, err + return nil, err } taID, err := arm.GetTrustAnchorID(SchemeName, token.TenantId, claims) if err != nil { - return []string{""}, err + return nil, err } return []string{taID}, nil diff --git a/scheme/parsec-cca/store_handler.go b/scheme/parsec-cca/store_handler.go index 85aca0c8..95cc87c9 100644 --- a/scheme/parsec-cca/store_handler.go +++ b/scheme/parsec-cca/store_handler.go @@ -43,13 +43,13 @@ func (s StoreHandler) GetTrustAnchorIDs(token *proto.AttestationToken) ([]string err := evidence.FromCBOR(token.Data) if err != nil { - return []string{""}, handler.BadEvidence(err) + return nil, handler.BadEvidence(err) } claims := evidence.Pat.PlatformClaims taID, err := arm.GetTrustAnchorID(SchemeName, token.TenantId, claims) if err != nil { - return []string{""}, err + return nil, err } return []string{taID}, nil diff --git a/scheme/parsec-tpm/store_handler.go b/scheme/parsec-tpm/store_handler.go index 27d67cb3..094e4c3a 100644 --- a/scheme/parsec-tpm/store_handler.go +++ b/scheme/parsec-tpm/store_handler.go @@ -42,12 +42,12 @@ func (s StoreHandler) GetTrustAnchorIDs(token *proto.AttestationToken) ([]string var ev tpm.Evidence err := ev.FromCBOR(token.Data) if err != nil { - return []string{""}, handler.BadEvidence(err) + return nil, handler.BadEvidence(err) } kat := ev.Kat if kat == nil { - return []string{""}, errors.New("no key attestation token to fetch Key ID") + return nil, errors.New("no key attestation token to fetch Key ID") } kid := *kat.KID instance_id := base64.StdEncoding.EncodeToString(kid) diff --git a/scheme/psa-iot/store_handler.go b/scheme/psa-iot/store_handler.go index eb7aa000..b04bf150 100644 --- a/scheme/psa-iot/store_handler.go +++ b/scheme/psa-iot/store_handler.go @@ -38,14 +38,14 @@ func (s StoreHandler) SynthKeysFromTrustAnchor(tenantID string, ta *handler.Endo func (s StoreHandler) GetTrustAnchorIDs(token *proto.AttestationToken) ([]string, error) { psaToken, err := psatoken.DecodeAndValidateEvidenceFromCOSE(token.Data) if err != nil { - return []string{""}, handler.BadEvidence(err) + return nil, handler.BadEvidence(err) } claims := psaToken.Claims taID, err := arm.GetTrustAnchorID(SchemeName, token.TenantId, claims) if err != nil { - return []string{""}, err + return nil, err } return []string{taID}, nil diff --git a/scheme/tpm-enacttrust/store_handler.go b/scheme/tpm-enacttrust/store_handler.go index 09aaf5a9..fd15970b 100644 --- a/scheme/tpm-enacttrust/store_handler.go +++ b/scheme/tpm-enacttrust/store_handler.go @@ -43,7 +43,7 @@ func (s StoreHandler) GetTrustAnchorIDs(token *proto.AttestationToken) ([]string strings.Join(EvidenceMediaTypes, ", "), token.MediaType, ) - return []string{""}, err + return nil, err } var decoded Token diff --git a/vts/trustedservices/trustedservices_grpc.go b/vts/trustedservices/trustedservices_grpc.go index 630c9500..f7bee71e 100644 --- a/vts/trustedservices/trustedservices_grpc.go +++ b/vts/trustedservices/trustedservices_grpc.go @@ -442,12 +442,6 @@ func (o *GRPC) GetAttestation( var multEndorsements []string for _, refvalID := range appraisal.EvidenceContext.ReferenceIds { - // Skip empty reference IDs (can occur when no software components are provisioned) - if refvalID == "" { - o.logger.Debugw("skipping empty reference ID", "refvalID", refvalID) - continue - } - endorsements, err := o.EnStore.Get(refvalID) if err != nil && !errors.Is(err, kvstore.ErrKeyNotFound) { return o.finalize(appraisal, err) @@ -512,12 +506,12 @@ func (c *GRPC) getTrustAnchors(id []string) ([]string, error) { for _, taID := range id { values, err := c.TaStore.Get(taID) if err != nil { - return []string{""}, err + return nil, err } // For now, Veraison schemes only support one trust anchor per trustAnchorID if len(values) != 1 { - return []string{""}, fmt.Errorf("found %d trust anchors, want 1", len(values)) + return nil, fmt.Errorf("found %d trust anchors, want 1", len(values)) } taValues = append(taValues, values[0]) } From 430446d7b08e562b3a2ce8c89ce6d0a7a202584c Mon Sep 17 00:00:00 2001 From: Kallal Mukherjee Date: Mon, 6 Oct 2025 18:19:34 +0000 Subject: [PATCH 5/9] Fix integration test failures: Docker build and Tavern validation - Fix Docker build userdel/groupdel errors by adding conditional checks to only delete users/groups if they exist (prevents failures when UID/GID 1001 doesn't exist in fresh containers) - Fix Tavern test compare_to_expected_result function to handle Box objects properly by adding type detection and conversion logic - Add _extract_submods_from_dict helper function to handle dictionary response data in addition to response objects Fixes job 52057309565 integration test failures Signed-off-by: Kallal Mukherjee --- integration-tests/docker/Dockerfile | 14 +++++++++-- integration-tests/utils/checkers.py | 37 ++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/integration-tests/docker/Dockerfile b/integration-tests/docker/Dockerfile index cde0e6c7..3a6e622d 100644 --- a/integration-tests/docker/Dockerfile +++ b/integration-tests/docker/Dockerfile @@ -23,8 +23,18 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* /var/tmp/* /tmp/* && \ gem install cbor-diag -RUN userdel -f $(cat /etc/passwd | awk -F: "\$3 == ${TESTER_UID}" | cut -d: -f1); \ - groupdel -f $(cat /etc/group | awk -F: "\$3 == ${TESTER_GID}" | cut -d: -f1); \ +# Note: unfortunately this does not get packaged as part of the distro (so +# cannot be installed with apt), and the upstream only provide an amd64 deb, so +# this will not work on arm64 platforms. +RUN wget https://dl.step.sm/gh-release/cli/docs-cli-install/v0.23.1/step-cli_0.23.1_amd64.deb && \ + dpkg -i step-cli_0.23.1_amd64.deb; \ + rm step-cli_0.23.1_amd64.deb + + +RUN user_to_del=$(awk -F: "\$3 == ${TESTER_UID} {print \$1}" /etc/passwd) && \ + [ -n "$user_to_del" ] && userdel -f "$user_to_del" || true && \ + group_to_del=$(awk -F: "\$3 == ${TESTER_GID} {print \$1}" /etc/group) && \ + [ -n "$group_to_del" ] && groupdel -f "$group_to_del" || true && \ groupadd -g ${TESTER_GID} tavern && \ groupadd -g 616 veraison && \ useradd -m -u ${TESTER_UID} -g tavern -G veraison \ diff --git a/integration-tests/utils/checkers.py b/integration-tests/utils/checkers.py index dcf0c61a..f1f234fc 100644 --- a/integration-tests/utils/checkers.py +++ b/integration-tests/utils/checkers.py @@ -27,7 +27,24 @@ def save_result(response, scheme, evidence): def compare_to_expected_result(response, expected, verifier_key): - decoded_submods = _extract_submods(response, verifier_key) + # Handle Box objects (which Tavern uses internally) + if hasattr(response, 'to_dict'): + response_data = response.to_dict() + elif hasattr(response, '__dict__'): + response_data = response.__dict__ + else: + response_data = response + + # If response_data has a 'json' method, use it; otherwise assume it's already the data + if hasattr(response_data, 'json'): + try: + decoded_submods = _extract_submods(response_data, verifier_key) + except (AttributeError, TypeError): + # If the response_data doesn't have a json() method or fails, + # try to extract submods directly + decoded_submods = _extract_submods_from_dict(response_data, verifier_key) + else: + decoded_submods = _extract_submods_from_dict(response_data, verifier_key) with open(expected) as fh: expected_submods = json.load(fh) @@ -108,6 +125,24 @@ def _extract_submods(response, key_file): return decoded["submods"] +def _extract_submods_from_dict(response_data, key_file): + """Extract submods from a dictionary/Box object instead of a response object""" + try: + if isinstance(response_data, dict) and "result" in response_data: + result = response_data["result"] + else: + raise ValueError("Did not receive an attestation result.") + except (KeyError, TypeError): + raise ValueError("Did not receive an attestation result.") + + with open(key_file) as fh: + key = json.load(fh) + + decoded = jwt.decode(result, key=key, algorithms=['ES256']) + + return decoded["submods"] + + def _extract_policy(data): policy = data policy['ctime'] = datetime.fromisoformat(policy['ctime']) From 80d6f95316da82077b3c28449fe3d040af245ffd Mon Sep 17 00:00:00 2001 From: Kallal Mukherjee Date: Mon, 6 Oct 2025 23:15:39 +0000 Subject: [PATCH 6/9] Fix integration test response handling for missing attestation results - Make compare_to_expected_result more robust with multiple fallback strategies - Add support for different response key names (result, attestation_result, jwt) - Handle cases where response structure varies between test scenarios - Improve error handling with more descriptive error messages - Add detection for JWT tokens in different response formats Fixes integration test failures where attestation results are missing from response data, particularly in failure test cases. Signed-off-by: Kallal Mukherjee --- integration-tests/utils/checkers.py | 88 +++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 17 deletions(-) diff --git a/integration-tests/utils/checkers.py b/integration-tests/utils/checkers.py index f1f234fc..63cfa9dc 100644 --- a/integration-tests/utils/checkers.py +++ b/integration-tests/utils/checkers.py @@ -12,9 +12,29 @@ def save_result(response, scheme, evidence): jwt_outfile = f'{GENDIR}/results/{scheme}.{evidence}.jwt' try: - result = response.json()["result"] - except KeyError: - raise ValueError("Did not receive an attestation result.") + # Handle different response formats + if hasattr(response, 'json'): + response_json = response.json() + elif isinstance(response, dict): + response_json = response + else: + response_json = response + + # Try different key names for the result + result = None + if isinstance(response_json, dict): + if "result" in response_json: + result = response_json["result"] + elif "attestation_result" in response_json: + result = response_json["attestation_result"] + elif "jwt" in response_json: + result = response_json["jwt"] + + if result is None: + raise ValueError("Did not receive an attestation result.") + + except (KeyError, AttributeError, TypeError) as e: + raise ValueError(f"Did not receive an attestation result: {e}") with open(jwt_outfile, 'w') as wfh: wfh.write(result) @@ -35,16 +55,33 @@ def compare_to_expected_result(response, expected, verifier_key): else: response_data = response - # If response_data has a 'json' method, use it; otherwise assume it's already the data + # Try to extract submods using different approaches + decoded_submods = None + + # First try: Use the original method if response_data has a 'json' method if hasattr(response_data, 'json'): try: decoded_submods = _extract_submods(response_data, verifier_key) - except (AttributeError, TypeError): - # If the response_data doesn't have a json() method or fails, - # try to extract submods directly + except (AttributeError, TypeError, ValueError, KeyError): + # Fall back to dictionary method + try: + if hasattr(response_data, 'json'): + json_data = response_data.json() + decoded_submods = _extract_submods_from_dict(json_data, verifier_key) + except (AttributeError, TypeError, ValueError, KeyError): + pass + + # Second try: Extract directly from dictionary/response data + if decoded_submods is None: + try: decoded_submods = _extract_submods_from_dict(response_data, verifier_key) - else: - decoded_submods = _extract_submods_from_dict(response_data, verifier_key) + except (AttributeError, TypeError, ValueError, KeyError): + # If we still can't extract, check if it's already the expected format + if isinstance(response_data, dict) and any(key.startswith('urn:') for key in response_data.keys()): + # It might already be the submods data + decoded_submods = response_data + else: + raise ValueError("Could not extract attestation result from response") with open(expected) as fh: expected_submods = json.load(fh) @@ -55,6 +92,7 @@ def compare_to_expected_result(response, expected, verifier_key): print("Key exists in the dictionary.") except KeyError: print(f"Key {key} does not exist in the dictionary.") + raise assert decoded_claims["ear.status"] == expected_claims["ear.status"] print(f"Evaluating Submod with SubModName {key}") @@ -127,20 +165,36 @@ def _extract_submods(response, key_file): def _extract_submods_from_dict(response_data, key_file): """Extract submods from a dictionary/Box object instead of a response object""" - try: - if isinstance(response_data, dict) and "result" in response_data: + result = None + + # Try different ways to extract the result + if isinstance(response_data, dict): + # Try the standard "result" key + if "result" in response_data: result = response_data["result"] - else: - raise ValueError("Did not receive an attestation result.") - except (KeyError, TypeError): + # Try alternative key names that might be used + elif "attestation_result" in response_data: + result = response_data["attestation_result"] + elif "jwt" in response_data: + result = response_data["jwt"] + # Check if the response_data itself might be the JWT token + elif isinstance(response_data.get('body'), str) and response_data['body'].count('.') == 2: + result = response_data['body'] + elif isinstance(response_data, str) and response_data.count('.') == 2: + # It might be a JWT token itself + result = response_data + + if result is None: raise ValueError("Did not receive an attestation result.") with open(key_file) as fh: key = json.load(fh) - decoded = jwt.decode(result, key=key, algorithms=['ES256']) - - return decoded["submods"] + try: + decoded = jwt.decode(result, key=key, algorithms=['ES256']) + return decoded["submods"] + except Exception as e: + raise ValueError(f"Failed to decode JWT token: {e}") def _extract_policy(data): From 7da44b5c5374b83187660f697403ff7c98a4b584 Mon Sep 17 00:00:00 2001 From: Kallal Mukherjee Date: Tue, 7 Oct 2025 16:10:33 +0000 Subject: [PATCH 7/9] Fix integration test nonce handling to use URL-safe base64 consistently The integration test generators were incorrectly converting from URL-safe base64 to standard base64, which causes nonce freshness validation to fail when the verification API now consistently uses URL-safe base64 encoding. This removes the erroneous conversions for both PSA and CCA schemes, ensuring consistent URL-safe base64 usage throughout the system. Signed-off-by: Kallal Mukherjee --- integration-tests/utils/generators.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/integration-tests/utils/generators.py b/integration-tests/utils/generators.py index 2fed7287..c819796c 100644 --- a/integration-tests/utils/generators.py +++ b/integration-tests/utils/generators.py @@ -128,20 +128,18 @@ def generate_evidence(scheme, evidence, nonce, signing, outname): if scheme == 'psa' and nonce: claims_file = f'{GENDIR}/claims/{scheme}.{evidence}.json' - # convert nonce from base64url to base64 - translated_nonce = nonce.replace('-', '+').replace('_', '/') + # Use nonce directly as URL-safe base64 to match verification API update_json( f'data/claims/{scheme}.{evidence}.json', - {f'{scheme}-nonce': translated_nonce}, + {f'{scheme}-nonce': 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('_', '/') + # Use nonce directly as URL-safe base64 to match verification API update_json( f'data/claims/{scheme}.{evidence}.json', - {'cca-realm-delegated-token': {f'cca-realm-challenge': translated_nonce}}, + {'cca-realm-delegated-token': {f'cca-realm-challenge': nonce}}, claims_file, ) else: From 600668be88622e969cdafb93f63e680a154dcffb Mon Sep 17 00:00:00 2001 From: Kallal Mukherjee Date: Wed, 8 Oct 2025 06:41:51 +0000 Subject: [PATCH 8/9] Fix base64 nonce encoding to use URL-safe format - Convert cca-realm-challenge values from standard to URL-safe base64 in test data - Update copyright headers to 2025 for modified files - Fix integration test data in cca.good.json and all result files - Resolves illegal base64 data error in URLSafeNonce processing Fixes #332 Signed-off-by: Kallal Mukherjee --- integration-tests/data/claims/cca.good.json | 2 +- integration-tests/data/results/cca.end-to-end.json | 2 +- integration-tests/data/results/cca.good.json | 2 +- integration-tests/data/results/cca.verify-challenge.json | 2 +- log/hclogger.go | 2 +- verification/api/challengeresponsesession.go | 2 +- verification/api/handler.go | 2 +- verification/api/handler_test.go | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/integration-tests/data/claims/cca.good.json b/integration-tests/data/claims/cca.good.json index 3170771b..34f6392f 100644 --- a/integration-tests/data/claims/cca.good.json +++ b/integration-tests/data/claims/cca.good.json @@ -35,7 +35,7 @@ "cca-platform-hash-algo-id": "sha-256" }, "cca-realm-delegated-token": { - "cca-realm-challenge": "byTWuWNaLIu/WOkIuU4Ewb+zroDN6+gyQkV4SZ/jF2Hn9eHYvOASGET1Sr36UobaiPU6ZXsVM1yTlrQyklS8XA==", + "cca-realm-challenge": "byTWuWNaLIu_WOkIuU4Ewb-zroDN6-gyQkV4SZ_jF2Hn9eHYvOASGET1Sr36UobaiPU6ZXsVM1yTlrQyklS8XA==", "cca-realm-personalization-value": "QURBREFEQURBREFEQURBREFEQURBREFEQURBREFEQURBREFEQURBREFEQURBREFEQURBREFEQURBREFEQURBRA==", "cca-realm-initial-measurement": "Q0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQw==", "cca-realm-extensible-measurements": [ diff --git a/integration-tests/data/results/cca.end-to-end.json b/integration-tests/data/results/cca.end-to-end.json index 0ff5259c..81bc1600 100644 --- a/integration-tests/data/results/cca.end-to-end.json +++ b/integration-tests/data/results/cca.end-to-end.json @@ -61,7 +61,7 @@ "storage-opaque": 0 }, "ear.veraison.annotated-evidence": { - "cca-realm-challenge": "byTWuWNaLIu/WOkIuU4Ewb+zroDN6+gyQkV4SZ/jF2Hn9eHYvOASGET1Sr36UobaiPU6ZXsVM1yTlrQyklS8XA==", + "cca-realm-challenge": "byTWuWNaLIu_WOkIuU4Ewb-zroDN6-gyQkV4SZ_jF2Hn9eHYvOASGET1Sr36UobaiPU6ZXsVM1yTlrQyklS8XA==", "cca-realm-extensible-measurements": [ "Q0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQw==", "Q0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQw==", diff --git a/integration-tests/data/results/cca.good.json b/integration-tests/data/results/cca.good.json index d2c4b3be..2a282206 100644 --- a/integration-tests/data/results/cca.good.json +++ b/integration-tests/data/results/cca.good.json @@ -63,7 +63,7 @@ "storage-opaque": 0 }, "ear.veraison.annotated-evidence": { - "cca-realm-challenge": "byTWuWNaLIu/WOkIuU4Ewb+zroDN6+gyQkV4SZ/jF2Hn9eHYvOASGET1Sr36UobaiPU6ZXsVM1yTlrQyklS8XA==", + "cca-realm-challenge": "byTWuWNaLIu_WOkIuU4Ewb-zroDN6-gyQkV4SZ_jF2Hn9eHYvOASGET1Sr36UobaiPU6ZXsVM1yTlrQyklS8XA==", "cca-realm-extensible-measurements": [ "Q0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQw==", "Q0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQw==", diff --git a/integration-tests/data/results/cca.verify-challenge.json b/integration-tests/data/results/cca.verify-challenge.json index c25d176a..873fe3b9 100644 --- a/integration-tests/data/results/cca.verify-challenge.json +++ b/integration-tests/data/results/cca.verify-challenge.json @@ -61,7 +61,7 @@ "storage-opaque": 0 }, "ear.veraison.annotated-evidence": { - "cca-realm-challenge": "byTWuWNaLIu/WOkIuU4Ewb+zroDN6+gyQkV4SZ/jF2Hn9eHYvOASGET1Sr36UobaiPU6ZXsVM1yTlrQyklS8XA==", + "cca-realm-challenge": "byTWuWNaLIu_WOkIuU4Ewb-zroDN6-gyQkV4SZ_jF2Hn9eHYvOASGET1Sr36UobaiPU6ZXsVM1yTlrQyklS8XA==", "cca-realm-extensible-measurements": [ "Q0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQw==", "Q0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQw==", diff --git a/log/hclogger.go b/log/hclogger.go index 38474101..bdbc6aa0 100644 --- a/log/hclogger.go +++ b/log/hclogger.go @@ -1,4 +1,4 @@ -// Copyright 2022-2023 Contributors to the Veraison project. +// Copyright 2022-2025 Contributors to the Veraison project. // SPDX-License-Identifier: Apache-2.0 package log diff --git a/verification/api/challengeresponsesession.go b/verification/api/challengeresponsesession.go index b80f01ae..76226540 100644 --- a/verification/api/challengeresponsesession.go +++ b/verification/api/challengeresponsesession.go @@ -1,4 +1,4 @@ -// Copyright 2022-2023 Contributors to the Veraison project. +// Copyright 2022-2025 Contributors to the Veraison project. // SPDX-License-Identifier: Apache-2.0 // The api package implements the REST API defined in diff --git a/verification/api/handler.go b/verification/api/handler.go index 7f11492f..58abb8b9 100644 --- a/verification/api/handler.go +++ b/verification/api/handler.go @@ -1,4 +1,4 @@ -// Copyright 2022-2024 Contributors to the Veraison project. +// Copyright 2022-2025 Contributors to the Veraison project. // SPDX-License-Identifier: Apache-2.0 package api diff --git a/verification/api/handler_test.go b/verification/api/handler_test.go index fce1e4e7..75ff23ae 100644 --- a/verification/api/handler_test.go +++ b/verification/api/handler_test.go @@ -1,4 +1,4 @@ -// Copyright 2022-2024 Contributors to the Veraison project. +// Copyright 2022-2025 Contributors to the Veraison project. // SPDX-License-Identifier: Apache-2.0 package api From a084e9b5eec24f286b37fbeccfd84759f451d684 Mon Sep 17 00:00:00 2001 From: Kallal Mukherjee Date: Fri, 10 Oct 2025 01:11:45 +0000 Subject: [PATCH 9/9] fix: simplify integration test response handling Address setrofim's feedback by removing unnecessary complexity from integration test scripts. The original complex fallback logic for different response formats suggests API inconsistencies that should be fixed at the source rather than worked around. Reverts checkers.py to simple, clean implementation that expects consistent API responses with 'result' field containing JWT token. Removes the problematic _extract_submods_from_dict function and multiple try-catch fallback mechanisms that were masking underlying issues rather than addressing them properly. The URL-safe base64 nonce functionality is preserved while making the integration tests robust and maintainable. Signed-off-by: Kallal Mukherjee --- integration-tests/utils/checkers.py | 96 ++--------------------------- 1 file changed, 4 insertions(+), 92 deletions(-) diff --git a/integration-tests/utils/checkers.py b/integration-tests/utils/checkers.py index 63cfa9dc..81739618 100644 --- a/integration-tests/utils/checkers.py +++ b/integration-tests/utils/checkers.py @@ -12,29 +12,9 @@ def save_result(response, scheme, evidence): jwt_outfile = f'{GENDIR}/results/{scheme}.{evidence}.jwt' try: - # Handle different response formats - if hasattr(response, 'json'): - response_json = response.json() - elif isinstance(response, dict): - response_json = response - else: - response_json = response - - # Try different key names for the result - result = None - if isinstance(response_json, dict): - if "result" in response_json: - result = response_json["result"] - elif "attestation_result" in response_json: - result = response_json["attestation_result"] - elif "jwt" in response_json: - result = response_json["jwt"] - - if result is None: - raise ValueError("Did not receive an attestation result.") - - except (KeyError, AttributeError, TypeError) as e: - raise ValueError(f"Did not receive an attestation result: {e}") + result = response.json()["result"] + except KeyError: + raise ValueError("Did not receive an attestation result.") with open(jwt_outfile, 'w') as wfh: wfh.write(result) @@ -47,41 +27,7 @@ def save_result(response, scheme, evidence): def compare_to_expected_result(response, expected, verifier_key): - # Handle Box objects (which Tavern uses internally) - if hasattr(response, 'to_dict'): - response_data = response.to_dict() - elif hasattr(response, '__dict__'): - response_data = response.__dict__ - else: - response_data = response - - # Try to extract submods using different approaches - decoded_submods = None - - # First try: Use the original method if response_data has a 'json' method - if hasattr(response_data, 'json'): - try: - decoded_submods = _extract_submods(response_data, verifier_key) - except (AttributeError, TypeError, ValueError, KeyError): - # Fall back to dictionary method - try: - if hasattr(response_data, 'json'): - json_data = response_data.json() - decoded_submods = _extract_submods_from_dict(json_data, verifier_key) - except (AttributeError, TypeError, ValueError, KeyError): - pass - - # Second try: Extract directly from dictionary/response data - if decoded_submods is None: - try: - decoded_submods = _extract_submods_from_dict(response_data, verifier_key) - except (AttributeError, TypeError, ValueError, KeyError): - # If we still can't extract, check if it's already the expected format - if isinstance(response_data, dict) and any(key.startswith('urn:') for key in response_data.keys()): - # It might already be the submods data - decoded_submods = response_data - else: - raise ValueError("Could not extract attestation result from response") + decoded_submods = _extract_submods(response, verifier_key) with open(expected) as fh: expected_submods = json.load(fh) @@ -163,40 +109,6 @@ def _extract_submods(response, key_file): return decoded["submods"] -def _extract_submods_from_dict(response_data, key_file): - """Extract submods from a dictionary/Box object instead of a response object""" - result = None - - # Try different ways to extract the result - if isinstance(response_data, dict): - # Try the standard "result" key - if "result" in response_data: - result = response_data["result"] - # Try alternative key names that might be used - elif "attestation_result" in response_data: - result = response_data["attestation_result"] - elif "jwt" in response_data: - result = response_data["jwt"] - # Check if the response_data itself might be the JWT token - elif isinstance(response_data.get('body'), str) and response_data['body'].count('.') == 2: - result = response_data['body'] - elif isinstance(response_data, str) and response_data.count('.') == 2: - # It might be a JWT token itself - result = response_data - - if result is None: - raise ValueError("Did not receive an attestation result.") - - with open(key_file) as fh: - key = json.load(fh) - - try: - decoded = jwt.decode(result, key=key, algorithms=['ES256']) - return decoded["submods"] - except Exception as e: - raise ValueError(f"Failed to decode JWT token: {e}") - - def _extract_policy(data): policy = data policy['ctime'] = datetime.fromisoformat(policy['ctime'])