|
| 1 | +# Delegatable Credentials |
| 2 | + |
| 3 | +Delegatable credentials enable a credential issuer (delegator) to delegate their authority to issue specific types of credentials to other entities (delegees). This creates a chain of trust where delegees can issue credentials on behalf of the original issuer, within the bounds of their delegated authority. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +A delegatable credential allows an issuer to: |
| 8 | +- Grant specific issuance authorities to delegates |
| 9 | +- Control which claims delegates can issue |
| 10 | +- Establish delegation chains with multiple levels |
| 11 | +- Enforce policies using Cedar policy language |
| 12 | +- Verify the entire delegation chain during presentation |
| 13 | + |
| 14 | +## Key Concepts |
| 15 | + |
| 16 | +### Delegation Chain |
| 17 | + |
| 18 | +A delegation chain consists of: |
| 19 | +- **Root Credential**: The original delegation issued by the authoritative issuer |
| 20 | +- **Delegated Credentials**: Credentials issued by delegates using their delegated authority |
| 21 | +- **Chain Verification**: Each credential in the chain references its predecessor via `previousCredentialId` and the root via `rootCredentialId` |
| 22 | + |
| 23 | +### May Claim |
| 24 | + |
| 25 | +The `mayClaim` property (IRI: `https://www.dock.io/rdf2020#mayClaim`) specifies which claims a delegate is authorized to issue. For example: |
| 26 | + |
| 27 | +```javascript |
| 28 | +credentialSubject: { |
| 29 | + id: 'did:example:delegate', |
| 30 | + creditScore: 760, |
| 31 | + [MAY_CLAIM_IRI]: ['creditScore'], // Delegate can only issue creditScore claims |
| 32 | +} |
| 33 | +``` |
| 34 | + |
| 35 | +### Cedar Policies |
| 36 | + |
| 37 | +[Cedar](https://docs.cedarpolicy.com/) is a language for defining permissions as policies. Cedar policies provide fine-grained authorization control over delegation chains. They can enforce: |
| 38 | +- Maximum delegation depth |
| 39 | +- Required root issuer |
| 40 | +- Minimum claim values |
| 41 | +- Specific claim values |
| 42 | + |
| 43 | +## API Reference |
| 44 | + |
| 45 | +### Core Functions |
| 46 | + |
| 47 | +#### `issueDelegationCredential(keyDoc, options)` |
| 48 | + |
| 49 | +Issues a root delegation credential that grants authority to a delegate. |
| 50 | + |
| 51 | +**Parameters:** |
| 52 | +- `keyDoc`: Signing key document of the issuer |
| 53 | +- `options`: |
| 54 | + - `id`: Unique credential identifier |
| 55 | + - `issuer`: DID of the issuer |
| 56 | + - `@context`: JSON-LD context including delegation terms |
| 57 | + - `issuanceDate`: ISO timestamp |
| 58 | + - `type`: Array including 'DelegationCredential' |
| 59 | + - `credentialSubject`: Object containing: |
| 60 | + - `id`: DID of the delegate |
| 61 | + - `[MAY_CLAIM_IRI]`: Array of authorized claim names |
| 62 | + - Additional claims |
| 63 | + - `previousCredentialId`: Should be `null` for root credentials |
| 64 | + - `rootCredentialId`: Should be `null` for root credentials |
| 65 | + |
| 66 | +**Returns:** Signed delegation credential |
| 67 | + |
| 68 | +**Example:** |
| 69 | +```javascript |
| 70 | +const rootCredential = await issueDelegationCredential(issuerKey, { |
| 71 | + id: 'urn:uuid:12345678', |
| 72 | + issuer: issuerDid, |
| 73 | + '@context': CREDIT_SCORE_DELEGATION_CONTEXT, |
| 74 | + issuanceDate: new Date().toISOString(), |
| 75 | + type: [ |
| 76 | + 'VerifiableCredential', |
| 77 | + 'CreditScoreCredential', |
| 78 | + 'DelegationCredential', |
| 79 | + ], |
| 80 | + credentialSubject: { |
| 81 | + id: holderDid, |
| 82 | + creditScore: 760, |
| 83 | + [MAY_CLAIM_IRI]: ['creditScore'], |
| 84 | + }, |
| 85 | + previousCredentialId: null, |
| 86 | + rootCredentialId: null, |
| 87 | +}); |
| 88 | +``` |
| 89 | + |
| 90 | +#### `issueDelegatedCredential(keyDoc, options)` |
| 91 | + |
| 92 | +Issues a credential using delegated authority. |
| 93 | + |
| 94 | +**Parameters:** |
| 95 | +- `keyDoc`: Signing key document of the delegate |
| 96 | +- `options`: |
| 97 | + - `id`: Unique credential identifier |
| 98 | + - `issuer`: DID of the delegate issuer |
| 99 | + - `@context`: JSON-LD context |
| 100 | + - `issuanceDate`: ISO timestamp |
| 101 | + - `type`: Array of credential types |
| 102 | + - `credentialSubject`: Object containing: |
| 103 | + - `id`: DID of the credential subject |
| 104 | + - Claims being issued |
| 105 | + - `[MAY_CLAIM_IRI]` (optional): For further delegation |
| 106 | + - `previousCredentialId`: ID of the delegating credential |
| 107 | + - `rootCredentialId`: ID of the root credential |
| 108 | + |
| 109 | +**Returns:** Signed delegated credential |
| 110 | + |
| 111 | +**Example:** |
| 112 | +```javascript |
| 113 | +const delegatedCredential = await issueDelegatedCredential(holderKey, { |
| 114 | + id: 'urn:uuid:87654321', |
| 115 | + issuer: holderDid, |
| 116 | + '@context': CREDIT_SCORE_CREDENTIAL_CONTEXT, |
| 117 | + issuanceDate: new Date().toISOString(), |
| 118 | + type: [ |
| 119 | + 'VerifiableCredential', |
| 120 | + 'CreditScoreCredential', |
| 121 | + 'DelegationCredential', |
| 122 | + ], |
| 123 | + credentialSubject: { |
| 124 | + id: agentDid, |
| 125 | + creditScore: 400, |
| 126 | + [MAY_CLAIM_IRI]: ['creditScore'], // For further delegation |
| 127 | + }, |
| 128 | + previousCredentialId: rootCredential.id, |
| 129 | + rootCredentialId: rootCredential.id, |
| 130 | +}); |
| 131 | +``` |
| 132 | + |
| 133 | +#### `createSignedPresentation(keyDoc, options)` |
| 134 | + |
| 135 | +Creates a signed verifiable presentation containing delegated credentials. |
| 136 | + |
| 137 | +**Parameters:** |
| 138 | +- `keyDoc`: Signing key document of the holder |
| 139 | +- `options`: |
| 140 | + - `credentials`: Array of credentials (must include full delegation chain) |
| 141 | + - `holderDid`: DID of the presentation holder |
| 142 | + - `challenge`: Challenge string for replay protection |
| 143 | + - `domain`: Domain string for binding |
| 144 | + |
| 145 | +**Returns:** Signed verifiable presentation |
| 146 | + |
| 147 | +**Example:** |
| 148 | +```javascript |
| 149 | +const presentation = await createSignedPresentation(agentKey, { |
| 150 | + credentials: [rootCredential, credDelegatedToAgent], |
| 151 | + holderDid: agentDid, |
| 152 | + challenge: 'test-challenge-123', |
| 153 | + domain: 'test.example.com', |
| 154 | +}); |
| 155 | +``` |
| 156 | + |
| 157 | +#### `verifyDelegatablePresentation(presentation, options)` |
| 158 | + |
| 159 | +Verifies a presentation containing delegated credentials. |
| 160 | + |
| 161 | +**Parameters:** |
| 162 | +- `presentation`: The verifiable presentation to verify |
| 163 | +- `options`: |
| 164 | + - `challenge`: Expected challenge string |
| 165 | + - `domain`: Expected domain string |
| 166 | + - `policies` (optional): Cedar policies object |
| 167 | + |
| 168 | +**Returns:** Verification result object containing: |
| 169 | +- `verified`: Boolean indicating overall verification status |
| 170 | +- `delegationResult`: Delegation verification result with: |
| 171 | + - `decision`: 'allow' or 'deny' |
| 172 | + - `summaries`: Array of delegation chain summaries |
| 173 | + - `failures`: Array of failure objects (if denied) |
| 174 | +- `credentialResults`: Array of individual credential verification results |
| 175 | + |
| 176 | +**Example:** |
| 177 | +```javascript |
| 178 | +const result = await verifyDelegatablePresentation(presentation, { |
| 179 | + challenge: CHALLENGE, |
| 180 | + domain: DOMAIN, |
| 181 | + policies, |
| 182 | +}); |
| 183 | + |
| 184 | +console.log('Verified:', result.verified); |
| 185 | +console.log('Decision:', result.delegationResult?.decision); |
| 186 | +``` |
| 187 | +
|
| 188 | +#### `createCedarPolicy(options)` |
| 189 | +
|
| 190 | +Creates Cedar policy rules for delegation authorization. |
| 191 | +
|
| 192 | +**Parameters:** |
| 193 | +- `options`: |
| 194 | + - `maxDepth`: Maximum allowed delegation chain depth |
| 195 | + - `rootIssuer`: Required DID of the root issuer |
| 196 | + - `requiredClaims`: Object mapping claim names to required values: |
| 197 | + - Number values: minimum required value (e.g., `creditScore: 500`) |
| 198 | + - String values: exact required value (e.g., `role: "admin"`) |
| 199 | + - Any value: description of the claim requirement |
| 200 | +
|
| 201 | +**Returns:** Policy object with `staticPolicies` string |
| 202 | +
|
| 203 | +**Example:** |
| 204 | +```javascript |
| 205 | +const policies = createCedarPolicy({ |
| 206 | + maxDepth: 2, |
| 207 | + rootIssuer: 'did:example:root-issuer', |
| 208 | + requiredClaims: { |
| 209 | + creditScore: 500, // Minimum value |
| 210 | + role: 'admin', // Exact match |
| 211 | + body: 'Issuer of Credits', // Description |
| 212 | + }, |
| 213 | +}); |
| 214 | +``` |
| 215 | +
|
| 216 | +### Context Constants |
| 217 | +
|
| 218 | +#### `MAY_CLAIM_IRI` |
| 219 | +The IRI used for the `mayClaim` property in credential subjects. |
| 220 | +```javascript |
| 221 | +const MAY_CLAIM_IRI = 'https://www.dock.io/rdf2020#mayClaim'; |
| 222 | +``` |
| 223 | +
|
| 224 | +#### `DELEGATION_CONTEXT_TERMS` |
| 225 | +Base JSON-LD terms for delegation credentials. |
| 226 | +
|
| 227 | +#### `W3C_CREDENTIALS_V1` |
| 228 | +Standard W3C Verifiable Credentials context URL. |
| 229 | +
|
| 230 | +
|
| 231 | +## Integration with Presentation Exchange (PEX) |
| 232 | +
|
| 233 | +Delegatable credentials can be used with Presentation Exchange to request specific delegation chains: |
| 234 | +
|
| 235 | +```javascript |
| 236 | +const presentationDefinition = { |
| 237 | + id: 'delegation_test', |
| 238 | + input_descriptors: [ |
| 239 | + { |
| 240 | + id: 'root-credential', |
| 241 | + name: 'Root Credential', |
| 242 | + purpose: 'Must be the root credential issued by the required issuer', |
| 243 | + group: ['1'], |
| 244 | + constraints: { |
| 245 | + fields: [ |
| 246 | + { |
| 247 | + path: ['$.type'], |
| 248 | + filter: { |
| 249 | + type: 'array', |
| 250 | + contains: { const: 'CreditScoreCredential' }, |
| 251 | + }, |
| 252 | + }, |
| 253 | + { |
| 254 | + path: ['$.issuer', '$.iss'], |
| 255 | + filter: { |
| 256 | + type: 'string', |
| 257 | + const: issuerDid, |
| 258 | + }, |
| 259 | + }, |
| 260 | + ], |
| 261 | + }, |
| 262 | + }, |
| 263 | + { |
| 264 | + id: 'other-credentials', |
| 265 | + name: 'Additional Credentials', |
| 266 | + purpose: 'Any number of additional credentials of the specified type', |
| 267 | + group: ['2'], |
| 268 | + constraints: { |
| 269 | + fields: [ |
| 270 | + { |
| 271 | + path: ['$.type'], |
| 272 | + filter: { |
| 273 | + type: 'array', |
| 274 | + contains: { const: 'CreditScoreCredential' }, |
| 275 | + }, |
| 276 | + }, |
| 277 | + ], |
| 278 | + }, |
| 279 | + }, |
| 280 | + ], |
| 281 | + submission_requirements: [ |
| 282 | + { |
| 283 | + from: '1', |
| 284 | + name: 'Root Credential', |
| 285 | + rule: 'pick', |
| 286 | + count: 1, |
| 287 | + }, |
| 288 | + { |
| 289 | + from: '2', |
| 290 | + name: 'Additional Credentials', |
| 291 | + rule: 'pick', |
| 292 | + min: 0, |
| 293 | + }, |
| 294 | + ], |
| 295 | +}; |
| 296 | + |
| 297 | +// Filter credentials that match the definition |
| 298 | +const filterResult = await credentialService.filterCredentials({ |
| 299 | + credentials: [rootCredential, credDelegatedToAgent], |
| 300 | + presentationDefinition, |
| 301 | + holderDid: agentDid, |
| 302 | +}); |
| 303 | + |
| 304 | +// Create presentation |
| 305 | +const presentation = await createSignedPresentation(agentKey, { |
| 306 | + credentials: [rootCredential, credDelegatedToAgent], |
| 307 | + holderDid: agentDid, |
| 308 | + challenge: CHALLENGE, |
| 309 | + domain: DOMAIN, |
| 310 | +}); |
| 311 | + |
| 312 | +// Evaluate presentation against definition |
| 313 | +const validationResults = await pexService.evaluatePresentation({ |
| 314 | + presentation, |
| 315 | + presentationDefinition, |
| 316 | +}); |
| 317 | +``` |
| 318 | +
|
| 319 | +## Best Practices |
| 320 | +
|
| 321 | +1. **Always Include Full Chain**: When creating presentations with delegated credentials, include the complete delegation chain from root to leaf. |
| 322 | +
|
| 323 | +2. **Use Specific mayClaim Values**: Be explicit about which claims a delegate can issue. Avoid overly broad delegations. |
| 324 | +
|
| 325 | +3. **Implement Cedar Policies**: Use Cedar policies to enforce business rules like maximum delegation depth and claim requirements. |
| 326 | +
|
| 327 | +4. **Handle Verification Failures**: Check both the overall `verified` status and the `delegationResult.decision` to understand why verification failed. |
| 328 | +
|
| 329 | +
|
| 330 | +## Testing |
| 331 | +
|
| 332 | +See `integration-tests/delegatable-credentials.test.ts` for comprehensive test examples including: |
| 333 | +
|
| 334 | +- Issuing valid delegation credentials |
| 335 | +- Issuing delegated credentials with proper chain references |
| 336 | +- Creating and verifying presentations with delegation chains |
| 337 | +- Enforcing authorization with Cedar policies |
| 338 | +- Handling unauthorized delegations |
| 339 | +- Multi-level delegation chains |
| 340 | +- Integration with Presentation Exchange |
| 341 | +
|
| 342 | +Run the tests with: |
| 343 | +```bash |
| 344 | +npm test integration-tests/delegatable-credentials.test.ts |
| 345 | +``` |
| 346 | +
|
| 347 | +## References |
| 348 | +
|
| 349 | +- [W3C Verifiable Credentials](https://www.w3.org/TR/vc-data-model/) |
| 350 | +- [Cedar Policy Language](https://www.cedarpolicy.com/) |
| 351 | +- [Presentation Exchange](https://identity.foundation/presentation-exchange/) |
0 commit comments