Skip to content

Brief #21 — OpenID4VCI Issuer Endpoint

SURFACE: keys.ucca.online (ucca-keys worker) DO NOT TOUCH: ucca-site, ucca-engine, ucca-ops, ucca-ir, rtopacks-site, ucca-api, ucca-corporate Cloudflare account: e5a9830215a8d88961dc6c80a8c7442a D1: engine-db (0efa8970-0053-4623-8436-4e877af10887)


→ ALEX

What this brief does

Adds an OpenID4VCI 1.0 compliant credential issuance endpoint to ucca-keys. UCCA becomes a standards-compliant credential issuer. A wallet (Mattr, Google Wallet compatible) can request a UCCA capability credential and receive a signed W3C VC 2.0 JSON-LD or SD-JWT VC in return, delivered via the OpenID4VCI protocol. The OIDF conformance suite must pass against this implementation.


New endpoints on ucca-keys

1. /.well-known/openid-credential-issuer

OpenID4VCI issuer metadata document. Required by spec.

{
  "issuer": "https://keys.ucca.online",
  "credential_issuer": "https://keys.ucca.online",
  "credential_endpoint": "https://keys.ucca.online/v1/credentials",
  "token_endpoint": "https://keys.ucca.online/v1/token",
  "jwks_uri": "https://keys.ucca.online/.well-known/jwks.json",
  "credential_configurations_supported": {
    "UCCACapabilityCredential": {
      "format": "jwt_vc_json",
      "scope": "UCCACapabilityCredential",
      "cryptographic_binding_methods_supported": ["did:web", "jwk"],
      "credential_signing_alg_values_supported": ["ES256"],
      "credential_definition": {
        "@context": [
          "https://www.w3.org/2018/credentials/v1",
          "https://ucca.online/ns/ucco/v1"
        ],
        "type": ["VerifiableCredential", "UCCACapabilityCredential"]
      },
      "display": [
        {
          "name": "UCCA Capability Credential",
          "locale": "en-AU",
          "logo": {
            "uri": "https://ucca.online/logo.png",
            "alt_text": "UCCA Logo"
          },
          "background_color": "#0a0a0a",
          "text_color": "#c8dff0"
        }
      ]
    }
  }
}

2. /.well-known/jwks.json

Public key set for signature verification. Derive from the existing signing key already used in did:web:ucca.online. Must expose the same key as the DID doc's verificationMethod. Format:

{
  "keys": [
    {
      "kty": "EC",
      "crv": "P-256",
      "kid": "signing-key-v1",
      "use": "sig",
      "alg": "ES256",
      "x": "<x coordinate>",
      "y": "<y coordinate>"
    }
  ]
}

3. POST /v1/token

OAuth 2.0 token endpoint. Supports urn:ietf:params:oauth:grant-type:pre-authorized_code flow (pre-auth code flow — the simplest valid OpenID4VCI flow, no user login required).

Request body (form-encoded):

grant_type=urn:ietf:params:oauth:grant-type:pre-authorized_code
&pre-authorized_code=<code>
&user_pin=<pin if required>

Response:

{
  "access_token": "<opaque token>",
  "token_type": "Bearer",
  "expires_in": 86400,
  "c_nonce": "<random nonce>",
  "c_nonce_expires_in": 86400
}

Token and pre-auth codes stored in engine-db D1 table vc_tokens:

CREATE TABLE IF NOT EXISTS vc_tokens (
  id TEXT PRIMARY KEY,
  pre_auth_code TEXT UNIQUE NOT NULL,
  access_token TEXT UNIQUE,
  c_nonce TEXT,
  envelope_id TEXT NOT NULL,
  holder_did TEXT,
  used INTEGER DEFAULT 0,
  created_at TEXT DEFAULT (datetime('now')),
  expires_at TEXT NOT NULL
);

4. POST /v1/credentials

Credential endpoint. Accepts Bearer token + proof of key possession. Returns signed VC.

Request:

{
  "format": "jwt_vc_json",
  "credential_definition": {
    "type": ["VerifiableCredential", "UCCACapabilityCredential"]
  },
  "proof": {
    "proof_type": "jwt",
    "jwt": "<holder key binding JWT containing c_nonce>"
  }
}

