Skip to content

Commit df01fea

Browse files
Update the Verifying zCaps section.
Signed-off-by: Dmitri Zagidulin <dzagidulin@gmail.com>
1 parent 23b8899 commit df01fea

1 file changed

Lines changed: 270 additions & 0 deletions

File tree

README.md

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ Report issues to this guide's repo: https://github.com/interop-alliance/zcap-dev
1919
- [Revoking zCaps](#revoking-zcaps)
2020
* [Using zCaps with HTTP Requests](#using-zcaps-with-http-requests)
2121
* [Verifying zCaps on the Resource Server](#verifying-zcaps-on-the-resource-server)
22+
- [Verification Algorithm Overview](#verification-algorithm-overview)
23+
- [Where Does the Root zCap Come From?](#where-does-the-root-zcap-come-from)
24+
- [JS Example - Verifying an Incoming Request](#js-example---verifying-an-incoming-request)
25+
- [What the Verification Library Does *Not* Check](#what-the-verification-library-does-not-check)
26+
- [Verifying the `Digest` Header](#verifying-the-digest-header)
2227
* [Performance Considerations](#performance-considerations)
2328
- [Caching zCaps by `id` for Verification](#caching-zcaps-by-id-for-verification)
2429
* [Case Studies](#case-studies)
@@ -581,6 +586,271 @@ The `signature` parameter is base64 encoded (RFC 4648, standard alphabet, PADDED
581586

582587
## Verifying zCaps on the Resource Server
583588

589+
The [resource server](#resource-server-rs) is ultimately responsible for
590+
verifying and enforcing zCaps. As with making requests, developers are
591+
encouraged to use an existing library rather than implementing verification
592+
from scratch -- in Javascript, that's the `@interop/http-signature-zcap-verify`
593+
package (a fork of `@digitalbazaar/http-signature-zcap-verify`).
594+
595+
Verifying an incoming request involves two separate cryptographic checks:
596+
597+
1. **The HTTP signature** (from the `Authorization` header) -- proves that the
598+
requester controls the key it claims, and that the request was not tampered
599+
with in transit (this is the proof-of-possession part).
600+
2. **The capability chain** (from the `Capability-Invocation` header) -- proves
601+
that the key used to sign the request was actually granted authority over
602+
this target, by an unbroken chain of [delegations](#delegation) leading back
603+
to the expected [root zcap](#root-zcap).
604+
605+
### Verification Algorithm Overview
606+
607+
Note: as with the request construction section, this is mostly for the benefit
608+
of zCap library implementers. The whole algorithm below is performed by a
609+
single call to `verifyCapabilityInvocation()` (see the code example in the next
610+
section).
611+
612+
1. Parse the `Authorization` header (see [Current vs Future
613+
Deployments](#current-vs-future-deployments)) and check that all the
614+
expected pseudo-headers and headers were covered by the signature:
615+
`['(key-id)', '(created)', '(expires)', '(request-target)', 'host',
616+
'capability-invocation']`, plus `'content-type'` and `'digest'` if the
617+
request has a body. Check the signature's `created` and `expires`
618+
timestamps against the current time (allowing for some clock skew,
619+
typically 300 seconds).
620+
2. Check that the `Host` header matches the server's own expected host.
621+
3. Dereference the signature's `keyId` (for example, resolve the `did:key` to
622+
get the public key), reconstruct the signing string, and verify the
623+
signature.
624+
4. Parse the `Capability-Invocation` header (the scheme must be `zcap`):
625+
* If it contains an `id` parameter, the invoked capability is a [root
626+
zcap](#root-zcap), referenced by its `urn:zcap:root:...` id.
627+
* If it contains a `capability` parameter, base64url-decode and gunzip it
628+
to get the full delegated zcap. A zcap passed by value _must_ have a
629+
`parentCapability` (root zcaps may only be invoked by `id`).
630+
5. Validate the capability itself:
631+
* Dereference the [capability chain](#capability-chain-proof-chain) back to
632+
the root, and check that the root matches the *expected* root capability
633+
id for this URL (which the server computes itself -- see the next
634+
section).
635+
* Verify the [data integrity proof](#data-integrity-proof) on each
636+
delegation in the chain.
637+
* Check that each zcap in the chain is not [expired](#expiration), that
638+
each step only [attenuates](#attenuation) authority, and that the chain
639+
does not exceed the maximum allowed length.
640+
* Check that the invoked action is in the zcap's [allowed
641+
actions](#action-allowed-action), matches the action the endpoint
642+
expects, and that the request URL matches the zcap's [invocation
643+
target](#target-invocation-target).
644+
* Check that the key that signed the request belongs to the zcap's
645+
[controller](#controller).
646+
6. If all of the above passes, the request is authorized. The verification
647+
result includes the invoked `capability`, the `capabilityAction`, and the
648+
`controller` (the invoker's DID), which the server can use for any further
649+
application-level checks, logging, or auditing.
650+
651+
### Where Does the Root zCap Come From?
652+
653+
Recall from [Creating a Root zCap](#creating-a-root-zcap) that root zcap ids
654+
are deterministic: `urn:zcap:root:${encodeURIComponent(url)}`. This means the
655+
resource server never needs to _store_ root zcaps. Instead, it synthesizes
656+
them on demand during verification: given an expected target URL, the server
657+
constructs the root capability object itself, filling in the `controller`
658+
with the DID of the resource's owner (which the server knows from its own
659+
database -- for example, the controller of a storage space, or an admin DID
660+
from a config file).
661+
662+
This is the crucial trust step: by synthesizing the root zcap, **the server
663+
itself decides who sits at the root of the delegation chain** for each
664+
resource. Everything else (delegations) is verified cryptographically from
665+
there.
666+
667+
In the `@interop` / `@digitalbazaar` library stack, this synthesis is done by
668+
giving the verifier a document loader that handles the `urn:zcap:root:` URN
669+
prefix (see the code example below).
670+
671+
### JS Example - Verifying an Incoming Request
672+
673+
Adapted from a working server implementation
674+
([was-teaching-server](https://github.com/interop-alliance/was-teaching-server)):
675+
676+
```js
677+
import { securityLoader } from '@interop/security-document-loader'
678+
import { verifyCapabilityInvocation } from '@interop/http-signature-zcap-verify'
679+
import { Ed25519VerificationKey } from '@interop/ed25519-verification-key'
680+
import { Ed25519Signature2020 } from '@interop/ed25519-signature'
681+
import * as didKey from '@interop/did-method-key'
682+
683+
// Set up a did:key resolver, used to dereference the invocation's signing key
684+
const didKeyDriver = didKey.driver()
685+
didKeyDriver.use({
686+
multibaseMultikeyHeader: 'z6Mk',
687+
fromMultibase: Ed25519VerificationKey.from
688+
})
689+
690+
/**
691+
* Verifies the capability invocation on an incoming HTTP request.
692+
*
693+
* @param options {object}
694+
* @param options.url {string} full request URL
695+
* @param options.method {string} HTTP method of the request
696+
* @param options.headers {object} request headers (including `authorization`,
697+
* `capability-invocation`, and `digest`)
698+
* @param options.allowedTarget {string} the expected invocationTarget URL
699+
* @param options.allowedAction {string} the expected action, e.g. 'GET'
700+
* @param options.resourceController {string} DID of the resource owner; this
701+
* becomes the controller of the synthesized root zcap
702+
* @returns {Promise<object>} `{ verified, capability, capabilityAction,
703+
* controller, dereferencedChain, ... }`
704+
*/
705+
export async function verifyZcap({
706+
url, method, headers, allowedTarget, allowedAction, resourceController
707+
}) {
708+
// The server computes the expected root capability id itself
709+
const expectedRootCapability = `urn:zcap:root:${encodeURIComponent(allowedTarget)}`
710+
711+
// Document loader that synthesizes root zcaps on demand
712+
const loader = securityLoader()
713+
loader.setProtocolHandler({
714+
protocol: 'urn',
715+
handler: {
716+
get: async ({ id, url }) => {
717+
const resolvedUrl = url || id
718+
const rootZcapTarget = decodeURIComponent(
719+
resolvedUrl.split('urn:zcap:root:')[1]
720+
)
721+
return {
722+
'@context': 'https://w3id.org/zcap/v1',
723+
id: resolvedUrl,
724+
invocationTarget: rootZcapTarget,
725+
// This is the trust anchor: the server decides who the root
726+
// controller is for this resource
727+
controller: resourceController
728+
}
729+
}
730+
}
731+
})
732+
const documentLoader = loader.build()
733+
734+
return verifyCapabilityInvocation({
735+
url, method, headers,
736+
expectedHost: new URL(allowedTarget).host,
737+
expectedAction: allowedAction,
738+
expectedRootCapability,
739+
expectedTarget: allowedTarget,
740+
documentLoader,
741+
// Resolves the invocation signature's keyId to a verifier
742+
async getVerifier({ keyId }) {
743+
const verificationMethod = await didKeyDriver.get({ url: keyId })
744+
const key = await Ed25519VerificationKey.from(verificationMethod)
745+
return { verifier: key.verifier(), verificationMethod }
746+
},
747+
suite: new Ed25519Signature2020()
748+
})
749+
}
750+
```
751+
752+
The result object looks like:
753+
754+
```js
755+
{
756+
verified: true,
757+
capability, // the invoked zcap
758+
capabilityAction, // the action that was invoked, e.g. 'GET'
759+
controller, // DID of the invoker
760+
invoker: controller,
761+
dereferencedChain, // the full zcap chain, root first
762+
verificationMethod
763+
}
764+
```
765+
766+
A typical HTTP handler then becomes:
767+
768+
```js
769+
const result = await verifyZcap({
770+
url, method, headers,
771+
allowedTarget: 'https://example.com/documents/123',
772+
allowedAction: method, // e.g. require the action to match the HTTP verb
773+
resourceController: documentOwnerDid
774+
})
775+
if (!result.verified) {
776+
return response.status(401).end()
777+
}
778+
// ... proceed with the request
779+
```
780+
781+
### What the Verification Library Does *Not* Check
782+
783+
A few things remain the resource server's responsibility, beyond the
784+
`verifyCapabilityInvocation()` call:
785+
786+
* **The request body hash.** The HTTP signature covers the `Digest` _header_,
787+
but the library never sees the request body -- the server must independently
788+
compute the hash of the received body and compare it against the `Digest`
789+
header value. Skipping this check allows an attacker to replay a signed
790+
request with a different payload. See [Verifying the `Digest`
791+
Header](#verifying-the-digest-header) below.
792+
* **[Revocation](#revocation).** The library accepts an optional
793+
`inspectCapabilityChain` callback; use it to check each zcap id in the chain
794+
against your revocation list. (See also [Revoking zCaps](#revoking-zcaps).)
795+
* **Application-level authorization.** A verified invocation proves the
796+
requester holds a valid, unexpired zcap chain for this target and action --
797+
any business rules beyond that (rate limits, quotas, per-tenant logic) are
798+
up to the server, using the returned `controller` and `capability`.
799+
800+
Useful optional knobs on `verifyCapabilityInvocation()`:
801+
802+
* `allowTargetAttenuation` - allow delegated zcaps to narrow the invocation
803+
target to a sub-path of the parent's URL (hierarchical RESTful attenuation;
804+
see [attenuation](#attenuation)). Most storage use cases (EDV, WAS) need
805+
this enabled.
806+
* `maxChainLength` - maximum number of delegations allowed in a chain.
807+
* `maxDelegationTtl` - maximum time-to-live of any delegated zcap in the
808+
chain (measured as the difference between the delegation proof's `created`
809+
and the zcap's `expires`).
810+
* `maxClockSkew` - seconds of clock skew tolerated when checking signature
811+
and capability expiration times (default: 300).
812+
813+
### Verifying the `Digest` Header
814+
815+
This is the server-side counterpart of [Constructing the `Digest`
816+
Header](#constructing-the-digest-header), and only applies to requests that
817+
have a body (PUT, POST, etc).
818+
819+
The procedure: hash the request body with SHA-256, encode the hash the same
820+
way the client did (the encoding is indicated by the header value's prefix --
821+
`SHA-256=` for base64, `mh=` for Multihash), and compare against the received
822+
`Digest` header value.
823+
824+
Two things to watch out for:
825+
826+
* Hash the **raw received body bytes**, not a re-serialized version of the
827+
parsed body. A `JSON.parse()` / `JSON.stringify()` round trip is not
828+
guaranteed to reproduce the exact bytes the client hashed.
829+
* If the endpoint accepts a body, treat a _missing_ `Digest` header as an
830+
error, too -- otherwise an attacker can strip the header along with swapping
831+
the body.
832+
833+
Javascript example, using the `@interop/http-digest-header` package (which
834+
handles both encodings automatically):
835+
836+
```js
837+
import { verifyHeaderValue } from '@interop/http-digest-header'
838+
839+
// In your HTTP handler, alongside the zcap verification:
840+
if (requestHasBody) {
841+
const digestHeader = headers.digest
842+
if (!digestHeader) {
843+
return response.status(400).end()
844+
}
845+
const { verified } = await verifyHeaderValue({
846+
data: rawBody, headerValue: digestHeader
847+
})
848+
if (!verified) {
849+
return response.status(400).end()
850+
}
851+
}
852+
```
853+
584854
## Performance Considerations
585855

586856
### Caching zCaps by `id` for Verification

0 commit comments

Comments
 (0)