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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,6 @@ iOSInjectionProject/
# E2E test local config
waltid-applications/waltid-wallet-demo-ios/scripts/e2e.env
waltid-applications/waltid-wallet-demo-android/scripts/e2e.env

# OpenID conformance runner local state
waltid-services/waltid-openid4vp-conformance-runners/mongo/
87 changes: 75 additions & 12 deletions waltid-services/waltid-openid4vp-conformance-runners/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Conformance test runners for OpenID4VCI and OpenID4VP against the [OpenID Founda

1. **Conformance Suite** running locally:
```bash
cd ~/dev/openid/conformance-suite
cd openid/conformance-suite
docker compose -f docker-compose-walt.yml up -d
```
Verify: https://localhost.emobix.co.uk:8443/
Expand All @@ -20,11 +20,12 @@ Conformance test runners for OpenID4VCI and OpenID4VP against the [OpenID Founda
### Running Tests

```bash
cd ~/dev/walt-id/waltid-unified-build
cd waltid-unified-build

# VCI Issuer tests
export OPENID4VCI_CONFORMANCE_CREDENTIAL_ISSUER_URL="https://YOUR-NGROK.ngrok-free.app/openid4vci"
./gradlew :waltid-services:waltid-openid4vp-conformance-runners:test --tests "IssuerConformanceTests"
export OPENID4VCI_CONFORMANCE_CLIENT_ATTESTER_JWKS_FILE="$PWD/waltid-identity/waltid-services/waltid-openid4vp-conformance-runners/src/test/resources/keys/attester-key.json"
./gradlew :waltid-services:waltid-openid4vp-conformance-runners:test --tests "id.walt.openid4vp.conformance.IssuerConformanceTests"

# VP Verifier tests
export VERIFIER_NGROK_URL="https://YOUR-NGROK.ngrok-free.app"
Expand All @@ -35,17 +36,16 @@ export VERIFIER_NGROK_URL="https://YOUR-NGROK.ngrok-free.app"

## Test Status Summary

### VCI Issuer (2026-07-08)
### VCI Issuer (handover snapshot, 2026-07-14)

| Test | Result | Notes |
|------|--------|-------|
| `oid4vci-1_0-issuer-metadata-test` | ✅ PASSED | Metadata endpoint compliant |
| `oid4vci-1_0-issuer-happy-flow` | ❌ FAILED | Missing RFC 9207 `iss` parameter |
| `oid4vci-1_0-issuer-metadata-test-signed` | ❌ FAILED | Test environment issue |
The active issuer2 handover profile is the base issuer conformance plan:

**Pass rate: 1/3 (33%)**
- Test plan: `oid4vci-1_0-issuer-test-plan`
- Variant: `sender_constrain=dpop`, `client_auth_type=client_attestation`, `vci_authorization_code_flow_variant=wallet_initiated`, `vci_grant_type=pre-authorized_code`, `credential_format=sd_jwt_vc`, `authorization_request_type=simple`, `fapi_request_method=unsigned`, `fapi_profile=vci`, `fapi_response_mode=plain_response`
- Config: issuer URL, credential configuration ID, client-attestation issuer, attester JWKS, DPoP client JWKS, and an issuer2-generated `credential_offer_uri`
- Status: observed passing in the local handover environment. Re-run before treating this as a CI or release signal.

📄 **Details:** [docs/VCI-ISSUER.md](docs/VCI-ISSUER.md)
Details: [docs/VCI-ISSUER.md](docs/VCI-ISSUER.md)

### VP Verifier (2026-07-08)

Expand Down Expand Up @@ -76,7 +76,7 @@ export VERIFIER_NGROK_URL="https://YOUR-NGROK.ngrok-free.app"

| Interface | Baseline | HAIP | Key Difference |
|-----------|----------|------|----------------|
| **VCI Issuer** | `pre-authorized_code` | `authorization_code` | Grant type |
| **VCI Issuer** | `pre-authorized_code` + `client_attestation` | `authorization_code` + `private_key_jwt` | Grant/auth profile |
| **VP Verifier** | `x509_san_dns` | `x509_hash` | Client ID scheme |

**Baseline:** Automated functional testing
Expand Down Expand Up @@ -131,3 +131,66 @@ waltid-openid4vp-conformance-runners/
| Metadata 404 | Check path: `/.well-known/openid-credential-issuer/<path>` |

See individual docs for detailed troubleshooting.

## VCI Issuer Client Attestation

Issuer conformance plans use `client_attestation` by default. The default attester key is:

```text
src/test/resources/keys/attester-key.json
```

For issuer2 `static-jwk` verification, use the matching public key:

```text
src/test/resources/keys/attester-public-jwk.json
```

For issuer2 `x509-chain` verification, trust the matching local root CA:

```text
src/test/resources/certs/root-ca.pem
```

For the local handover run, issuer2 needs matching base URL, CI token key, and client-attestation trust configuration. Replace `baseUrl` when the ngrok tunnel changes.

```hocon
baseUrl = "https://9bc0-2a02-8109-8686-5d00-8333-124e-42b0-6481.ngrok-free.app"

ciTokenKey = """{"type":"jwk","jwk":{"kty":"EC","d":"KJ4k3Vcl5Sj9Mfq4rrNXBm2MoPoY3_Ak_PIR_EgsFhQ","crv":"P-256","x":"G0RINBiF-oQUD3d5DGnegQuXenI29JDaMGoMvioKRBM","y":"ed3eFGs2pEtrp7vAZ7BLcbrUtpKkYWAT2JPUQK4lN4E"}}"""

clientAuthenticationConfig {
supportedMethods = [
{
type = "preauth-anonymous"
},
{
type = "client-attestation"
config {
verificationMethod {
type = "x509-chain"
trustedRootCertificatesPem = [
"""-----BEGIN CERTIFICATE-----
MIICCTCCAa6gAwIBAgIUd2OgSqKSx5bt1dwVpxyOsdBrCwEwCgYIKoZIzj0EAwIw
UDEvMC0GA1UEAwwmd2FsdC5pZCBPcGVuSUQ0VkNJIENvbmZvcm1hbmNlIFRlc3Qg
Q0ExEDAOBgNVBAoMB3dhbHQuaWQxCzAJBgNVBAYTAlVUMB4XDTI2MDcxMzE2MTYz
M1oXDTM2MDcxMDE2MTYzM1owUDEvMC0GA1UEAwwmd2FsdC5pZCBPcGVuSUQ0VkNJ
IENvbmZvcm1hbmNlIFRlc3QgQ0ExEDAOBgNVBAoMB3dhbHQuaWQxCzAJBgNVBAYT
AlVUMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcKWoEYWPMA8sMHQt4Whhdnyb
eGY4uxNJ61K8qEkR7yjxpDPlTUwMLoFY4LwvDZbmrd1wuAQzC19vN3ZCKy0waqNm
MGQwHwYDVR0jBBgwFoAUUGfw1hxU8WtLHa5RnP+dVRINVTYwEgYDVR0TAQH/BAgw
BgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFFBn8NYcVPFrSx2uUZz/
nVUSDVU2MAoGCCqGSM49BAMCA0kAMEYCIQC/45X54n1VyZuAN8vmin6cluuoNBD5
VACJ445Tx9FAuQIhAN6yqTj1u30N51FsULyrdbwXRgBRo7CgE1CZC9ejeD1E
-----END CERTIFICATE-----"""
]
}
}
}
]
}
```

The current handover intentionally preserves the variant values that passed locally, including `wallet_initiated` and `pre-authorized_code`. Do not change them to alternate conformance-suite enum spellings without re-running the same issuer2 test plan.

Do not include local runtime artifacts such as `mongo/` or a locally mutated `conformance-truststore.jks` in a clean handover commit.
Original file line number Diff line number Diff line change
Expand Up @@ -84,16 +84,35 @@ Examples:
- **Role:** Issuer (Credential Provider)
- **Credential Format:** SD-JWT VC (`vc+sd-jwt`)
- **Authentication:** DPoP + Client Attestation
- **Flow:** Authorization Code
- **Flow:** Pre-Authorized Code
- **Key Binding:** openid4vci-proof+jwt

**Test Plan Name:** `oid4vci-1_0-issuer-test-credential-issuance-dpop-client_attestation-sd_jwt_vc-issuer_initiated-simple-immediate-unsigned-authorization_code-by_value-plain`
**Test Plan Name:** `oid4vci-1_0-issuer-test-plan`

**Current handover variant:**

```json
{
"sender_constrain": "dpop",
"client_auth_type": "client_attestation",
"vci_authorization_code_flow_variant": "wallet_initiated",
"credential_format": "sd_jwt_vc",
"authorization_request_type": "simple",
"fapi_request_method": "unsigned",
"vci_grant_type": "pre-authorized_code",
"fapi_profile": "vci",
"fapi_response_mode": "plain_response"
}
```

The conformance-suite UI may display `vci_credential_encryption=plain` as part of the normalized variant. The runner creates an issuer2 credential offer and adds it to the plan configuration as `vci.credential_offer_uri`.

**Status:** ⚠️ **53/55 tests passing**
**Status:** Observed passing in the local issuer2 handover setup. Re-run before using as a CI or release gate.

**Known Issues:**
1. Missing RFC 9207 `iss` in authorization response
2. Unexpected HTTP status on credential endpoint
**Handover notes:**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chsavvaidis Are these issues fixed?

  1. Missing RFC 9207 iss in authorization response
  2. Unexpected HTTP status on credential endpoint

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1. Keep the working variant values above unless the changed values are re-tested against the same conformance-suite plan.
2. The issuer2 target must trust the attester key via `static-jwk` or the generated root CA via `x509-chain`.
3. Keycloak is only needed for authorization-code plans, not for this pre-authorized-code issuer baseline.

**HAIP Features:**
- ✅ DPoP support
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@ This document covers setup, execution, and status of OpenID4VCI Issuer conforman

| Profile | Test Plan | Grant Type | Format | Status |
|---------|-----------|------------|--------|--------|
| issuer2 Client Attestation + DPoP | `oid4vci-1_0-issuer-test-plan` | `pre-authorized_code` | SD-JWT VC | Observed passing locally |
| SD-JWT Baseline | `oid4vci-1_0-issuer-metadata-test` | `pre-authorized_code` | SD-JWT VC | ✅ PASSED |
| SD-JWT Happy Flow | `oid4vci-1_0-issuer-happy-flow` | `pre-authorized_code` | SD-JWT VC | ❌ FAILED |
| SD-JWT Signed Metadata | `oid4vci-1_0-issuer-metadata-test-signed` | `pre-authorized_code` | SD-JWT VC | ❌ FAILED |

**Last tested:** 2026-07-08
**Last tested:** 2026-07-14

---

Expand All @@ -23,39 +21,45 @@ This document covers setup, execution, and status of OpenID4VCI Issuer conforman
- Metadata structure compliant ✓
- Credential configurations valid ✓

### ❌ Failing Tests

**`oid4vci-1_0-issuer-happy-flow`** — Full issuance flow
| Issue | Severity | Description |
|-------|----------|-------------|
| Missing `iss` parameter | FAILURE | Authorization response must include `iss` (RFC 9207) |
| Invalid HTTP status | FAILURE | Cascading failure from above |
| Unexpected auth parameters | WARNING | Review authorization endpoint response |

**`oid4vci-1_0-issuer-metadata-test-signed`** — Signed metadata validation
| Issue | Severity | Description |
|-------|----------|-------------|
| Metadata fetch failed | FAILURE | ngrok URL expired during test run |
```text
sender_constrain=dpop
client_auth_type=client_attestation
vci_authorization_code_flow_variant=wallet_initiated
credential_format=sd_jwt_vc
authorization_request_type=simple
fapi_request_method=unsigned
vci_grant_type=pre-authorized_code
vci_credential_encryption=plain
fapi_profile=vci
fapi_response_mode=plain_response
```

### Required Fixes
The runner also creates an issuer2 credential offer before creating the conformance-suite plan and injects it as `vci.credential_offer_uri`. The resulting config includes:

1. **RFC 9207 Compliance** — Add `iss` parameter to authorization response
- Specification: [RFC 9207 - OAuth 2.0 Authorization Server Issuer Identification](https://datatracker.ietf.org/doc/html/rfc9207)
- The `iss` parameter prevents mix-up attacks
- `vci.credential_issuer_url`
- `vci.credential_configuration_id`
- `vci.client_attestation_issuer`
- `vci.client_attester_keys_jwks`
- `vci.credential_offer_uri`
- `client_attestation.issuer`
- `client_attestation.attester_jwks`
- `client.jwks` and `client2.jwks` for DPoP

2. **Stable Test URL** — Use persistent ngrok URL or stable test environment
Do not rename the working variant values to alternate suite enum spellings without re-running this same plan. The observed passing setup used the values above.

---

## Prerequisites

1. **Conformance Suite** running at `https://localhost.emobix.co.uk:8443`
```bash
cd ~/dev/openid/conformance-suite
cd openid/conformance-suite
docker compose -f docker-compose-walt.yml up -d
```

2. **Keycloak** running at `http://keycloak.localhost:8080` with `issuer` realm
2. **Keycloak** running at `http://keycloak.localhost:8080` with `issuer` realm for authorization-code plans. The pre-authorized-code issuer baseline does not need Keycloak.

3. **ngrok** for exposing issuer to conformance suite
```bash
Expand Down Expand Up @@ -125,7 +129,7 @@ baseUrl = "https://YOUR-NGROK-SUBDOMAIN.ngrok-free.app"
ngrok http 7002

# Terminal 2: Start issuer-api2
cd ~/dev/walt-id/waltid-unified-build/waltid-identity
cd waltid-unified-build/waltid-identity
./gradlew :waltid-services:waltid-issuer-api2:run
```

Expand All @@ -139,23 +143,23 @@ curl -s "https://YOUR-NGROK.ngrok-free.app/.well-known/openid-credential-issuer/
## Running Tests

```bash
cd ~/dev/walt-id/waltid-unified-build
cd waltid-unified-build

# Set environment variables
export OPENID4VCI_CONFORMANCE_CREDENTIAL_ISSUER_URL="https://YOUR-NGROK.ngrok-free.app/openid4vci"
export OPENID4VCI_CONFORMANCE_SD_JWT_CREDENTIAL_CONFIGURATION_ID="identity_credential"
export OPENID4VCI_CONFORMANCE_CLIENT_ATTESTER_JWKS_FILE="$PWD/waltid-identity/waltid-services/waltid-openid4vp-conformance-runners/src/test/resources/keys/attester-key.json"

# Run tests
./gradlew :waltid-services:waltid-openid4vp-conformance-runners:test --tests "IssuerConformanceTests"
./gradlew :waltid-services:waltid-openid4vp-conformance-runners:test --tests "id.walt.openid4vp.conformance.IssuerConformanceTests"
```

### Test Execution Flow

1. **Baseline tests** run automatically (pre-authorized_code grant)
2. **HAIP tests** enter WAITING state for OAuth login:
- Click authorization button in conformance suite UI
- Login at Keycloak with `testuser` / `testuser`
- After "Processing response" screen, return to test results
1. The test fetches issuer metadata from `/.well-known/openid-credential-issuer/openid4vci`.
2. The runner creates an issuer2 credential offer through the issuer management API.
3. The runner creates `oid4vci-1_0-issuer-test-plan` with the handover variant above.
4. The conformance suite uses the supplied `credential_offer_uri`, DPoP keys, and client-attestation keys to call issuer2.

---

Expand All @@ -166,8 +170,26 @@ export OPENID4VCI_CONFORMANCE_SD_JWT_CREDENTIAL_CONFIGURATION_ID="identity_crede
| `OPENID4VCI_CONFORMANCE_CREDENTIAL_ISSUER_URL` | Full issuer URL | `https://xxx.ngrok-free.app/openid4vci` |
| `OPENID4VCI_CONFORMANCE_SD_JWT_CREDENTIAL_CONFIGURATION_ID` | SD-JWT credential config | `identity_credential` |
| `OPENID4VCI_CONFORMANCE_MDOC_CREDENTIAL_CONFIGURATION_ID` | mDOC credential config | `org.iso.18013.5.1.mDL` |
| `OPENID4VCI_CONFORMANCE_CLIENT_ATTESTER_JWKS_FILE` | Private client-attester JWK/JWKS used by the conformance suite to sign client attestation JWTs | `src/test/resources/keys/attester-key.json` |
| `OPENID4VCI_CONFORMANCE_AUTHORIZATION_SERVER` | External auth server | (optional) |

## Client Attestation Keys

The issuer runner uses `client_attestation` by default. The default private attester key includes an `x5c` chain so the conformance suite can create a valid `OAuth-Client-Attestation` JWT:

```text
src/test/resources/keys/attester-key.json
```

issuer2 can verify the same attestation in either of these modes:

| issuer2 verification method | Test resource to configure |
|-----------------------------|----------------------------|
| `static-jwk` | `src/test/resources/keys/attester-public-jwk.json` |
| `x509-chain` | `src/test/resources/certs/root-ca.pem` as `trustedRootCertificatesPem` |

The EUDI PID root certificate can only be used if the attester JWK also has a leaf certificate/private key chain issued under that root. A trusted root PEM by itself is not enough to generate a valid client attestation JWT.

---

## Troubleshooting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,12 @@ class Oid4vciIssuerClientAttestationDpop(
}
""".trimIndent()
private val clientJwksJson = Json.decodeFromString<JsonObject>(clientJwks)
// OSS configuration: Use private_key_jwt instead of client_attestation
// For Enterprise: Change "private_key_jwt" to "client_attestation"
// Using pre-authorized_code for baseline happy-path testing
// Using pre-authorized_code for baseline happy-path testing.
private val moduleVariant = """
{
"fapi_profile": "vci",
"sender_constrain": "dpop",
"client_auth_type": "private_key_jwt",
"client_auth_type": "client_attestation",
"vci_authorization_code_flow_variant": "wallet_initiated",
"authorization_request_type": "simple",
"fapi_request_method": "unsigned",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import id.walt.openid4vp.conformance.config.ConformanceConfig
import id.walt.openid4vp.conformance.testplans.IssuerConformanceTestRunner
import id.walt.openid4vp.conformance.testplans.http.ConformanceInterface
import org.junit.jupiter.api.Assumptions.assumeTrue
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
Expand Down Expand Up @@ -140,20 +140,24 @@ class IssuerConformanceTests {
}

@Test
fun runIssuerConformanceTests() = runTest(timeout = 10.minutes) {
assumeTrue(isConformanceAvailable, "OpenID conformance suite is not reachable")
assumeTrue(isIssuerConfigured, "No credential issuer URL / enterprise issuer target configured")

IssuerConformanceTestRunner(
credentialIssuerUrl = requireNotNull(credentialIssuerUrl),
conformanceHost = ConformanceConfig.CONFORMANCE_HOST,
conformancePort = ConformanceConfig.CONFORMANCE_PORT,
sdJwtCredentialConfigurationId = sdJwtCredentialConfigurationId,
mdocCredentialConfigurationId = mdocCredentialConfigurationId,
clientAttestationIssuer = clientAttestationIssuer,
clientAttesterJwks = clientAttesterJwks,
authorizationServer = authorizationServer,
credentialProofTypeHint = credentialProofTypeHint,
).run()
fun runIssuerConformanceTests() {
runBlocking {
assumeTrue(isConformanceAvailable, "OpenID conformance suite is not reachable")
assumeTrue(isIssuerConfigured, "No credential issuer URL / enterprise issuer target configured")

withTimeout(10.minutes) {
IssuerConformanceTestRunner(
credentialIssuerUrl = requireNotNull(credentialIssuerUrl),
conformanceHost = ConformanceConfig.CONFORMANCE_HOST,
conformancePort = ConformanceConfig.CONFORMANCE_PORT,
sdJwtCredentialConfigurationId = sdJwtCredentialConfigurationId,
mdocCredentialConfigurationId = mdocCredentialConfigurationId,
clientAttestationIssuer = clientAttestationIssuer,
clientAttesterJwks = clientAttesterJwks,
authorizationServer = authorizationServer,
credentialProofTypeHint = credentialProofTypeHint,
).run()
}
}
}
}
Loading
Loading