Steps: 1. Validate Bearer token against vc_tokens table — must exist, not used, not expired 2. Validate proof JWT — must contain matching c_nonce in claims 3. Extract envelope_id from token record 4. Call internal get_envelope(envelope_id) → retrieve TriumvirateEnvelope + JobEnvelope from engine-db 5. Call existing renderer/vc.py logic (replicate in JS or call engine API endpoint) to produce W3C VC 2.0 JSON-LD 6. Sign with UCCA signing key → attach proof block 7. Mark token as used in D1 8. Return:

{
  "format": "jwt_vc_json",
  "credential": "<signed JWT or JSON-LD VC>",
  "c_nonce": "<new nonce>",
  "c_nonce_expires_in": 86400
}

5. POST /v1/credential-offer

UCCA-internal endpoint (not part of OpenID4VCI spec, but needed to initiate flows). Called by the engine or ops console to generate a credential offer for a given envelope.

Request:

{
  "envelope_id": "<JobEnvelope id>",
  "holder_did": "did:web:recipient.example.com"
}

Response — credential offer URI ready to hand to a wallet:

{
  "offer_uri": "openid-credential-offer://?credential_offer_uri=https://keys.ucca.online/v1/offers/<offer_id>",
  "offer_id": "<uuid>",
  "pre_authorized_code": "<code>",
  "expires_at": "<ISO datetime>"
}

Offer stored in D1 vc_tokens table as pre-auth code record.

6. GET /v1/offers/<offer_id>

Returns credential offer object for wallets that fetch by URI:

{
  "credential_issuer": "https://keys.ucca.online",
  "credential_configuration_ids": ["UCCACapabilityCredential"],
  "grants": {
    "urn:ietf:params:oauth:grant-type:pre-authorized_code": {
      "pre-authorized_code": "<code>",
      "user_pin_required": false
    }
  }
}


Engine-side: new API endpoint

Add to ucca-api worker (or ucca-engine Python if Alex prefers):

POST /internal/vc-payload

Called by ucca-keys during credential issuance. Takes envelope_id, returns the VC payload JSON ready for signing (same output as renderer/vc.py). Auth: shared secret header X-UCCA-Internal.

This keeps the VC construction logic in Python (where it belongs) and ucca-keys just handles the OpenID4VCI protocol layer + signing.


New D1 table: vc_issuance_log

CREATE TABLE IF NOT EXISTS vc_issuance_log (
  id TEXT PRIMARY KEY,
  envelope_id TEXT NOT NULL,
  holder_did TEXT,
  format TEXT DEFAULT 'jwt_vc_json',
  issued_at TEXT DEFAULT (datetime('now')),
  offer_id TEXT,
  wallet_hint TEXT,
  cf_country TEXT,
  cf_city TEXT
);

