Verifier · 驗證憑證
這頁是給 Verifier 角色:要求 holder 證明某件事的服務。從你的後端視角,驗證只有兩支 API 呼叫 — 建立 authorization request、然後讀結果。中間的事(取得簽妥的 request object、提示 user、組 VP token、提交)都在錢包端。
你做什麼 vs. TCS 做什麼
Section titled “你做什麼 vs. TCS 做什麼”你要實作的:
- 建立 request。 POST 你的 DCQL query 與 verifier 身分到 Verifier Service。
- 交付 request URI。
- 保管模式: 代表 user 呼叫 Holder Service
POST /v1/oid4vp/presentation。 - 非保管模式: 把 deep link 渲染成 QR 給 user 的錢包 App。
- 保管模式: 代表 user 呼叫 Holder Service
- Poll 結果。 監看
GET /v1/verifier/result/{session_id}直到 session 變completed。
VP token、SD-JWT 簽章、key-binding JWT 你都不用自己組或驗。Verifier Service 全做完,結果端點上會回 status(整體通過/失敗)與 check_results[](逐項檢查)。
Step 1 · 建立 authorization request
Section titled “Step 1 · 建立 authorization request”curl -X POST https://verifier.turingspace.co/v1/verifier/authorization-request \ -H "X-API-Key: tcs_production_7f3a9b2c1d4e5f6a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e" \ -H "Content-Type: application/json" \ -d '{ "dcql_query": { "credentials": [ { "id": "identity_credential", "format": "dc+sd-jwt", "meta": { "vct_values": ["https://schema-registry.turingspace.co/schemas/TuringCerts_Standard_Credential/v2"] }, "claims": [ { "path": ["credentialName"] }, { "path": ["issuedTime"] } ] } ] }, "verifier_private_key": "${VERIFIER_PRIVATE_KEY}", "verifier_client_id": "did:iota:0xae72f18dc7b6af5b740edae8e4e0b2d3c9fb10a4e7d6c5b3a2f1e0d9c8b7a6f5", "response_mode": "direct_post", "expires_in": 300 }'回應(201 Created):
{ "session_id": "b2c3d4e5-f6a7-8b9c-0d1e-2f3a4b5c6d7e", "request_uri": "https://verifier.turingspace.co/v1/verifier/request/b2c3d4e5-f6a7-8b9c-0d1e-2f3a4b5c6d7e", "authorization_request": "openid4vp://authorize?client_id=did%3Aiota%3A0xae72...&request_uri=https%3A%2F%2Fverifier.turingspace.co%2Fv1%2Fverifier%2Frequest%2Fb2c3d4e5-...", "expires_at": "2026-03-26T00:05:00Z"}| 名稱 | 型別 | 必填 | 說明 |
|---|---|---|---|
dcql_query | object | 是 | 你要哪些憑證、哪些 claim。詳見 DCQL Query Structure。 |
verifier_private_key | string | 是 | Base64url 編碼的原生 Ed25519(32 bytes)。用來簽 JAR(request object)讓錢包驗「是誰在問」。 |
verifier_client_id | string | 是 | Verifier 的 DID(或 URL)。錢包同意畫面上會顯示。 |
response_mode | string | 否 | "direct_post"(預設,明文 server-to-server VP 交付)或 "direct_post.jwt"(用 ECDH-ES 加密交付;TCS 會自動以 ephemeral 公鑰填入 client_metadata.jwks)。 |
expires_in | number | 否 | Session TTL 秒數(預設 300)。 |
client_metadata | object | 否 | client_name、logo_uri、policy_uri、tos_uri、jwks — 顯示在錢包同意畫面。 |
redirect_uri | string | 否 | 錢包提交後把 user redirect 去哪。Headless 流程可省略。 |
| 狀態 | 原因 |
|---|---|
| 400 | Body 格式錯(缺欄位、dcql_query.credentials 為空等) |
| 401 | X-API-Key 缺或無效 |
Step 2 · 交付 request URI
Section titled “Step 2 · 交付 request URI”把 request 送到 holder 面前的方式取決於模式。
代表 user 把 authorization_request 傳給 Holder Service。Holder Service 會 fetch 簽妥的 request、提示 user、組 VP token、提交。詳見 Holder · 管理錢包與憑證 § 4。
curl -X POST https://holder.turingspace.co/v1/oid4vp/presentation \ -H "Authorization: Bearer ${HOLDER_USER_ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "authorization_request_uri": "openid4vp://authorize?client_id=...&request_uri=...", "selected_credentials": [ { "credential_id": "...", "query_id": "identity_credential", "disclosed_claims": ["credentialName", "issuedTime"] } ], "holder_did": "did:iota:testnet:0x7a8b...", "private_key": "${HOLDER_PRIVATE_KEY}" }'回應(200 OK):
{ "success": true, "redirect_uri": "https://verifier.example.com/callback?session_id=b2c3d4e5-..."}redirect_uri 只有 verifier 設了才會帶回 — deep-link user 過去完成 verifier 端後續流程。從 verifier 角度看 session 變 completed,verifier 整合用下面 Step 3 poll 結果。
把 authorization_request 渲染成 QR、或在 user 裝置上打開 deep link。錢包 App 跑剩下的 OID4VP,把 VP token 直接 POST 給 TCS。
Step 3 · 檢查驗證結果
Section titled “Step 3 · 檢查驗證結果”curl https://verifier.turingspace.co/v1/verifier/result/b2c3d4e5-f6a7-8b9c-0d1e-2f3a4b5c6d7e \ -H "X-API-Key: tcs_production_7f3a9b2c1d4e5f6a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e"回應(200 OK,completed):
{ "session_id": "b2c3d4e5-f6a7-8b9c-0d1e-2f3a4b5c6d7e", "status": "completed", "created_at": "2026-04-01T00:00:00.000Z", "expires_at": "2026-04-01T00:05:00.000Z", "verified_at": "2026-04-01T00:01:23.000Z", "verification_mode": "fail_fast", "check_results": [ { "check": "oid4vp_compliance", "status": "success", "reason_code": "oid4vp_request_valid", "message": "OID4VP request and response are spec-compliant" }, { "check": "credential_format", "status": "success", "reason_code": "credential_format_valid", "message": "Credential format is valid SD-JWT VC" }, { "check": "signature", "status": "success", "reason_code": "signature_verified", "message": "Issuer signature verified against the resolved DID Document" }, { "check": "issuer_trust", "status": "success", "reason_code": "not_implemented", "message": "Trust Registry check not yet wired into the verification pipeline" } ], "presentation_metadata": { "claims": { "credentialName": "Bachelor of Science", "issuedTime": "2026-01-15" }, "credential_metadata": [ { "issuer": "did:iota:testnet:0xabcdef...", "credential_type": ["UniversityDegreeCredential"], "issued_at": "2026-01-15T00:00:00.000Z", "expires_at": "2027-01-15T00:00:00.000Z" } ] }}只有 check_results 裡所有項都 success,session 才會是 completed。要做 boolean 分支判斷「憑證有沒有通過」,看 status === "completed" 即可。要對使用者解釋失敗原因,巡 check_results 找 status: "fail" 的項,把 message 拿出來顯示。
回應(200 OK,failed — 逐項結果):
{ "session_id": "b2c3d4e5-f6a7-8b9c-0d1e-2f3a4b5c6d7e", "status": "failed", "created_at": "2026-04-01T00:00:00.000Z", "expires_at": "2026-04-01T00:05:00.000Z", "verified_at": "2026-04-01T00:01:23.000Z", "verification_mode": "fail_fast", "check_results": [ { "check": "credential_format", "status": "success", "reason_code": "credential_format_valid", "message": "Credential format is valid SD-JWT VC" }, { "check": "signature", "status": "fail", "reason_code": "signature_verification_failed", "message": "Issuer signature did not verify against the resolved DID Document" } ]}fail_fast 模式下 check_results 在第一個失敗就停。run_all 模式即使遇到 fail 也跑完所有適用檢查 — 適合需要完整診斷給客服的場景。
模式設定的位置。 verification_mode 不是 POST /v1/verifier/authorization-request 的參數。它由錢包(或代表持有者的保管模式 Holder Service)在送出 VP 時,於 POST /v1/verifier/presentation body 中傳入:{"verification_mode": "run_all"}(預設 "fail_fast")。Verifier-side 只是把它讀回來,並非在 request 建立時選擇模式。
reason_code 對照
Section titled “reason_code 對照”驗證 pipeline 目前對外呈現的 check 名稱與 reason_code 封閉集合。可作為 triage 失敗或對應使用者文案的依據。
check | reason_code(成功) | reason_code(失敗) | 含義 |
|---|---|---|---|
oid4vp_compliance | oid4vp_request_valid | oid4vp_non_compliant、presentation_invalid | OID4VP 請求 / 回應結構符合規範 |
credential_format | credential_format_valid | credential_format_invalid | 出示之 payload 可解析為 dc+sd-jwt |
signature | signature_verified | signature_verification_failed、signature_payload_mismatch、issuer_key_not_resolvable | 發行方簽章對解析出之公鑰驗證通過 |
issuer_trust | not_implemented | — | Trust Registry 名單檢查(pipeline 尚未串接,目前對外呈現 not_implemented) |
schema_governance | not_implemented | — | Schema 治理檢查(roadmap,呈現 not_implemented) |
schema_version | not_implemented | — | Schema 版本政策檢查(roadmap,呈現 not_implemented) |
issuer_governance | not_implemented | — | 發行方治理檢查(roadmap,呈現 not_implemented) |
| (任一檢查) | — | dcql_not_satisfied、skipped | DCQL 未匹配(沒有任何憑證滿足查詢);或在 fail_fast 下因前次失敗被略過 |
status: "success" + reason_code: "not_implemented" 是「該檢查在 roadmap 上、尚未強制執行」的誠實訊號 —— 並不代表底層性質已被驗證。
回應(200 OK,expired):
{ "session_id": "b2c3d4e5-f6a7-8b9c-0d1e-2f3a4b5c6d7e", "status": "expired", "created_at": "2026-04-01T00:00:00.000Z", "expires_at": "2026-04-01T00:05:00.000Z", "error": { "code": "session_expired", "message": "Session TTL elapsed without a wallet response" }}Status 值
Section titled “Status 值”| Status | 意義 |
|---|---|
pending | Request 已建立,等錢包來抓 request object |
in_progress | 錢包已抓 request object,使用者尚未提交 |
completed | VP token 驗證通過;presentation_metadata 已填,且 check_results 每項都是 success |
failed | check_results 至少一項是 fail(簽章錯、nonce 不對、憑證過期等) |
expired | Session 在錢包回應前 TTL 到期;error 已填 |
實務 poll loop(每 2 秒):
while true; do RESULT=$(curl -s "$URL/v1/verifier/result/$SESSION_ID" -H "X-API-Key: $KEY") STATUS=$(echo "$RESULT" | jq -r '.status') case "$STATUS" in completed|failed|expired) echo "$RESULT" | jq .; break;; esac sleep 2donepush 模式 webhook(讓 verifier 後端不必 polling)為路線圖項目;在它推出之前,polling result 端點為唯一支援的整合路徑。如果 push 對你的部署是硬性需求,請聯繫我們。
DCQL Query Structure
Section titled “DCQL Query Structure”DCQL(Digital Credentials Query Language)描述你向 holder 要哪些憑證、哪些 claim。每個 authorization request 都需要 dcql_query。
{ "dcql_query": { "credentials": [ /* 一個或多個 credential descriptor */ ] }}Credential descriptor 欄位
Section titled “Credential descriptor 欄位”| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
id | string | 是 | 該 credential 在這份 query 內的識別符 — Holder Service 在 selected_credentials[].query_id 也會引用。 |
format | string | 是 | SD-JWT VC 用 "dc+sd-jwt"。 |
meta | object | 是 | 放憑證格式相關的限制條件。即使只設定其中一個內部欄位也是必填。 |
meta.vct_values | string[] | 否 | 可接受的 VCT URL。錢包對照憑證上的 vct claim。 |
claims | object[] | 否 | 要的 claim,每個用 { "path": [...] }。省略則由錢包決定。 |
trusted_authorities | object[] | 否 | 限定特定 issuer,例如 [{ "type": "did", "values": ["did:iota:0x..."] }]。 |
每個 claim path 是從憑證根開始的 JSON path:["credentialName"]、["degreeName"]。沒列的東西在 SD-JWT selective disclosure 下保持隱藏。
範例 — 大學學位 + 鎖定信任 issuer
Section titled “範例 — 大學學位 + 鎖定信任 issuer”{ "dcql_query": { "credentials": [ { "id": "degree_credential", "format": "dc+sd-jwt", "meta": { "vct_values": ["https://schema-registry.turingspace.co/schemas/UniversityDegreeCredential/v1"] }, "claims": [ { "path": ["credentialName"] }, { "path": ["studentId"] }, { "path": ["degreeName"] } ], "trusted_authorities": [ { "type": "did", "values": ["did:iota:0xabc123def456"] } ] } ] }}Holder 會在同意畫面看到這三個 claim;其他全部隱藏。
- 把
verifier_private_key當設定。 它是簽章金鑰 — 放 secret manager、需要時才取,不要進跟原始碼一起 commit 的.env。 - 無限 poll。 一定要尊重 Step 1 回的
expires_at。Session 過期(status: expired)就停止 poll、回報失敗給 user。 - 要太多 claim。 錢包同意畫面會列出所有 claim path — 要 10 個 optional 欄位會訓練 user 不讀就同意。要最小集合。
- 忘記
vct_values。 沒這欄錢包可能配出無關類型的憑證。 verifier_client_id用非 DID URL,又沒提供 JWKS。 錢包用client_id找你的簽章公鑰。用 non-DID URL 的話要在標準位置暴露 JWKS,並在client_metadata.jwks裡引用。
- Holder · 管理錢包與憑證 — 保管模式下 Step 2 的 request URI 怎麼變成 presentation。
- Issuer · 發行憑證 — 生命週期的另一半。
- 信任與 Schema 治理 — 註冊 verifier、決定接受哪些憑證。
- 身份驗證 — API key、JWT、DPoP 細節。