@@ -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