-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathzcap.mjs
More file actions
140 lines (122 loc) · 4.98 KB
/
Copy pathzcap.mjs
File metadata and controls
140 lines (122 loc) · 4.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import {
securityLoader
} from '@interop/security-document-loader'
import {
verifyCapabilityInvocation
} from '@interop/http-signature-zcap-verify'
import * as didKey from '@interop/did-method-key'
import { Ed25519VerificationKey } from '@interop/ed25519-verification-key'
import { Ed25519Signature2020 } from '@interop/ed25519-signature'
import { DynamoDBClient, ScanCommand } from '@aws-sdk/client-dynamodb'
const didKeyDriver = didKey.driver()
didKeyDriver.use({
multibaseMultikeyHeader: 'z6Mk',
fromMultibase: Ed25519VerificationKey.from
})
const baseDocumentLoader = securityLoader()
const dynamoClient = new DynamoDBClient()
const TABLE_NAME = process.env.TABLE_NAME ?? 'wallet-test'
// The space URL is everything in the request URL up to and including the
// {space_id} segment, matching the spaceURL registered for the account.
function getSpaceUrl(url) {
const match = url.match(/^(.*?\/space\/[^/?#]+)/)
return match?.[1]
}
// Looks up the DID registered for the space in the accounts table. The table
// is keyed by email, so filter on an exact match of the stored space URL.
async function getSpaceControllerDid(spaceUrl) {
const { Items: items = [] } = await dynamoClient.send(new ScanCommand({
TableName: TABLE_NAME,
FilterExpression: 'spaceURL = :spaceUrl',
ExpressionAttributeValues: { ':spaceUrl': { S: spaceUrl } }
}))
// Registered DIDs may carry a key fragment (did:key:z6Mk...#z6Mk...)
return items[0]?.did?.S?.split('#')[0]
}
function rootCapabilityLoader(spaceController) {
const loader = baseDocumentLoader.clone()
loader.setProtocolHandler({
protocol: 'urn',
handler: {
get: async ({ id, url }) => {
const resolvedUrl = url || id
const rootZcapTarget = decodeURIComponent(
resolvedUrl.split('urn:zcap:root:')[1]
)
return {
'@context': 'https://w3id.org/zcap/v1',
id: resolvedUrl,
invocationTarget: rootZcapTarget,
controller: spaceController,
}
}
}
})
return loader.build()
}
async function getVerifier({ keyId }) {
const didDocument = await didKeyDriver.get({ url: keyId })
const key = await Ed25519VerificationKey.from(didDocument)
const verifier = key.verifier()
return {
verifier,
verificationMethod: didDocument
}
}
// API Gateway passes headers through with whatever casing the client sent, so
// anything we read out of them has to be looked up case-insensitively.
function getHeader(headers, name) {
const match = Object.keys(headers).find(
key => key.toLowerCase() === name.toLowerCase()
)
return match === undefined ? undefined : headers[match]
}
export const verifyZcap = async (event) => {
const { headers = {} } = event
// HTTP API authorizer payload v2: the method lives under
// requestContext.http and the path is rawPath. The $default stage serves
// at the root, so rawPath is exactly the path the client signed.
const httpMethod = event.requestContext?.http?.method ?? event.httpMethod
const path = event.rawPath ?? event.requestContext?.path ?? event.path
const host = getHeader(headers, 'Host')
const proto = getHeader(headers, 'X-Forwarded-Proto') ?? 'https'
// The invoked capability's target should match the resource actually being requested.
const url = proto + '://' + host + path
// The root capability for the space is controlled by the DID registered
// for it in the accounts table, so verification rejects invocations
// signed by any other key.
const spaceUrl = getSpaceUrl(url)
if (!spaceUrl) {
throw new Error(`No space URL in request URL: ${url}`)
}
const spaceController = await getSpaceControllerDid(spaceUrl)
if (!spaceController) {
throw new Error(`No account registered for space: ${spaceUrl}`)
}
const result = await verifyCapabilityInvocation({
url,
method: httpMethod,
// The signature is computed over the lowercase header name.
headers: { ...headers, authorization: getHeader(headers, 'Authorization') },
suite: new Ed25519Signature2020(),
getVerifier,
documentLoader: rootCapabilityLoader(spaceController),
expectedHost: host,
expectedAction: httpMethod,
expectedTarget: url,
expectedRootCapability: 'urn:zcap:root:' + encodeURIComponent(url)
})
if (!result.verified) {
console.log("in the verifyZcap function - Verification failed:", JSON.stringify(result, null, 2));
// `result.error` describes why verification failed (bad signature,
// unexpected host, expired capability, unauthorized key, etc.)
console.log(JSON.stringify(result, null, 2));
throw result.error
}
// On success, `result` also includes the invoked `capability`,
// `capabilityAction`, the `controller`/`invoker`, the `verificationMethod`,
// and the `dereferencedChain`.
// console.log('invoked by', result.controller)
// console.log('result', JSON.stringify(result, null, 2));
return result
}