Credential-Gated Delegation
Credential-gated delegation is a delegation the policy engine issues only when the requester presents a verifiable credential that the engine independently verifies. In the Policy's when rule this is an evidence{} condition; at request time the holder attaches a credential presentation; the engine's VC evidence verifier (crates/policy-evidence-vc) checks it and adds the requirement to the satisfied set, after which when can pass and the grant is minted. It is the literal pipeline behind "credentials feed the policy engine".
Role
Policy as central primitive lets an owner condition authority on facts. Credential-gating is the most powerful such fact: not "is this a specific key" but "does this holder possess a credential — an email-domain, a membership — issued by a trusted witness." Crucially, the engine never trusts the holder's claim of satisfaction: evaluate_expression only counts evidence IDs the engine itself verified (overview), so a credential condition is a real cryptographic gate, not a flag. This is the join between Layer 1 permissioning and the OpenCredentials Layer 2 credential app.
Mechanics
An evidence{} requirement names a verifier and opaque requirements. The v0 verifier is VcEvidenceVerifier (crates/policy-evidence-vc/src/lib.rs), keyed to verifier profile w3c.vc/credential/v1 and credential type opencredentials.email/v1 (the email-domain SD-JWT from OpenCredentials). Its verify(requirement, presentation, context) enforces:
- Verifier match —
requirement.verifiermust bew3c.vc/credential/v1; elseevidence-verifier-unsupported. - Requirements parse —
requirementsdeserializes to{ type: "opencredentials.email/v1", emailDomains: [...] }; domains are NFC/ASCII-normalized (normalize_email_domain), rejecting non-ASCII (evidence-domain-invalid) and empty lists (evidence-domain-missing). - Accepted issuers — the requirement's
authority.accepted_issuersmust be non-empty, and each is looked up in the verifier's issuer-key registry; an issuer with no registered key isevidence-issuer-untrusted. - Credential verification — for each accepted issuer,
EmailCredentialVerifier(from the upstreamopencredentials-verifycrate, pinnedgit rev c9fa2fe) checks the SD-JWT signature, that its subject equalscontext.eligible_subject_did, that the disclosed email domain matches an allowed domain (selective disclosure — the full address is not required, and presenting the full email without the domain disclosure is rejected), and expiry. - Freshness — if the requirement sets
freshness.max_status_age_seconds, a credential older than that is rejectedevidence-freshness-expired.
On success it returns a Satisfaction { evidence_ids: [requirement_id], valid_until, provenance }. The runtime collects every satisfaction, re-runs evaluate_expression with the satisfied IDs, and — critically — caps the grant's TTL at min(max_ttl, presentation.expires_at, every credential's valid_until) (grant_expires_at), so the delegation cannot outlive the credential that authorized it.
Shape
// in Policy.when:
evidence { EvidenceRequirement {
requirement_id: "email-domain",
verifier: "w3c.vc/credential/v1",
requirements: { type: "opencredentials.email/v1", emailDomains: ["tinycloud.xyz"] },
authority: { accepted_issuers: ["did:web:issuer.tinycloud.xyz"] },
freshness?: { max_status_age_seconds }
}}
// in GrantPresentation.evidence:
PresentedEvidence { requirement_id: "email-domain", presentation: { sdJwt: "<SD-JWT>" } }
Relationships
Implements the evidence{} arm of the when grammar; verifies SD-JWT credentials issued by the witness service as part of OpenCredentials; the satisfied requirement lets resolve mint a portable-delegation; runs alongside holder enrollment; the end-to-end framing is feeds-policy-engine; lives in Layer 1 consuming L2 credentials.
Example
Policy when = evidence{"email-domain"} requiring opencredentials.email/v1 from did:web:issuer.tinycloud.xyz, domain tinycloud.xyz. A holder presents { sdJwt: "<email-domain SD-JWT for sam@tinycloud.xyz>" }. The verifier confirms the witness signature, that the credential's subject is the eligible subject, and that the disclosed domain is tinycloud.xyz — without the SD-JWT ever revealing sam@. evaluate_expression now sees {"email-domain"} satisfied, when passes, and a tinycloud.sql/read grant is issued, expiring at the credential's expiry or one hour, whichever is sooner. A wrong domain, wrong issuer, subject mismatch, expired or stale credential each fail with a distinct evidence-* error (the verifier's own tests cover all five).
Status & drift
in-progress. The verifier, the evidence{} grammar, and TTL-capping are frozen v0 + shipped + tested in policy-evidence-vc and policy-runtime. The only credential profile wired today is email-domain (opencredentials.email/v1); additional OpenCredentials credential types are design-intent. As with the whole engine, the grant it emits is honored by the node only once node consumption lands — currently the node does not consume the engine (see overview). See contradictions.
Sources
policy-engine:crates/policy-evidence-vc/src/lib.rs(VcEvidenceVerifier::verify, freshness, selective-disclosure tests),src/evaluator.rs:76(satisfied-evidence gating),crates/policy-runtime/src/lib.rs(verify_evidence+grant_expires_atTTL cap),crates/policy-evidence-vc/Cargo.toml(opencredentials-verifygit rev c9fa2fe)