diff --git a/verifiers-reference-implementation/README.md b/verifiers-reference-implementation/README.md index 7a3dd8e..daeb54a 100644 --- a/verifiers-reference-implementation/README.md +++ b/verifiers-reference-implementation/README.md @@ -108,6 +108,23 @@ Before running the server, you need to configure a few important variables in th ``` *** +## Testing & Mocking Notes + +To facilitate testing without a full production setup, the following mocks and defaults are in place in the Python server: + +### 1. Zero-Knowledge Proofs (ZKP) +* **Request Generation:** The server automatically mocks the ZK specifications response if `SPECS_URL` in `config.py` is left as the default placeholder. This allows you to test generating ZK requests from the UI without a running spec service. +* **Verification:** The `/zkverify` endpoint still attempts to contact the `ZK_VERIFIER_URL`. You will need to provide a valid URL in `config.py` for verification to succeed. + +### 2. Signed Requests +* The project includes a generated self-signed EC private key and certificate in `keys.py` for testing the `openid4vp-v1-signed` protocol. +* **Warning:** For production, you must replace these with a real certificate and store the private key securely in a Key Management Service (KMS). + +### 3. Hardcoded Nonce +* The `nonce` is currently hardcoded to `"test-nonce"` in `construct_openid4vp_request` to match testing environments. For production, this should be reverted to a random cryptographically secure string. + +*** + ### Running the Server You can run the application using either the Flask development server or a production-ready WSGI server like Gunicorn. @@ -153,7 +170,7 @@ The server exposes the following API endpoints: * **Request Body** (JSON): ```json { - "protocol": "openid4vp", + "protocol": "openid4vp-v1-unsigned", "doctype": "string", "requestZkp": boolean, "attributes": [ @@ -168,7 +185,7 @@ The server exposes the following API endpoints: * **Request Body** (JSON): ```json { - "protocol": "openid4vp", + "protocol": "openid4vp-v1-unsigned", "data": "...", "state": { ... }, "origin": "string" diff --git a/verifiers-reference-implementation/android/IdVerifierDemoApp/build.gradle b/verifiers-reference-implementation/android/IdVerifierDemoApp/build.gradle index b90f51a..5baa042 100644 --- a/verifiers-reference-implementation/android/IdVerifierDemoApp/build.gradle +++ b/verifiers-reference-implementation/android/IdVerifierDemoApp/build.gradle @@ -15,7 +15,7 @@ limitations under the License. */ plugins { id 'com.android.application' version '8.8.1' apply false - id 'com.android.library' version '8.7.3' apply false + id 'com.android.library' version '8.8.1' apply false id 'org.jetbrains.kotlin.android' version '2.1.0' apply false - id 'org.jetbrains.kotlin.plugin.compose' version '2.0.0' apply false + id 'org.jetbrains.kotlin.plugin.compose' version '2.1.0' apply false } \ No newline at end of file diff --git a/verifiers-reference-implementation/android/IdVerifierDemoApp/gradlew b/verifiers-reference-implementation/android/IdVerifierDemoApp/gradlew old mode 100644 new mode 100755 diff --git a/verifiers-reference-implementation/python-server/keys.py b/verifiers-reference-implementation/python-server/keys.py index 0ae018f..ccbc54a 100644 --- a/verifiers-reference-implementation/python-server/keys.py +++ b/verifiers-reference-implementation/python-server/keys.py @@ -14,12 +14,32 @@ limitations under the License. ''' -PRIVATE_KEY = """-----BEGIN PRIVATE KEY----- - ------END PRIVATE KEY-----""" +# --- Purpose of this file --- +# This file contains the private key and certificate used for SIGNING requests +# when using the 'openid4vp-v1-signed' protocol. +# They are NOT used for verifying the response. +# +# --- Production Security Warning --- +# DO NOT use these hardcoded self-signed keys in production! +# In production, you should: +# 1. Use a real certificate issued by a trusted authority. +# 2. Store the private key securely in a Key Management Service (KMS) or hardware security module (HSM). +# 3. Do not commit private keys to source control. + +PRIVATE_KEY = """-----BEGIN EC PARAMETERS----- +BggqhkjOPQMBBw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIAc1jY4u2abdGT73xOAFbos47jzbFgGqQBUXtQeOfxZroAoGCCqGSM49 +AwEHoUQDQgAENZnak7+/ZBCEFbIh5/x0swiZuEEEoVVeykJ/SeV3z3Wiph/f8oMh +HBUAt6kS+k9SOwGfc7fKrEWJLgAeMxI97A== +-----END EC PRIVATE KEY-----""" CERTIFICATE = """-----BEGIN CERTIFICATE----- - +MIIBGDCBvgIJAOvRMvbc+21VMAoGCCqGSM49BAMCMBQxEjAQBgNVBAMMCWxvY2Fs +aG9zdDAeFw0yNjA0MjgxNDA1MzZaFw0yNzA0MjgxNDA1MzZaMBQxEjAQBgNVBAMM +CWxvY2FsaG9zdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABDWZ2pO/v2QQhBWy +Ief8dLMImbhBBKFVXspCf0nld891oqYf3/KDIRwVALepEvpPUjsBn3O3yqxFiS4A +HjMSPewwCgYIKoZIzj0EAwIDSQAwRgIhAIQv1PzR9RBfPL8YyQztI7C3uCinjKK6 +LUTh/UVk5JETAiEAsg0rA+pMpm9HU4uZpR67lbVgHGbuo/rUKVpOKF7Dld4= -----END CERTIFICATE-----""" diff --git a/verifiers-reference-implementation/python-server/main.py b/verifiers-reference-implementation/python-server/main.py index eac8c1a..9ee8c63 100644 --- a/verifiers-reference-implementation/python-server/main.py +++ b/verifiers-reference-implementation/python-server/main.py @@ -148,15 +148,17 @@ def construct_openid4vp_request(doctypes: list[str], requested_fields: list[dict claims_list = [] for field_data in requested_fields: claim = { - "path": [field_data["namespace"], field_data["name"]], # Path to the claim within the mdoc - "intent_to_retain": False # set this to true if you are saving the value of the field + "path": [field_data["namespace"], field_data["name"]] } claims_list.append(claim) # Create a credential request for each doctype for i, doctype in enumerate(doctypes): # Generate a unique ID for each credential request for traceability # e.g., "mdl-request" or "idcard-request" - request_id = f"{doctype.split('.')[-1].lower()}-request" + if doctype == "eu.europa.ec.av.1": + request_id = "age_credential" + else: + request_id = f"{doctype.split('.')[-1].lower()}-request" meta = {"doctype_value": doctype} format_type = "mso_mdoc" @@ -165,7 +167,6 @@ def construct_openid4vp_request(doctypes: list[str], requested_fields: list[dict if error: return error # Propagate error meta["zk_system_type"] = zk_system_type - meta["verifier_message"] = "challenge" format_type = "mso_mdoc_zk" credential_request = { @@ -185,12 +186,7 @@ def construct_openid4vp_request(doctypes: list[str], requested_fields: list[dict # Define the credential query using DCQL (Digital Credential Query Language - conceptual) dcql_query = { - "credentials": credentials_list, - "credential_sets" : [ - { - "options": credential_set_options - } - ] + "credentials": credentials_list } @@ -218,10 +214,9 @@ def construct_openid4vp_request(doctypes: list[str], requested_fields: list[dict # Construct the main OpenID4VP request payload request_payload = { "response_type": "vp_token", # Requesting a Verifiable Presentation Token - "response_mode": "dc_api.jwt", # Response delivered via DeviceCheck API as JWT, - "nonce": nonce_base64, # Nonce (must match state) - note base64 without padding - "dcql_query": dcql_query, # The credential query - "client_metadata": client_metadata # How the client wants the response encrypted + "response_mode": "dc_api", # Response delivered via DeviceCheck API + "nonce": "test-nonce", # Hardcoded for testing to match example + "dcql_query": dcql_query # The credential query } if is_signed_request: # --- Request Signing (JAR / OpenID4VP) --- @@ -301,6 +296,27 @@ def fetch_and_process_specs(num_attributes): - A list of specs (list) if successful, otherwise None. - A dictionary containing error details (dict) if an error occurred, otherwise None. """ + if config.SPECS_URL.startswith(""): + print("Using mock ZK specs for testing.") + return [ + { + "system": "longfellow-libzk-v1", + "circuit_hash": "f88a39e561ec0be02bb3dfe38fb609ad154e98decbbe632887d850fc612fea6f", + "num_attributes": num_attributes, + "version": 5, + "block_enc_hash": 4096, + "block_enc_sig": 2945, + }, + { + "system": "longfellow-libzk-v1", + "circuit_hash": "137e5a75ce72735a37c8a72da1a8a0a5df8d13365c2ae3d2c2bd6a0e7197c7c6", + "num_attributes": num_attributes, + "version": 6, + "block_enc_hash": 4096, + "block_enc_sig": 2945, + } + ], None + try: # Make a GET request to the external specs endpoint # NOTE: 'requests' library needs to be imported for this to work. @@ -536,28 +552,30 @@ def process_openid4vp_response(encrypted_jwe_string: str, request_state: dict, o if origin.startswith("https://") or origin.startswith("http://"): # Web Origin if "sign_request_client_id" in request_state: client_id = request_state["sign_request_client_id"] + origin_info = client_id # Use client_id as origin_info for signed requests else: client_id = f"web-origin:{origin}" - origin_info = origin + origin_info = origin session_transcript_list = generate_openid4vp_session_transcript( client_id, nonce_base64_unpadded, origin_info, encryption_public_jwk_thumbprint ) else: # Assume Android Origin if "sign_request_client_id" in request_state: client_id = request_state["sign_request_client_id"] + origin_info = client_id # Use client_id as origin_info for signed requests else: client_id = f"android-origin:{config.APP_PACKAGE_NAME}" - # Calculate the base64 encoded SHA256 hash of the app signing cert - try: - app_signature_hash_bytes = bytes.fromhex(config.ANDROID_APP_SIGNATURE_HASH) - app_signature_hash_base64 = base64.b64encode(app_signature_hash_bytes).decode("utf-8").rstrip("=") - origin_info = f"android:apk-key-hash:{app_signature_hash_base64}" - session_transcript_list = generate_openid4vp_session_transcript( - client_id, nonce_base64_unpadded, origin_info, encryption_public_jwk_thumbprint - ) - except ValueError as e: - print(f"Error processing Android signature hash: {e}. Ensure config.ANDROID_APP_SIGNATURE_HASH is correct hex.") - return None + # Calculate the base64 encoded SHA256 hash of the app signing cert + try: + app_signature_hash_bytes = bytes.fromhex(config.ANDROID_APP_SIGNATURE_HASH) + app_signature_hash_base64 = base64.b64encode(app_signature_hash_bytes).decode("utf-8").rstrip("=") + origin_info = f"android:apk-key-hash:{app_signature_hash_base64}" + except ValueError as e: + print(f"Error processing Android signature hash: {e}. Ensure config.ANDROID_APP_SIGNATURE_HASH is correct hex.") + return None + session_transcript_list = generate_openid4vp_session_transcript( + client_id, nonce_base64_unpadded, origin_info, encryption_public_jwk_thumbprint + ) # print(f"Using Session Transcript (List) for Verification: {session_transcript_list}") # Debugging @@ -796,6 +814,7 @@ def handle_request_initiation(): doctypes = request_data.get("doctype") # Expect a list of strings # Use 'attributes' for consistency, default to empty list if missing requested_attributes = request_data.get("attributes", []) + origin = request_data.get("origin", "") is_zkp_request = False if "requestZkp" in request_data and request_data["requestZkp"] is True: diff --git a/verifiers-reference-implementation/python-server/templates/RP_web.html b/verifiers-reference-implementation/python-server/templates/RP_web.html index 648ae7b..ad3a7cc 100644 --- a/verifiers-reference-implementation/python-server/templates/RP_web.html +++ b/verifiers-reference-implementation/python-server/templates/RP_web.html @@ -133,6 +133,16 @@

Verification Result

+ @@ -375,6 +385,7 @@

Verification Result

console.error("Verification POST failed:", error); showStatus(`Verification failed: An error occurred while communicating with the server.`, "error"); resultDisplay.classList.add('hidden'); + document.getElementById('troubleshooting-hints').classList.remove('hidden'); } } @@ -389,7 +400,7 @@

Verification Result

const credentialResponse = await navigator.credentials.get({ digital: { requests: [{ - protocol: protocol, + protocol: "openid4vp", data: data }] } @@ -408,6 +419,7 @@

Verification Result

} catch (e) { showStatus(`Wallet request cancelled or failed: ${e.message}`, "error"); console.error("Error getting credential from wallet:", e); + document.getElementById('troubleshooting-hints').classList.remove('hidden'); } } @@ -450,6 +462,7 @@

Verification Result

try { const response = await sendRequestToServer(requestBody); const data = await response.json(); + console.log("Generated Request Payload:", data); if (!response.ok) { throw new Error(data.error || `Server responded with ${response.status}`); @@ -516,6 +529,7 @@

Verification Result

console.error("Manual verification failed:", error); showStatus(`Verification failed: An error occurred while communicating with the server.`, "error"); resultDisplay.classList.add('hidden'); + document.getElementById('troubleshooting-hints').classList.remove('hidden'); } } @@ -527,14 +541,18 @@

Verification Result

resultOutput.textContent = JSON.stringify(data, null, 2); resultDisplay.classList.remove('hidden'); + const hintsBox = document.getElementById('troubleshooting-hints'); + if (data.success) { resultOutput.parentElement.classList.remove('bg-red-100', 'text-red-800'); resultOutput.parentElement.classList.add('bg-green-100', 'text-green-800'); showStatus(`Verification successful!`, "success"); + hintsBox.classList.add('hidden'); } else { resultOutput.parentElement.classList.remove('bg-green-100', 'text-green-800'); resultOutput.parentElement.classList.add('bg-red-100', 'text-red-800'); showStatus(`Verification failed: ${data.error || 'Unknown error'}`, 'error'); + hintsBox.classList.remove('hidden'); } }