Skip to content

Commit 21491b4

Browse files
fix: handling content-serve delegations (#189)
Added Validator Proof to be able to validate attestations issued by the upload service when clients attempt to store `content/serve` delegations. Related to storacha/project-tracking#592 --------- Co-authored-by: Hannah Howard <[email protected]>
1 parent fd7b0db commit 21491b4

15 files changed

+333
-97
lines changed

package-lock.json

Lines changed: 7 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,13 @@
3636
"test:unit:only": "npm run build:debug && mocha --experimental-vm-modules"
3737
},
3838
"dependencies": {
39+
"@ipld/dag-json": "^10.2.5",
3940
"@ipld/dag-pb": "^4.1.5",
4041
"@microlabs/otel-cf-workers": "^1.0.0-rc.48",
4142
"@opentelemetry/api": "^1.9.0",
4243
"@opentelemetry/sdk-trace-base": "^1.27.0",
4344
"@storacha/capabilities": "^1.10.0",
44-
"@storacha/indexing-service-client": "^2.6.6",
45+
"@storacha/indexing-service-client": "^2.6.9",
4546
"@types/node": "^24.10.1",
4647
"@ucanto/client": "^9.0.2",
4748
"@ucanto/core": "^10.4.5",
@@ -50,14 +51,15 @@
5051
"@ucanto/server": "^11.0.3",
5152
"@ucanto/transport": "^9.2.1",
5253
"@ucanto/validator": "^10.0.1",
53-
"@web3-storage/blob-fetcher": "^4.2.5",
54+
"@web3-storage/blob-fetcher": "^4.2.6",
5455
"@web3-storage/gateway-lib": "^5.2.2",
5556
"dagula": "^8.0.0",
5657
"http-range-parse": "^1.0.0",
5758
"lnmap": "^2.0.0",
5859
"multiformats": "^13.0.1"
5960
},
6061
"devDependencies": {
62+
"@ipld/dag-ucan": "^3.4.5",
6163
"@storacha/blob-index": "^1.2.4",
6264
"@storacha/cli": "^1.6.2",
6365
"@storacha/client": "^1.8.2",

scripts/mk-validator-proof.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* Create a "ucan/attest" delegation allowing the gateway to validate
3+
* attestations issued by the upload-service.
4+
*
5+
* This generates the GATEWAY_VALIDATOR_PROOF environment variable value.
6+
*
7+
* Usage: node scripts/mk-validator-proof.js <upload-service-did-web> <upload-service-private-key> <gateway-did-web>
8+
*
9+
* Example (staging):
10+
* node scripts/mk-validator-proof.js \
11+
* did:web:staging.up.storacha.network \
12+
* MgCZT5J+...your-key-here... \
13+
* did:web:staging.w3s.link
14+
*
15+
* Example (production):
16+
* node scripts/mk-validator-proof.js \
17+
* did:web:up.storacha.network \
18+
* MgCZT5J+...your-key-here... \
19+
* did:web:w3s.link
20+
*/
21+
import * as DID from '@ipld/dag-ucan/did'
22+
import { CAR, delegate } from '@ucanto/core'
23+
import * as ed25519 from '@ucanto/principal/ed25519'
24+
import { base64 } from 'multiformats/bases/base64'
25+
import { identity } from 'multiformats/hashes/identity'
26+
import * as Link from 'multiformats/link'
27+
28+
// CORRECT DIRECTION (staging):
29+
// - issuer should be did:web:staging.up.storacha.network (upload-service)
30+
// - audience should be did:web:staging.w3s.link (gateway)
31+
// - can should be 'ucan/attest'
32+
// - with should be issuer.did() (i.e. did:web:staging.up.storacha.network)
33+
// The private key must be the upload-service private key. This makes the
34+
// gateway trust attestations issued by the upload-service.
35+
36+
const uploadServiceDIDWeb = process.argv[2]
37+
const uploadServicePrivateKey = process.argv[3]
38+
const gatewayDIDWeb = process.argv[4]
39+
40+
if (!uploadServiceDIDWeb || !uploadServicePrivateKey || !gatewayDIDWeb) {
41+
console.error('Error: Missing required arguments')
42+
console.error('Usage: node scripts/mk-validator-proof.js <upload-service-did-web> <upload-service-private-key> <gateway-did-web>')
43+
console.error('')
44+
console.error('Example (staging):')
45+
console.error(' node scripts/mk-validator-proof.js \\')
46+
console.error(' did:web:staging.up.storacha.network \\')
47+
console.error(' MgCZT5J+...your-key-here... \\')
48+
console.error(' did:web:staging.w3s.link')
49+
process.exit(1)
50+
}
51+
52+
console.log(`Upload Service DID: ${uploadServiceDIDWeb}`)
53+
console.log(`Upload Service Private Key: ${uploadServicePrivateKey.slice(0, 7)}...${uploadServicePrivateKey.slice(-7)}`)
54+
console.log(`Gateway DID: ${gatewayDIDWeb}`)
55+
console.log('')
56+
57+
const issuer = ed25519
58+
.parse(uploadServicePrivateKey)
59+
.withDID(DID.parse(uploadServiceDIDWeb).did())
60+
const audience = DID.parse(gatewayDIDWeb)
61+
62+
// Note: variable names are confusing - "uploadService" is actually the issuer (gateway in our case)
63+
// and "gateway" is actually the audience (upload service in our case)
64+
// The 'with' should be the issuer's DID per colleague's instructions
65+
const delegation = await delegate({
66+
issuer,
67+
audience,
68+
capabilities: [{ can: 'ucan/attest', with: issuer.did() }],
69+
expiration: Infinity
70+
})
71+
72+
console.log('✅ Delegation created:')
73+
console.log(` Issuer: ${issuer.did()}`)
74+
console.log(` Audience: ${audience.did()}`)
75+
console.log(` Capability: ucan/attest with ${issuer.did()}`)
76+
console.log('')
77+
78+
const res = await delegation.archive()
79+
if (res.error) {
80+
console.error('❌ Error archiving delegation:', res.error)
81+
throw res.error
82+
}
83+
84+
const proof = Link.create(CAR.code, identity.digest(res.ok)).toString(base64)
85+
86+
console.log('✅ Validator proof generated successfully!')
87+
console.log('')
88+
console.log('Add this to your environment variables:')
89+
console.log('')
90+
console.log('GATEWAY_VALIDATOR_PROOF=' + proof)
91+
console.log('')

src/bindings.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ export interface Environment
2626
HONEYCOMB_API_KEY: string
2727
FF_TELEMETRY_ENABLED: string
2828
TELEMETRY_RATIO: string
29+
GATEWAY_VALIDATOR_PROOF?: string
2930
}

