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
21 changes: 19 additions & 2 deletions verifiers-reference-implementation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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": [
Expand All @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Empty file.
32 changes: 26 additions & 6 deletions verifiers-reference-implementation/python-server/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,32 @@
limitations under the License.
'''

PRIVATE_KEY = """-----BEGIN PRIVATE KEY-----
<Your private key / Should get it directly from your
Key Management service>
-----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-----
<Your Pulic cert / Can be added here, can come from
your Key management Service.>
MIIBGDCBvgIJAOvRMvbc+21VMAoGCCqGSM49BAMCMBQxEjAQBgNVBAMMCWxvY2Fs
aG9zdDAeFw0yNjA0MjgxNDA1MzZaFw0yNzA0MjgxNDA1MzZaMBQxEjAQBgNVBAMM
CWxvY2FsaG9zdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABDWZ2pO/v2QQhBWy
Ief8dLMImbhBBKFVXspCf0nld891oqYf3/KDIRwVALepEvpPUjsBn3O3yqxFiS4A
HjMSPewwCgYIKoZIzj0EAwIDSQAwRgIhAIQv1PzR9RBfPL8YyQztI7C3uCinjKK6
LUTh/UVk5JETAiEAsg0rA+pMpm9HU4uZpR67lbVgHGbuo/rUKVpOKF7Dld4=
-----END CERTIFICATE-----"""
71 changes: 45 additions & 26 deletions verifiers-reference-implementation/python-server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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 = {
Expand All @@ -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
}


Expand Down Expand Up @@ -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) ---
Expand Down Expand Up @@ -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("<path_to_ZKverifier>"):
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.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,16 @@ <h2 class="text-2xl font-semibold mb-4">Verification Result</h2>
<pre><code id="result-output"></code></pre>
</div>
</div>
<div id="troubleshooting-hints" class="hidden mt-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<h3 class="text-lg font-semibold text-yellow-800 mb-2">Troubleshooting Hints</h3>
<ul class="list-disc list-inside text-yellow-700 text-sm space-y-1">
<li>If verification failed, ensure you have loaded valid <b>Issuer IACA root certificates</b> in the backend.
</li>
<li>If ZK verification failed, ensure you have configured a valid <b>ZK Verifier URL</b> in
<code>config.py</code>.
</li>
</ul>
</div>
</main>
</div>

Expand Down Expand Up @@ -375,6 +385,7 @@ <h2 class="text-2xl font-semibold mb-4">Verification Result</h2>
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');
}
}

Expand All @@ -389,7 +400,7 @@ <h2 class="text-2xl font-semibold mb-4">Verification Result</h2>
const credentialResponse = await navigator.credentials.get({
digital: {
requests: [{
protocol: protocol,
protocol: "openid4vp",
data: data
}]
}
Expand All @@ -408,6 +419,7 @@ <h2 class="text-2xl font-semibold mb-4">Verification Result</h2>
} 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');
}
}

Expand Down Expand Up @@ -450,6 +462,7 @@ <h2 class="text-2xl font-semibold mb-4">Verification Result</h2>
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}`);
Expand Down Expand Up @@ -516,6 +529,7 @@ <h2 class="text-2xl font-semibold mb-4">Verification Result</h2>
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');
}
}

Expand All @@ -527,14 +541,18 @@ <h2 class="text-2xl font-semibold mb-4">Verification Result</h2>
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');
}
}

Expand Down