Log every issuance. This feeds the ops console VC status card (Brief #20 deferred stub).


Conformance test

Run the OIDF conformance suite against the live endpoint:

# Clone OIDF conformance suite
git clone https://gitlab.com/openid/conformance-suite.git
cd conformance-suite

# Run against UCCA issuer (pre-auth code flow)
./gradlew bootRun

# In the web UI at localhost:8080:
# Plan: OpenID4VCI Pre-Authorized Code Flow
# Issuer URL: https://keys.ucca.online
# credential_configuration_id: UCCACapabilityCredential

All mandatory tests must pass before this brief is marked closed. Paste results into the ops console VC status card.


Mattr wallet test (human presentation)

  1. Install Mattr Wallet (iOS or Android) — free, open source compatible
  2. Scan the credential offer QR code generated by /v1/credential-offer
  3. QR encodes: openid-credential-offer://?credential_offer_uri=https://keys.ucca.online/v1/offers/<id>
  4. Wallet fetches offer → hits token endpoint → hits credential endpoint → stores VC
  5. Verify the credential renders in wallet with UCCA issuer identity visible
  6. Screenshot for ops console

AI agent test

No wallet needed. Direct HTTP:

import httpx, json

# Step 1: Get credential offer
offer = httpx.post("https://keys.ucca.online/v1/credential-offer", json={
    "envelope_id": "<test_envelope_id>",
    "holder_did": "did:web:agent.example.com"
}).json()

# Step 2: Exchange pre-auth code for token
token = httpx.post("https://keys.ucca.online/v1/token", data={
    "grant_type": "urn:ietf:params:oauth:grant-type:pre-authorized_code",
    "pre-authorized_code": offer["pre_authorized_code"]
}).json()

# Step 3: Request credential (no proof required for agent flow — set user_pin_required: false)
credential = httpx.post("https://keys.ucca.online/v1/credentials",
    headers={"Authorization": f"Bearer {token['access_token']}"},
    json={
        "format": "jwt_vc_json",
        "credential_definition": {"type": ["VerifiableCredential", "UCCACapabilityCredential"]}
    }
).json()

print(json.dumps(credential, indent=2))

Save this as scripts/test_oidc4vci_agent.py in ucca-engine.


New files

ucca-keys worker:
  src/routes/openid/metadata.js       ← /.well-known/openid-credential-issuer
  src/routes/openid/jwks.js           ← /.well-known/jwks.json
  src/routes/openid/token.js          ← POST /v1/token
  src/routes/openid/credentials.js    ← POST /v1/credentials
  src/routes/openid/offer.js          ← POST /v1/credential-offer + GET /v1/offers/:id
  src/router.js                       ← add all new routes

ucca-engine (Python):
  renderer/openid4vci.py              ← VC payload builder for internal API
  scripts/test_oidc4vci_agent.py      ← agent flow smoke test

Definition of done

  • /.well-known/openid-credential-issuer returns valid JSON
  • /.well-known/jwks.json returns public key matching DID doc
  • Pre-auth code flow completes end-to-end (offer → token → credential)
  • OIDF conformance suite: pre-auth code flow — all mandatory tests pass
  • Mattr wallet: credential received and renders with UCCA issuer
  • AI agent test script runs and returns a signed VC
  • vc_issuance_log writes on every issuance
  • Ops console VC status card shows issuance count + last issued timestamp

→ TIM

What this is in plain English

Right now UCCA can make a credential but there's no standardised way for anyone to collect it. A wallet app or an AI agent has no protocol to knock on the door and say "give me the credential I earned."

OpenID4VCI is that door. It's the standard that Google Wallet, the EU digital identity ecosystem, 38 governments, and increasingly AI agent frameworks use to collect credentials from issuers. It sits on top of OAuth 2.0 — the same plumbing the whole internet uses for "login with Google."

This brief adds that door to ucca-keys. The flow is:

  1. UCCA engine finishes a credential → generates a QR code or a link
  2. Person opens their Mattr wallet, scans the QR → wallet talks to ucca-keys
  3. ucca-keys checks the token, retrieves the TriumvirateEnvelope, hands a signed VC to the wallet
  4. The credential now lives in the wallet, verifiable by anyone, traceable back to the UCCA engine

For AI agents it's even simpler — no wallet, just an API call. Agent asks, engine answers, credential is machine-verified.

Why ucca-keys and not a new worker

ucca-keys is already the trust anchor — it holds the signing key, it serves the DID document, it's the thing the world already knows to trust for UCCA credentials. Adding OpenID4VCI here means the trust chain stays in one place. One worker, one signing key, one issuer identity.

Test targets and why

  • Mattr wallet — open source, real production wallet, used by the New Zealand government, participated in the official OIDF interoperability tests. If it works with Mattr, it works with the ecosystem.
  • OIDF conformance suite — the official free test suite from the OpenID Foundation. If UCCA passes this, we can legitimately say we are a conformant OpenID4VCI issuer. That sentence matters to enterprise buyers and government.
  • AI agent script — proves the machine-to-machine path. No wallet, pure API. This is the agentic credential verification story.

What this unlocks

Google Wallet compatibility comes next (same protocol, different wallet app). Apple Wallet is a separate workstream (different format — mdoc — deferred). LinkedIn credential display is watch-and-wait. But the engine primitive — issue a credential that any conformant wallet or agent can collect and verify — is live after this brief.

OPS SURFACE RULE

After this brief deploys, add to ops.ucca.online → RTOpacks world block → VC Output: - Count of VCs issued (from vc_issuance_log) - Last issued timestamp - Conformance suite result badge (PASS/FAIL) - Sample credential offer QR generator (envelope_id input → QR out)


Brief #21 — UCCA Inc — 17 March 2026