Skip to content

Commit ec8f6a1

Browse files
committed
docs: signing service RFC
1 parent 0875d71 commit ec8f6a1

File tree

1 file changed

+276
-0
lines changed

1 file changed

+276
-0
lines changed

rfc/signing-service.md

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
# Authorizing Proof Data Provider Operations
2+
3+
## Problem
4+
5+
Node providers on the Storacha network need to perform operations on proof sets - adding piece roots, deleting roots, and other state changes.
6+
These operations require signatures from Storacha to be valid on-chain.
7+
8+
Currently, we don't have a clean way to authorize these operations. Node providers need to:
9+
1. Prove they're authorized to request signatures from Storacha
10+
2. Get Storacha's signature for their operation payload
11+
3. Use that signature to submit blockchain transactions
12+
13+
We need a system that:
14+
- Only allows authorized operators to request signatures
15+
- Prevents unauthorized access to the signing service
16+
- Maintains a verifiable chain of authority back to Storacha
17+
- Doesn't require manual management of operator allow-lists
18+
- Uses short-lived delegations instead of relying on revocations
19+
20+
## Background
21+
22+
### EIP-712: Typed Structured Data Signing
23+
24+
The operations we're authorizing need to be submitted as blockchain transactions.
25+
Specifically, we're calling smart contract methods like `addPieces()` and `deletePieces()` on Filecoin.
26+
27+
These contract methods require signatures that prove the operations were authorized by Storacha.
28+
We use [EIP-712](https://eips.ethereum.org/EIPS/eip-712) for this signing.
29+
30+
EIP-712 is an Ethereum standard for signing typed, structured data.
31+
Instead of signing raw bytes (which are hard for users to verify), EIP-712 lets you sign structured data with clear types and fields.
32+
The signature proves:
33+
1. The exact data being signed (operation type, parameters, values)
34+
2. Which contract the data is intended for (domain separation)
35+
3. Who signed it (via signature verification on-chain)
36+
37+
For example, an `AddPieces` operation might have a structure like:
38+
39+
```solidity
40+
struct AddPieces {
41+
uint256 clientDataSetId;
42+
uint256 firstAdded;
43+
bytes32[] pieceData;
44+
bytes[] metadata;
45+
}
46+
```
47+
48+
The EIP-712 signature of this structure can be verified on-chain by the smart contract, which checks that it was signed by Storacha's authorized key.
49+
50+
This is why we need a signing service.
51+
The private key that creates these EIP-712 signatures must be controlled by Storacha, but node providers need to be able to request signatures for their operations.
52+
The signing service is the bridge - it holds Storacha's key and issues signatures to authorized operators.
53+
54+
## Proposal
55+
56+
### UCAN-Authorized Signing Service
57+
58+
We'll create a signing service that uses UCAN delegations to authorize signature requests.
59+
The flow works like this:
60+
61+
1. Storacha issues delegations to authorized operators granting them the `pdp/sign` capability
62+
2. Operators create UCAN invocations requesting signatures, proving their authority via the delegation chain
63+
3. The signing service validates the delegation chain and signs the operation payload
64+
4. Operators use the returned signature for blockchain transactions
65+
66+
The key insight: the delegation chain IS the authorization.
67+
If an operator has a valid delegation from Storacha, they can request signatures.
68+
No separate allow-list needed.
69+
70+
### The `pdp/sign` Capability
71+
72+
We'll introduce a new capability `pdp/sign` that represents the ability to request signatures for proof data provider operations.
73+
74+
Storacha has the natural authority to grant this capability, as it controls the signing keys used for on-chain operations.
75+
76+
### Flow
77+
78+
#### Step 1: Operator Creates UCAN Invocation
79+
80+
When a node provider needs to perform an operation (like adding pieces), they create a UCAN invocation:
81+
82+
```
83+
┌─────────────────────────────────────────┐
84+
│ Issuer: did:key:PiriNode... │ ← Operator's identity
85+
│ Audience: did:key:SigningService │ ← Signing service identity
86+
│ Capability: │
87+
│ with: "did:key:SigningService" │ ← Resource (signing service)
88+
│ can: "pdp/sign" │ ← Ability
89+
│ nb: { │ ← Caveats
90+
│ operation: "AddPieces", │
91+
│ payload: { │
92+
│ clientDataSetId: "123", │
93+
│ firstAdded: "0", │
94+
│ pieceData: ["0xabc...", ...], │
95+
│ metadata: [...] │
96+
│ } │
97+
│ } │
98+
│ Proofs: [ │ ← Delegation chain
99+
│ <delegation from Storacha> │ (proves operator can pdp/sign)
100+
│ ] │
101+
│ Signature: <signed by operator key> │
102+
└─────────────────────────────────────────┘
103+
```
104+
105+
The `proofs` field contains a delegation from Storacha that looks like:
106+
107+
```
108+
┌─────────────────────────────────────────┐
109+
│ Issuer: did:web:storacha.network │ ← Storacha's identity
110+
│ Audience: did:key:PiriNode... │ ← Operator's identity
111+
│ Capability: │
112+
│ with: "did:key:SigningService" │
113+
│ can: "pdp/sign" │
114+
│ nb: { │
115+
│ operations: [ │
116+
│ "AddPieces", │
117+
│ "DeletePieces", │
118+
│ ... │
119+
│ ] │
120+
│ } │
121+
│ Expiration: <short lived, e.g. 1 day> │
122+
│ Signature: <signed by Storacha> │
123+
└─────────────────────────────────────────┘
124+
```
125+
126+
#### Step 2: Send to Signing Service
127+
128+
```
129+
Piri Node
130+
131+
HTTP POST to signing-service.storacha.network
132+
Content-Type: application/car
133+
Body: <CAR file containing UCAN invocation>
134+
135+
Signing Service
136+
```
137+
138+
#### Step 3: Signing Service Validates UCAN
139+
140+
The signing service validates the invocation:
141+
142+
1. **Verify UCAN signature**
143+
- Is the signature valid for the issuer DID?
144+
145+
2. **Check delegation chain**
146+
- Do the proofs link back to Storacha's root authority?
147+
- Is each delegation in the chain valid and not expired?
148+
- Does the chain grant the `pdp/sign` capability?
149+
150+
3. **Verify audience**
151+
- Is the audience the signing service's DID?
152+
153+
4. **Validate caveats**
154+
- Is the operation valid (e.g., "AddPieces", "DeletePieces")?
155+
- Does the payload have all required fields?
156+
157+
If validation fails, return a UCAN receipt with an error.
158+
If it succeeds, proceed to signing.
159+
160+
#### Step 4: Signing Service Signs Payload
161+
162+
The signing service extracts the operation and payload from the caveats and calls the appropriate EIP-712 signer:
163+
164+
```go
165+
operation := invocation.Capability.Nb["operation"]
166+
payload := invocation.Capability.Nb["payload"]
167+
168+
// For AddPieces 'operation':
169+
signature := signer.SignAddPieces(
170+
payload.ClientDataSetId,
171+
payload.FirstAdded,
172+
payload.PieceData,
173+
payload.Metadata,
174+
)
175+
176+
// Returns: {v: 28, r: 0x..., s: 0x..., signature: 0x...}
177+
```
178+
179+
#### Step 5: Signing Service Returns UCAN Receipt
180+
181+
The signing service creates a receipt linking back to the invocation:
182+
183+
```
184+
┌─────────────────────────────────────────┐
185+
│ Issuer: did:key:SigningService │ ← Signing service identity
186+
│ Ran: <CID of invocation> │ ← Links to the request
187+
│ Out: │
188+
│ ok: { │
189+
│ v: 28, │
190+
│ r: "0x1234...", │
191+
│ s: "0x5678...", │
192+
│ signature: "0xabcd...", │
193+
│ signedData: "0xef01...", │
194+
│ signer: "0x742d35..." │
195+
│ } │
196+
│ Signature: <signed by signing service> │
197+
└─────────────────────────────────────────┘
198+
```
199+
200+
For errors:
201+
202+
```
203+
┌─────────────────────────────────────────┐
204+
│ Issuer: did:key:SigningService │
205+
│ Ran: <CID of invocation> │
206+
│ Out: │
207+
│ error: { │
208+
│ name: "UnauthorizedError", │
209+
│ message: "Invalid delegation chain" │
210+
│ } │
211+
│ Signature: <signed by signing service> │
212+
└─────────────────────────────────────────┘
213+
```
214+
215+
#### Step 6: Operator Receives Response
216+
217+
```
218+
Signing Service
219+
220+
HTTP 200 OK
221+
Content-Type: application/car
222+
Body: <CAR file containing UCAN receipt>
223+
224+
Piri Node
225+
```
226+
227+
The operator validates the receipt and extracts the signature:
228+
229+
```go
230+
// Validate receipt
231+
if receipt.Ran != invocationCID {
232+
return errors.New("receipt doesn't match invocation")
233+
}
234+
235+
// Verify receipt signature
236+
if !verifySignature(receipt, signingServiceDID) {
237+
return errors.New("invalid receipt signature")
238+
}
239+
240+
// Extract signature components
241+
result := receipt.Out.Ok
242+
v := result.V
243+
r := result.R
244+
s := result.S
245+
246+
// Submit blockchain transaction
247+
contract.AddPieces(v, r, s, clientDataSetId, firstAdded, pieceData, metadata)
248+
```
249+
250+
### Benefits
251+
252+
**No manual operator management**: The delegation chain proves authorization. No need to maintain allow-lists in the signing service.
253+
254+
**Short-lived delegations**: Storacha can issue delegations with short expiration times (e.g., 1 day). Revocations are rarely needed.
255+
256+
**Auditable**: Every signature request is a UCAN invocation with a full delegation chain. We can trace who requested what and when.
257+
258+
**Flexible**: Easy to add new operations or adjust scopes in delegations without redeploying services.
259+
260+
**Standard**: Uses UCAN patterns consistent with the rest of the network.
261+
262+
### Implementation Notes
263+
264+
**Delegation distribution**: Storacha needs a mechanism to issue delegations to new operators. This could be:
265+
- Automated when operators join the network
266+
- Requested via an `access/authorize` flow (see https://github.com/storacha/RFC/pull/68)
267+
- Issued through an admin interface
268+
269+
**Delegation caching**: Operators should cache delegations until they expire to avoid requesting new ones for every operation.
270+
271+
**Replay protection**: The blockchain transaction layer handles replay protection via nonces.
272+
The signing service doesn't need to track used invocations, though it could for monitoring purposes.
273+
274+
**Operation types**: Initial operations will be `CreateDataSet`, `AddPieces` and `DeletePieces`.
275+
The design supports adding more operation types as needed.
276+

0 commit comments

Comments
 (0)