跳到內容

Verifier · 驗證憑證

這頁是給 Verifier 角色:要求 holder 證明某件事的服務。從你的後端視角,驗證只有兩支 API 呼叫 — 建立 authorization request、然後讀結果。中間的事(取得簽妥的 request object、提示 user、組 VP token、提交)都在錢包端。


Rendering diagram...

你要實作的:

  1. 建立 request。 POST 你的 DCQL query 與 verifier 身分到 Verifier Service。
  2. 交付 request URI。
  3. Poll 結果。 監看 GET /v1/verifier/result/{session_id} 直到 session 變 completed

VP token、SD-JWT 簽章、key-binding JWT 你都不用自己組或驗。Verifier Service 全做完,結果端點上會回 status(整體通過/失敗)與 check_results[](逐項檢查)。


Terminal window
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_queryobject你要哪些憑證、哪些 claim。詳見 DCQL Query Structure
verifier_private_keystringBase64url 編碼的原生 Ed25519(32 bytes)。用來簽 JAR(request object)讓錢包驗「是誰在問」。
verifier_client_idstringVerifier 的 DID(或 URL)。錢包同意畫面上會顯示。
response_modestring"direct_post"(預設,明文 server-to-server VP 交付)或 "direct_post.jwt"(用 ECDH-ES 加密交付;TCS 會自動以 ephemeral 公鑰填入 client_metadata.jwks)。
expires_innumberSession TTL 秒數(預設 300)。
client_metadataobjectclient_namelogo_uripolicy_uritos_urijwks — 顯示在錢包同意畫面。
redirect_uristring錢包提交後把 user redirect 去哪。Headless 流程可省略。
狀態原因
400Body 格式錯(缺欄位、dcql_query.credentials 為空等)
401X-API-Key 缺或無效

把 request 送到 holder 面前的方式取決於模式。

代表 user 把 authorization_request 傳給 Holder Service。Holder Service 會 fetch 簽妥的 request、提示 user、組 VP token、提交。詳見 Holder · 管理錢包與憑證 § 4

Terminal window
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。


Terminal window
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_resultsstatus: "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 建立時選擇模式。

驗證 pipeline 目前對外呈現的 check 名稱與 reason_code 封閉集合。可作為 triage 失敗或對應使用者文案的依據。

checkreason_code(成功)reason_code(失敗)含義
oid4vp_complianceoid4vp_request_validoid4vp_non_compliantpresentation_invalidOID4VP 請求 / 回應結構符合規範
credential_formatcredential_format_validcredential_format_invalid出示之 payload 可解析為 dc+sd-jwt
signaturesignature_verifiedsignature_verification_failedsignature_payload_mismatchissuer_key_not_resolvable發行方簽章對解析出之公鑰驗證通過
issuer_trustnot_implementedTrust Registry 名單檢查(pipeline 尚未串接,目前對外呈現 not_implemented
schema_governancenot_implementedSchema 治理檢查(roadmap,呈現 not_implemented
schema_versionnot_implementedSchema 版本政策檢查(roadmap,呈現 not_implemented
issuer_governancenot_implemented發行方治理檢查(roadmap,呈現 not_implemented
(任一檢查)dcql_not_satisfiedskippedDCQL 未匹配(沒有任何憑證滿足查詢);或在 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意義
pendingRequest 已建立,等錢包來抓 request object
in_progress錢包已抓 request object,使用者尚未提交
completedVP token 驗證通過;presentation_metadata 已填,且 check_results 每項都是 success
failedcheck_results 至少一項是 fail(簽章錯、nonce 不對、憑證過期等)
expiredSession 在錢包回應前 TTL 到期;error 已填

實務 poll loop(每 2 秒):

Terminal window
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 2
done

push 模式 webhook(讓 verifier 後端不必 polling)為路線圖項目;在它推出之前,polling result 端點為唯一支援的整合路徑。如果 push 對你的部署是硬性需求,請聯繫我們


DCQL(Digital Credentials Query Language)描述你向 holder 要哪些憑證、哪些 claim。每個 authorization request 都需要 dcql_query

{
"dcql_query": {
"credentials": [ /* 一個或多個 credential descriptor */ ]
}
}
欄位型別必填說明
idstring該 credential 在這份 query 內的識別符 — Holder Service 在 selected_credentials[].query_id 也會引用。
formatstringSD-JWT VC 用 "dc+sd-jwt"
metaobject放憑證格式相關的限制條件。即使只設定其中一個內部欄位也是必填。
meta.vct_valuesstring[]可接受的 VCT URL。錢包對照憑證上的 vct claim。
claimsobject[]要的 claim,每個用 { "path": [...] }。省略則由錢包決定。
trusted_authoritiesobject[]限定特定 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 裡引用。