src/middleware/withAuthorizedSpace.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Verifier } from '@ucanto/principal'
22
import { ok, access, fail, Unauthorized } from '@ucanto/validator'
3+
import { resolveDIDKey, getValidatorProofs } from '../server/index.js'
34
import { HttpError } from '@web3-storage/gateway-lib/util'
45
import * as serve from '../capabilities/serve.js'
56
import { SpaceDID } from '@storacha/capabilities/utils'
@@ -51,9 +52,9 @@ function extractSpaceDID (space) {
5152
* @import * as Ucanto from '@ucanto/interface'
5253
* @import { IpfsUrlContext, Middleware } from '@web3-storage/gateway-lib'
5354
* @import { LocatorContext } from './withLocator.types.js'
54-
* @import { AuthTokenContext } from './withAuthToken.types.js'
55+
* @import { AuthTokenContext, Environment } from './withAuthToken.types.js'
5556
* @import { SpaceContext } from './withAuthorizedSpace.types.js'
56-
* @import { DelegationsStorageContext } from './withDelegationsStorage.types.js'
57+
* @import { DelegationsStorageContext, DelegationsStorageEnvironment } from './withDelegationsStorage.types.js'
5758
* @import { GatewayIdentityContext } from './withGatewayIdentity.types.js'
5859
* @import { DelegationProofsContext } from './withAuthorizedSpace.types.js'
5960
*/
@@ -95,6 +96,7 @@ export function withAuthorizedSpace (handler) {
9596
ctx.authToken === null
9697

9798
if (shouldServeLegacy) {
99+
console.log('[withAuthorizedSpace] Using legacy path (no space)')
98100
return handler(request, env, ctx)
99101
}
100102

@@ -117,7 +119,8 @@ export function withAuthorizedSpace (handler) {
117119
// First space to successfully authorize is the one we'll use.
118120
const { space: selectedSpace, delegationProofs } = await Promise.any(
119121
spaces.map(async (space) => {
120-
const result = await authorize(SpaceDID.from(space.toString()), ctx)
122+
// @ts-ignore
123+
const result = await authorize(SpaceDID.from(space), ctx, env)
121124
if (result.error) {
122125
throw result.error
123126
}
@@ -168,9 +171,10 @@ export function withAuthorizedSpace (handler) {
168171
*
169172
* @param {import('@storacha/capabilities/types').SpaceDID} space
170173
* @param {AuthTokenContext & DelegationsStorageContext & GatewayIdentityContext} ctx
174+
* @param {import('./withRateLimit.types.js').Environment} env
171175
* @returns {Promise<Ucanto.Result<{space: import('@storacha/capabilities/types').SpaceDID, delegationProofs: Ucanto.Delegation[]}, Ucanto.Failure>>}
172176
*/
173-
const authorize = async (space, ctx) => {
177+
const authorize = async (space, ctx, env) => {
174178
// Look up delegations that might authorize us to serve the content.
175179
const relevantDelegationsResult = await ctx.delegationsStorage.find(space)
176180
if (relevantDelegationsResult.error) {
@@ -197,11 +201,14 @@ const authorize = async (space, ctx) => {
197201
})
198202
.delegate()
199203

200-
// Validate the invocation.
204+
// Load validator proofs and validate the invocation
205+
const validatorProofs = await getValidatorProofs(env)
201206
const accessResult = await access(invocation, {
202207
capability: serve.transportHttp,
203208
authority: ctx.gatewayIdentity,
204209
principal: Verifier,
210+
proofs: validatorProofs,
211+
resolveDIDKey,
205212
validateAuthorization: () => ok({})
206213
})
207214

src/middleware/withDelegationsStorage.types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { SpaceDID } from '@storacha/capabilities/types'
77
export interface DelegationsStorageEnvironment extends MiddlewareEnvironment {
88
CONTENT_SERVE_DELEGATIONS_STORE: KVNamespace
99
FF_DELEGATIONS_STORAGE_ENABLED: string
10+
GATEWAY_VALIDATOR_PROOF?: string
1011
}
1112

1213
export interface DelegationsStorageContext
@@ -35,6 +36,6 @@ export interface DelegationsStorage {
3536
*/
3637
store: (
3738
space: SpaceDID,
38-
delegation: Ucanto.Delegation<Ucanto.Capabilities>
39+
delegation: Ucanto.Delegation<Ucanto.Capabilities>,
3940
) => Promise<Ucanto.Result<Ucanto.Unit, StoreOperationFailed | Ucanto.Failure>>
4041
}

src/middleware/withGatewayIdentity.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ export function withGatewayIdentity (handler) {
2020
const gatewayIdentity = gatewaySigner.withDID(
2121
Schema.DID.from(env.GATEWAY_SERVICE_DID)
2222
)
23-
return handler(req, env, { ...ctx, gatewaySigner, gatewayIdentity })
23+
return handler(req, env, {
24+
...ctx,
25+
gatewaySigner,
26+
gatewayIdentity
27+
})
2428
}
2529
}

src/middleware/withGatewayIdentity.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ export interface Environment extends MiddlewareEnvironment {
88
}
99

1010
export interface GatewayIdentityContext extends MiddlewareContext {
11-
gatewaySigner: EdSigner
11+
gatewaySigner: Ucanto.Signer
1212
gatewayIdentity: Ucanto.Signer
1313
}

src/middleware/withRateLimit.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface Environment extends MiddlewareEnvironment {
66
RATE_LIMITER: RateLimit
77
AUTH_TOKEN_METADATA: KVNamespace
88
FF_RATE_LIMITER_ENABLED: string
9+
GATEWAY_VALIDATOR_PROOF?: string
910
}
1011

1112
export interface TokenMetadata {

src/middleware/withUcanInvocationHandler.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ export function withUcanInvocationHandler (handler) {
2222
return handler(request, env, ctx)
2323
}
2424

25-
const service = ctx.service ?? createService(ctx)
26-
const server = ctx.server ?? createServer(ctx, service)
25+
const service =
26+
ctx.service ?? (await createService(ctx, env))
27+
const server = ctx.server ?? (await createServer(ctx, service, env))
2728

2829
const { headers, body, status } = await server.request({
2930
body: new Uint8Array(await request.arrayBuffer()),

0 commit comments

Comments
 (0)