@tideorg/mcp 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GAP_REGISTER.md +117 -0
- package/README.md +38 -0
- package/adapters/AGENTS.md +241 -0
- package/adapters/CLAUDE.md +146 -0
- package/adapters/replit.md +224 -0
- package/canon/anti-patterns.md +2133 -0
- package/canon/concepts.md +816 -0
- package/canon/custom-contracts.md +533 -0
- package/canon/delegation.md +195 -0
- package/canon/feature-mapping.md +637 -0
- package/canon/framework-matrix.md +1125 -0
- package/canon/invariants.md +851 -0
- package/canon/redirect-handler.md +254 -0
- package/canon/tidecloak-bootstrap.md +287 -0
- package/canon/tidecloak-endpoints.md +294 -0
- package/canon/troubleshooting.md +1494 -0
- package/canon/version-policy.md +89 -0
- package/mcp-server/dist/index.d.ts +2 -0
- package/mcp-server/dist/index.js +605 -0
- package/package.json +45 -0
- package/playbooks/add-auth-nextjs-existing.md +799 -0
- package/playbooks/add-auth-nextjs-fresh.md +654 -0
- package/playbooks/add-rbac-nextjs.md +190 -0
- package/playbooks/bootstrap-realm-from-template.md +170 -0
- package/playbooks/configure-e2ee-roles-and-policies.md +196 -0
- package/playbooks/deploy-tidecloak-docker.md +792 -0
- package/playbooks/diagnose-broken-login.md +234 -0
- package/playbooks/diagnose-missing-roles-or-claims.md +253 -0
- package/playbooks/initialize-admin-and-link-account.md +235 -0
- package/playbooks/migrate-from-existing-auth.md +198 -0
- package/playbooks/protect-api-nextjs.md +451 -0
- package/playbooks/protect-routes-nextjs.md +544 -0
- package/playbooks/setup-forseti-e2ee.md +756 -0
- package/playbooks/setup-iga-admin-panel.md +344 -0
- package/playbooks/setup-server-delegation.md +142 -0
- package/playbooks/start-tidecloak-dev.md +130 -0
- package/playbooks/verify-jwt-server-side.md +439 -0
- package/prompts/add-admin-approval-flow.md +50 -0
- package/prompts/build-private-customer-portal.md +85 -0
- package/prompts/migrate-generic-auth-to-tide.md +67 -0
- package/prompts/secure-existing-app.md +80 -0
- package/reference-apps/INDEX.md +87 -0
- package/reference-apps/encrypted-communication/anti-patterns.md +123 -0
- package/reference-apps/encrypted-communication/bootstrap-sequence.md +152 -0
- package/reference-apps/encrypted-communication/manifest.yaml +100 -0
- package/reference-apps/encrypted-communication/role-policy-matrix.md +80 -0
- package/reference-apps/encrypted-communication/scenario.md +134 -0
- package/reference-apps/git-pr-signing-service/anti-patterns.md +61 -0
- package/reference-apps/git-pr-signing-service/bootstrap-sequence.md +157 -0
- package/reference-apps/git-pr-signing-service/manifest.yaml +80 -0
- package/reference-apps/git-pr-signing-service/role-policy-matrix.md +99 -0
- package/reference-apps/git-pr-signing-service/scenario.md +169 -0
- package/reference-apps/iga-admin-governance/anti-patterns.md +133 -0
- package/reference-apps/iga-admin-governance/bootstrap-sequence.md +153 -0
- package/reference-apps/iga-admin-governance/manifest.yaml +87 -0
- package/reference-apps/iga-admin-governance/role-policy-matrix.md +67 -0
- package/reference-apps/iga-admin-governance/scenario.md +126 -0
- package/reference-apps/organisation-password-manager/anti-patterns.md +71 -0
- package/reference-apps/organisation-password-manager/bootstrap-sequence.md +127 -0
- package/reference-apps/organisation-password-manager/manifest.yaml +86 -0
- package/reference-apps/organisation-password-manager/role-policy-matrix.md +59 -0
- package/reference-apps/organisation-password-manager/scenario.md +107 -0
- package/reference-apps/policy-governed-signing/anti-patterns.md +67 -0
- package/reference-apps/policy-governed-signing/bootstrap-sequence.md +128 -0
- package/reference-apps/policy-governed-signing/manifest.yaml +78 -0
- package/reference-apps/policy-governed-signing/role-policy-matrix.md +62 -0
- package/reference-apps/policy-governed-signing/scenario.md +113 -0
- package/skills/tide-diagnostics/SKILL.md +99 -0
- package/skills/tide-integration/SKILL.md +136 -0
- package/skills/tide-learning-capture/SKILL.md +174 -0
- package/skills/tide-rbac-and-e2ee/SKILL.md +214 -0
- package/skills/tide-reviewer/SKILL.md +133 -0
- package/skills/tide-route-and-api-protection/SKILL.md +201 -0
- package/skills/tide-scenario-resolver/SKILL.md +81 -0
- package/skills/tide-setup/SKILL.md +218 -0
- package/skills/tide-solutions-architect/SKILL.md +130 -0
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
# Set Up Forseti-Governed E2EE (Policy-Based Shared Encryption)
|
|
2
|
+
|
|
3
|
+
Implement end-to-end encrypted data sharing using Tide's Forseti policy contracts and VVK encryption. This is the most complex Tide integration. Follow every step exactly.
|
|
4
|
+
|
|
5
|
+
**CRITICAL**: Self-encryption and policy-governed encryption are completely different mechanisms. Do not mix them. Do not skip the policy signing flow. Do not improvise byte serialization.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## When to Use
|
|
10
|
+
|
|
11
|
+
- Encrypting data that multiple users must decrypt (shared secrets, messages, credentials)
|
|
12
|
+
- Enforcing role-based access to encrypted data via Forseti contracts on the ORK network
|
|
13
|
+
- Building scoped group encryption (e.g., team vaults, org-level shared data)
|
|
14
|
+
- Requiring threshold-governed operations with custom business rules
|
|
15
|
+
|
|
16
|
+
**Do not use** for private-only encryption where only the encrypting user needs access. Use self-encryption instead.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Prerequisites
|
|
21
|
+
|
|
22
|
+
- TideCloak realm deployed with ORK network connected
|
|
23
|
+
- `@tidecloak/js` and `heimdall-tide` packages installed
|
|
24
|
+
- `IAMService` initialized via `useTideCloak()` or equivalent
|
|
25
|
+
- Admin user with `tide-realm-admin` role (required for policy signing)
|
|
26
|
+
- Backend API for storing signed policy bytes
|
|
27
|
+
- Familiarity with [protect-api-nextjs.md](protect-api-nextjs.md) and [add-rbac-nextjs.md](add-rbac-nextjs.md)
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Two Encryption Models
|
|
32
|
+
|
|
33
|
+
### 1. Self-Encryption (Private Data, NO Sharing)
|
|
34
|
+
|
|
35
|
+
Uses `doEncrypt`/`doDecrypt` from `useTideCloak()` WITHOUT a policy parameter. Ciphertext is bound to the encrypting user's identity. Only that user can ever decrypt. Giving another user the same `selfdecrypt` role does NOT let them decrypt your data.
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
const { doEncrypt, doDecrypt } = useTideCloak();
|
|
39
|
+
|
|
40
|
+
// Private: ONLY I can ever decrypt this
|
|
41
|
+
const encrypted = await doEncrypt([{ data: "my secret", tags: ["vault"] }]);
|
|
42
|
+
const plaintext = await doDecrypt([{ encrypted: encrypted[0], tags: ["vault"] }]);
|
|
43
|
+
// plaintext only exists in browser memory
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Requires `_tide_{tag}.selfencrypt` / `_tide_{tag}.selfdecrypt` realm roles.
|
|
47
|
+
|
|
48
|
+
### 2. Policy-Governed VVK Encryption (Shared Data)
|
|
49
|
+
|
|
50
|
+
Uses `IAMService.doEncrypt(data, policyBytes)` WITH a signed policy. Encrypts with VVK (organizational key distributed across ORKs). Any user whose doken satisfies the Forseti policy can decrypt.
|
|
51
|
+
|
|
52
|
+
```jsx
|
|
53
|
+
const { IAMService } = useTideCloak();
|
|
54
|
+
|
|
55
|
+
// Encrypt with VVK + policy (shared: anyone with the right role can decrypt)
|
|
56
|
+
const encrypted = await IAMService.doEncrypt(
|
|
57
|
+
[{ data: "shared message", tags: ["shared"] }],
|
|
58
|
+
signedPolicyBytes // MUST be VVK-signed Uint8Array
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Decrypt (any user whose doken satisfies the Forseti contract)
|
|
62
|
+
const decrypted = await IAMService.doDecrypt(
|
|
63
|
+
[{ encrypted: ciphertext, tags: ["shared"] }],
|
|
64
|
+
signedPolicyBytes
|
|
65
|
+
);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The `doEncrypt`/`doDecrypt` on `useTideCloak()` are convenience wrappers that do NOT pass the policy parameter through. For shared encryption, call `IAMService` directly.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Step 1: Write the Forseti Contract (C#)
|
|
73
|
+
|
|
74
|
+
Every contract implements `IAccessPolicy` from `Ork.Forseti.Sdk`. Without the `using` directive, the ORK compiler fails with `"The type or namespace name 'IAccessPolicy' could not be found"`.
|
|
75
|
+
|
|
76
|
+
```csharp
|
|
77
|
+
using Ork.Forseti.Sdk;
|
|
78
|
+
|
|
79
|
+
public class Contract : IAccessPolicy
|
|
80
|
+
{
|
|
81
|
+
[PolicyParam(Required = true)]
|
|
82
|
+
public string Role { get; set; }
|
|
83
|
+
|
|
84
|
+
public PolicyDecision ValidateData(DataContext ctx)
|
|
85
|
+
{
|
|
86
|
+
if (ctx.Data == null || ctx.Data.Length == 0)
|
|
87
|
+
return PolicyDecision.Deny("No data provided");
|
|
88
|
+
return PolicyDecision.Allow();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public PolicyDecision ValidateExecutor(ExecutorContext ctx)
|
|
92
|
+
{
|
|
93
|
+
var executor = new DokenDto(ctx.Doken);
|
|
94
|
+
return Decision
|
|
95
|
+
.RequireNotExpired(executor)
|
|
96
|
+
.RequireRole(executor, Role);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
For contracts that also need approver validation (EXPLICIT approval mode):
|
|
102
|
+
|
|
103
|
+
```csharp
|
|
104
|
+
using Ork.Forseti.Sdk;
|
|
105
|
+
|
|
106
|
+
public class Contract : IAccessPolicy
|
|
107
|
+
{
|
|
108
|
+
[PolicyParam(Required = true)]
|
|
109
|
+
public string Role { get; set; }
|
|
110
|
+
|
|
111
|
+
[PolicyParam(Required = true)]
|
|
112
|
+
public string Resource { get; set; }
|
|
113
|
+
|
|
114
|
+
public PolicyDecision ValidateData(DataContext ctx)
|
|
115
|
+
{
|
|
116
|
+
if (ctx.Data == null || ctx.Data.Length == 0)
|
|
117
|
+
return PolicyDecision.Deny("No data provided");
|
|
118
|
+
return PolicyDecision.Allow();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public PolicyDecision ValidateApprovers(ApproversContext ctx)
|
|
122
|
+
{
|
|
123
|
+
// M-of-N approval (for EXPLICIT policies)
|
|
124
|
+
var approvers = DokenDto.WrapAll(ctx.Dokens);
|
|
125
|
+
return Decision
|
|
126
|
+
.RequireAnyWithRole(approvers, Resource, Role);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public PolicyDecision ValidateExecutor(ExecutorContext ctx)
|
|
130
|
+
{
|
|
131
|
+
// Check executor has required role (for PRIVATE policies)
|
|
132
|
+
var executor = new DokenDto(ctx.Doken);
|
|
133
|
+
return Decision
|
|
134
|
+
.RequireNotExpired(executor)
|
|
135
|
+
.RequireRole(executor, Resource, Role);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Advanced contract features** (from forseti-crypto-quickstart):
|
|
141
|
+
- Separate `EncryptionRealmRole` and `DecryptionRealmRole` params (both optional)
|
|
142
|
+
- Detect encrypt vs decrypt via `ctx.RequestId` (`PolicyEnabledEncryption:1` / `PolicyEnabledDecryption:1`)
|
|
143
|
+
- Read tags from `ctx.Data` and enforce `DecryptTimeLock:{epoch}` tags (block decryption until a timestamp)
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Step 2: Compute the Contract Hash
|
|
148
|
+
|
|
149
|
+
**There is no REST API for deploying Forseti contracts.** Do not use `PUT` or `POST` to `/tide-admin/forseti-contracts` — these endpoints do not exist (404/405). Contract deployment happens via `PolicySignRequest.addForsetiContractToUpload(contractSource)` in the browser signing flow (Step 4). VERIFIED (session-001, LEARNINGS-batch-007 L-06).
|
|
150
|
+
|
|
151
|
+
The `contractId` must be the SHA-512 hash of the EXACT contract source. Compute it locally:
|
|
152
|
+
|
|
153
|
+
```js
|
|
154
|
+
async function computeContractHash(source) {
|
|
155
|
+
const data = new TextEncoder().encode(source);
|
|
156
|
+
const hash = await crypto.subtle.digest("SHA-512", data);
|
|
157
|
+
return Array.from(new Uint8Array(hash))
|
|
158
|
+
.map(b => b.toString(16).padStart(2, "0")).join("").toUpperCase();
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Step 3: Construct the Policy
|
|
165
|
+
|
|
166
|
+
Policy uses IMPLICIT + PRIVATE:
|
|
167
|
+
- **IMPLICIT** approval: no separate approver dokens needed
|
|
168
|
+
- **PRIVATE** execution: runs `ValidateExecutor` on the user's doken
|
|
169
|
+
|
|
170
|
+
**CRITICAL import path**: `ApprovalType` and `ExecutionType` are direct exports from `Models`, alongside `Policy`. They are NOT nested under `Models.Policy` (which is the class itself, not a namespace).
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
const { Models } = await import("@tideorg/js");
|
|
174
|
+
const { Policy, ApprovalType, ExecutionType } = Models;
|
|
175
|
+
|
|
176
|
+
// vendorId MUST come from the adapter JSON config (data/tidecloak.json).
|
|
177
|
+
// Do NOT read it from IAMService._tc.vendorId — that property is undefined.
|
|
178
|
+
// import tcConfig from '../../data/tidecloak.json';
|
|
179
|
+
const vendorId = tcConfig.vendorId; // or tcConfig["vendorId"]
|
|
180
|
+
|
|
181
|
+
const policy = new Policy({
|
|
182
|
+
version: "3",
|
|
183
|
+
contractId: contractHash,
|
|
184
|
+
// modelId MUST be an array of specific model IDs, NOT "any"
|
|
185
|
+
// These are the ORK request types the policy governs
|
|
186
|
+
modelId: ["PolicyEnabledEncryption:1", "PolicyEnabledDecryption:1"],
|
|
187
|
+
keyId: vendorId, // from adapter JSON, not from IAMService
|
|
188
|
+
approvalType: ApprovalType.IMPLICIT,
|
|
189
|
+
executionType: ExecutionType.PRIVATE,
|
|
190
|
+
params: new Map([["Role", "shared-data-access"]]),
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Wrong:**
|
|
195
|
+
```js
|
|
196
|
+
// WRONG: destructuring from Models.Policy (a class, not a namespace)
|
|
197
|
+
const { ApprovalType, ExecutionType } = Models.Policy;
|
|
198
|
+
|
|
199
|
+
// WRONG: using "any" instead of specific model IDs
|
|
200
|
+
modelId: "any"
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Step 4: Sign the Policy (5-Step Flow)
|
|
206
|
+
|
|
207
|
+
Raw `policy.toBytes()` will be rejected by the ORKs with `"Policy supplied has not been signed"`. Policy signing is a 5-step process.
|
|
208
|
+
|
|
209
|
+
### CRITICAL: Import Paths
|
|
210
|
+
|
|
211
|
+
`PolicySignRequest` MUST come from `heimdall-tide`. Despite broken package exports for some symbols, `PolicySignRequest` itself works and handles the draft serialization format that the ORK expects. Do NOT try to build the policy signing request manually with `BaseTideRequest` + `TideMemory`.
|
|
212
|
+
|
|
213
|
+
```js
|
|
214
|
+
const { PolicySignRequest } = await import("heimdall-tide");
|
|
215
|
+
const { Models } = await import("@tideorg/js");
|
|
216
|
+
const { BaseTideRequest } = Models;
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### The 5 Steps
|
|
220
|
+
|
|
221
|
+
```js
|
|
222
|
+
const tc = IAMService._tc;
|
|
223
|
+
if (!tc) throw new Error("TideCloak not initialized — user must be logged in before policy signing");
|
|
224
|
+
|
|
225
|
+
// Step 1: Build and initialize the request
|
|
226
|
+
const policyRequest = PolicySignRequest.New(policy);
|
|
227
|
+
policyRequest.addForsetiContractToUpload(CONTRACT_SOURCE);
|
|
228
|
+
policyRequest.setCustomExpiry(604800); // 1 week
|
|
229
|
+
|
|
230
|
+
const initializedBytes = await tc.createTideRequest(policyRequest.encode());
|
|
231
|
+
const initialized = BaseTideRequest.decode(initializedBytes);
|
|
232
|
+
|
|
233
|
+
// Step 2: Operator approval (opens popup for admin to sign)
|
|
234
|
+
const approvalResults = await tc.requestTideOperatorApproval([
|
|
235
|
+
{ id: "policy-sign", request: initialized.encode() },
|
|
236
|
+
]);
|
|
237
|
+
if (approvalResults[0].status !== "approved") throw new Error("Denied");
|
|
238
|
+
|
|
239
|
+
// Step 3: Decode approved request and ATTACH THE ADMIN POLICY
|
|
240
|
+
const approvedRequest = BaseTideRequest.decode(approvalResults[0].request);
|
|
241
|
+
const adminPolicyBytes = await fetchAdminPolicyFromTideCloak(token);
|
|
242
|
+
approvedRequest.addPolicy(adminPolicyBytes);
|
|
243
|
+
|
|
244
|
+
// Step 4: Execute sign request (waitForAll MUST be true)
|
|
245
|
+
const signatures = await tc.executeSignRequest(approvedRequest.encode(), true);
|
|
246
|
+
|
|
247
|
+
// Step 5: Attach VVK signature to the policy object
|
|
248
|
+
policy.signature = signatures[0];
|
|
249
|
+
const signedBytes = policy.toBytes(); // NOW has VVK signature
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**Reference implementation wrapper functions** (from forseti-crypto-quickstart `AuthProvider.tsx`). This is the EXACT pattern that works. Do not deviate:
|
|
253
|
+
|
|
254
|
+
```js
|
|
255
|
+
const initializeTideRequest = async (request) => {
|
|
256
|
+
return BaseTideRequest.decode(
|
|
257
|
+
await IAMService._tc?.createTideRequest(request.encode())
|
|
258
|
+
);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const approveTideRequests = async (requests) => {
|
|
262
|
+
return await IAMService._tc?.requestTideOperatorApproval(requests);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const executeTideRequest = async (request, waitForAll = false) => {
|
|
266
|
+
return await IAMService._tc?.executeSignRequest(request, waitForAll);
|
|
267
|
+
};
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### What Fails If You Skip Each Step
|
|
271
|
+
|
|
272
|
+
| Skipped Step | Error |
|
|
273
|
+
|---|---|
|
|
274
|
+
| Step 2 (approval popup) | `executeSignRequest` alone does not sign policies |
|
|
275
|
+
| Step 3 (admin policy) | ORKs reject: `"Policy supplied has not been signed"` |
|
|
276
|
+
| Step 4 (executeSignRequest) | Approval popup alone does not produce the VVK signature |
|
|
277
|
+
| Step 5 (signature assignment) | `policy.toBytes()` has no signature, ORKs reject |
|
|
278
|
+
|
|
279
|
+
### Critical Pitfalls
|
|
280
|
+
|
|
281
|
+
- Do NOT call `addPolicy(policy.toBytes())` on the `BaseTideRequest` during construction. Attaching an unsigned policy causes `createTideRequest` to fail immediately. Only call `addPolicy()` in step 3 to attach the signed admin policy.
|
|
282
|
+
- Do NOT store signed policy bytes to the server until verified. If a failed signing attempt stores unsigned bytes, subsequent calls fetch stale bytes and silently fail.
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Step 5: Get the Admin Policy (Correct Endpoint)
|
|
287
|
+
|
|
288
|
+
```
|
|
289
|
+
WRONG: GET /admin/realms/{realm}/tide-admin/realm-policy
|
|
290
|
+
-> { status: "none" } (even when the policy exists)
|
|
291
|
+
|
|
292
|
+
CORRECT: GET /realms/{realm}/tide-policy-resources/admin-policy
|
|
293
|
+
-> base64-encoded bytes (public, no auth required)
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
The correct endpoint does not require master admin credentials. Proxy through your backend.
|
|
297
|
+
|
|
298
|
+
**CORS: Browser cannot fetch this endpoint directly.** TideCloak and the app are on different origins. Direct browser fetch returns an opaque/blocked response. Create a same-origin API route that proxies the request server-side. VERIFIED (session-001).
|
|
299
|
+
|
|
300
|
+
**If the proxy route is protected with `withAuth` (correct per I-03), the client must use authenticated fetch** (e.g., `appFetch` or `secureFetch` with Bearer header). A bare `fetch()` without Authorization returns 401. The resulting empty/error response produces undefined policy bytes, causing `PolicyAuthorizationFlowException: "Model does not have a policy passed with it"` at signing time. VERIFIED (LEARNINGS-batch-009 L-08).
|
|
301
|
+
|
|
302
|
+
**There is no REST API for deploying Forseti contracts.** Do not try `PUT` or `POST` to `/tide-admin/forseti-contracts` — these endpoints do not exist on the dev image (404/405). Contract deployment happens via `PolicySignRequest.addForsetiContractToUpload(contractSource)` in the browser signing flow (Step 4). VERIFIED (session-001).
|
|
303
|
+
|
|
304
|
+
**The response is base64-encoded, not raw binary.** You must decode before using:
|
|
305
|
+
|
|
306
|
+
```js
|
|
307
|
+
// Backend proxy
|
|
308
|
+
const b64 = await tcRes.text();
|
|
309
|
+
const raw = Buffer.from(b64, "base64");
|
|
310
|
+
const bytes = Array.from(new Uint8Array(raw));
|
|
311
|
+
res.json({ policyBytes: bytes });
|
|
312
|
+
|
|
313
|
+
// Frontend
|
|
314
|
+
const data = await res.json();
|
|
315
|
+
const adminPolicyBytes = new Uint8Array(data.policyBytes);
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**Common mistake:** Treating the base64 string as raw bytes (passing each character's char code as a byte value). If admin policy bytes start with `[65, 81, 65, 65, ...]` (ASCII for "AQAA..."), you are passing base64 text as byte values instead of decoding it. The ORK will fail with `Index out of range` because the policy structure is garbage.
|
|
319
|
+
|
|
320
|
+
**Common mistake:** Malformed URL from trailing slash in `auth-server-url`. The adapter JSON `auth-server-url` may include a trailing slash (e.g., `http://localhost:8080/`). Constructing the admin policy URL without stripping it produces `http://localhost:8080//realms/...` (double slash), which returns HTML or 404 instead of policy bytes. Always normalize:
|
|
321
|
+
```js
|
|
322
|
+
const baseUrl = config["auth-server-url"].replace(/\/+$/, "");
|
|
323
|
+
const adminPolicyUrl = `${baseUrl}/realms/${config.realm}/tide-policy-resources/admin-policy`;
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## Step 6: Set Up Dual Role Requirement (Voucher Gates + Forseti Contract Roles)
|
|
329
|
+
|
|
330
|
+
Even though policy-governed encryption uses the VVK and the Forseti contract uses a regular role for access control, the TideCloak voucher system independently gates whether encrypt/decrypt operations are even allowed for a session.
|
|
331
|
+
|
|
332
|
+
The voucher system (in `VoucherResource.java`) performs role-based voucher authorization **VERIFIED** (vendor confirmation, GAP-051 resolved):
|
|
333
|
+
- `signin` and `updateaccount`: always allowed
|
|
334
|
+
- `vendorsign` (encrypt): allowed if user has ANY `_tide_*` role, an authorizer role, or `tide-realm-admin`
|
|
335
|
+
- `vendordecrypt` (decrypt): allowed if user has a role matching `_tide_*.selfdecrypt` or `_tide_*.decrypt`
|
|
336
|
+
|
|
337
|
+
Without the voucher, the ORK refuses to participate before the Forseti contract even runs. Error: `"session ID ... has not been allowed a voucher action of type vendordecrypt"`
|
|
338
|
+
|
|
339
|
+
**Policy-based encryption does NOT require per-tag `_tide_{tag}.encrypt`/`.decrypt` roles** (vendor confirmation, batch-02 Q-10). The SDK skips the tag-role check when a `decryption_policy` is provided. Authorization is handled entirely by the Forseti contract, which can check any Keycloak role. You only need `_tide_*` roles to satisfy the voucher system.
|
|
340
|
+
|
|
341
|
+
You need TWO types of roles:
|
|
342
|
+
|
|
343
|
+
| Role Type | Example | Purpose |
|
|
344
|
+
|-----------|---------|---------|
|
|
345
|
+
| Voucher gate (generic) | `_tide_x.selfencrypt` / `_tide_x.selfdecrypt` | Enables the voucher action type. Use a single generic tag (e.g., `x`) for all policy-based encrypt/decrypt. Does NOT control who can decrypt. |
|
|
346
|
+
| Forseti contract role | `shared-data-access` | Checked by the contract's `ValidateExecutor`. Controls WHO can actually decrypt. Regular realm role, NOT `_tide_*`. |
|
|
347
|
+
|
|
348
|
+
```json
|
|
349
|
+
// realm.json: need BOTH types of roles
|
|
350
|
+
{
|
|
351
|
+
"roles": {
|
|
352
|
+
"realm": [
|
|
353
|
+
{ "name": "shared-data-access", "description": "Forseti contract role: who can encrypt/decrypt" },
|
|
354
|
+
{ "name": "_tide_x.selfencrypt", "description": "Voucher gate: enables vendorsign (generic tag)" },
|
|
355
|
+
{ "name": "_tide_x.selfdecrypt", "description": "Voucher gate: enables vendordecrypt (generic tag)" }
|
|
356
|
+
]
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
All three roles should be in the default composite role so all users get them. To restrict decryption access, remove `shared-data-access` from specific users (not the voucher gate roles). Use tag `x` in all `doEncrypt`/`doDecrypt` calls.
|
|
362
|
+
|
|
363
|
+
After adding roles to a running realm, users must log out and back in for their doken to pick up the new roles.
|
|
364
|
+
|
|
365
|
+
Note: The `SimpleTagBasedDecryptionContract` is one built-in default that checks `_tide_{tag}.encrypt`/`.decrypt` roles, but the standard Forseti flow uses custom contracts where authorization is contract-defined — no per-tag `_tide_` roles needed.
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## Step 7: Store Signed Policy Bytes Server-Side
|
|
370
|
+
|
|
371
|
+
Signed policy bytes are needed by EVERY user who encrypts or decrypts. Store them on the server, not in localStorage (per-browser) or in-memory (lost on refresh).
|
|
372
|
+
|
|
373
|
+
```
|
|
374
|
+
ENCRYPT (sender):
|
|
375
|
+
1. getSignedPolicy() -> tries server, falls back to sign via enclave + store
|
|
376
|
+
2. Returns { bytes, key }
|
|
377
|
+
3. IAMService.doEncrypt(data, bytes)
|
|
378
|
+
4. POST /messages with { encryptedMessage, policyKey: key }
|
|
379
|
+
|
|
380
|
+
DECRYPT (recipient):
|
|
381
|
+
1. GET /messages/inbox -> each message has policyKey
|
|
382
|
+
2. fetchSignedPolicy(token, msg.policyKey) -> fetches from server by key
|
|
383
|
+
3. IAMService.doDecrypt(data, bytes)
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Store the policy key with each encrypted message so the recipient knows which signed policy to fetch. Different messages may use different policies (different contracts, roles, sharing groups).
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
## Group Lifecycle (CREATED -> POLICY_SETUP -> READY)
|
|
391
|
+
|
|
392
|
+
A group requires TWO signed policies before it is operational. Only a `tide-realm-admin` can sign these.
|
|
393
|
+
|
|
394
|
+
| Policy | Purpose | Without It |
|
|
395
|
+
|--------|---------|------------|
|
|
396
|
+
| Role policy (init-cert) | Enables VVK-signed UserContext creation when members are assigned | IGA can approve but ORKs will not produce a cryptographic proof |
|
|
397
|
+
| Encryption policy | Enables VVK encrypt/decrypt with scoped tag + Forseti contract | ORKs reject: `"Policy supplied has not been signed"` |
|
|
398
|
+
|
|
399
|
+
```
|
|
400
|
+
Group lifecycle states:
|
|
401
|
+
CREATED -- role exists, role policy attached, no policies signed
|
|
402
|
+
POLICY_SETUP -- admin has signed one of the two policies
|
|
403
|
+
READY -- both policies signed, group is usable
|
|
404
|
+
|
|
405
|
+
Frontend gates:
|
|
406
|
+
- "Add Member" disabled until READY
|
|
407
|
+
- "Send" / encrypt disabled until READY
|
|
408
|
+
- Admin sees setup checklist:
|
|
409
|
+
[ ] Sign role policy (init-cert) -- approval popup
|
|
410
|
+
[ ] Sign encryption policy -- approval popup
|
|
411
|
+
[x] Group is ready for use
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Signing policies lazily (on first use) causes confusing errors for non-admin users who cannot sign policies themselves. Better UX: admin completes setup, then the group is ready.
|
|
415
|
+
|
|
416
|
+
**The two policies serve completely different purposes and are signed through different APIs:**
|
|
417
|
+
|
|
418
|
+
| Policy | What It Does | How to Sign | API |
|
|
419
|
+
|--------|-------------|-------------|-----|
|
|
420
|
+
| Role policy (init-cert) | Enables VVK-signed UserContext when members are assigned roles | `POST /tide-iga-provider/role-policy/{roleId}/init-cert` | IGA admin API |
|
|
421
|
+
| Encryption policy | Enables VVK encrypt/decrypt with Forseti contract | `PolicySignRequest.New(policy)` via enclave approval popup | Direct ORK signing flow |
|
|
422
|
+
|
|
423
|
+
### Role Policy (init-cert) Signing Flow
|
|
424
|
+
|
|
425
|
+
```js
|
|
426
|
+
// 1. Build the policy for role signing (same Policy constructor)
|
|
427
|
+
const policy = new Policy({
|
|
428
|
+
version: "3",
|
|
429
|
+
contractId: contractHash,
|
|
430
|
+
modelId: ["PolicyEnabledEncryption:1", "PolicyEnabledDecryption:1"],
|
|
431
|
+
keyId: vendorId,
|
|
432
|
+
approvalType: ApprovalType.IMPLICIT,
|
|
433
|
+
executionType: ExecutionType.PRIVATE,
|
|
434
|
+
params: new Map([["Role", "shared-data-access"]]),
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// 2. Same PolicySignRequest.New -> createTideRequest -> approval popup -> executeSignRequest
|
|
438
|
+
const policyRequest = PolicySignRequest.New(policy);
|
|
439
|
+
policyRequest.addForsetiContractToUpload(contractCode);
|
|
440
|
+
policyRequest.setCustomExpiry(604800);
|
|
441
|
+
|
|
442
|
+
const initialized = BaseTideRequest.decode(
|
|
443
|
+
await tc.createTideRequest(policyRequest.encode())
|
|
444
|
+
);
|
|
445
|
+
const approvals = await tc.requestTideOperatorApproval([
|
|
446
|
+
{ id: "role-policy", request: initialized.encode() }
|
|
447
|
+
]);
|
|
448
|
+
const approved = BaseTideRequest.decode(approvals[0].request);
|
|
449
|
+
approved.addPolicy(adminPolicyBytes);
|
|
450
|
+
const signatures = await tc.executeSignRequest(approved.encode(), true);
|
|
451
|
+
|
|
452
|
+
policy.signature = signatures[0];
|
|
453
|
+
const signedPolicyData = bytesToBase64(policy.toBytes());
|
|
454
|
+
const signedPolicySignature = bytesToBase64(signatures[0]);
|
|
455
|
+
|
|
456
|
+
// 3. DIFFERENCE: send to TideCloak's init-cert endpoint (not stored locally)
|
|
457
|
+
// Note: path is tide-iga-provider, not tide-admin. No doken field — pure storage endpoint.
|
|
458
|
+
await fetch(`/admin/realms/${realm}/tide-iga-provider/role-policy/${roleId}/init-cert`, {
|
|
459
|
+
method: "POST",
|
|
460
|
+
headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
|
|
461
|
+
body: JSON.stringify({
|
|
462
|
+
initCert: signedPolicyData,
|
|
463
|
+
initCertSig: signedPolicySignature
|
|
464
|
+
})
|
|
465
|
+
});
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### Encryption Policy Signing Flow
|
|
469
|
+
|
|
470
|
+
Same steps 1-2 as above, but in step 3 store the signed bytes on YOUR server:
|
|
471
|
+
|
|
472
|
+
```js
|
|
473
|
+
policy.signature = signatures[0];
|
|
474
|
+
const signedBytes = policy.toBytes();
|
|
475
|
+
await storeSignedPolicy(policyKey, Array.from(signedBytes));
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
**What the init script can do (server-side, no browser):**
|
|
479
|
+
- Attach Forseti policy to a role: `PUT /tide-admin/ssh-policies`
|
|
480
|
+
|
|
481
|
+
These are prep steps. None produce VVK-signed bytes.
|
|
482
|
+
|
|
483
|
+
**What the frontend must do (requires admin's browser enclave):**
|
|
484
|
+
- Sign both policies via the `PolicySignRequest` flow above.
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
## The Complete Flow for Scoped Shared Encryption
|
|
489
|
+
|
|
490
|
+
```
|
|
491
|
+
1. ADMIN SETUP (one-time per group):
|
|
492
|
+
- Backend creates role: group-member:{groupId}
|
|
493
|
+
- Backend attaches Forseti policy to role: PUT /tide-admin/ssh-policies
|
|
494
|
+
- Frontend: admin signs role policy via approval popup (init-cert stored)
|
|
495
|
+
- Frontend: admin signs encryption policy via approval popup (stored on server)
|
|
496
|
+
- Both policies signed -- group transitions to READY
|
|
497
|
+
|
|
498
|
+
2. USER ASSIGNMENT (per user, per scope -- MANUAL APPROVAL REQUIRED):
|
|
499
|
+
- Backend creates IGA draft (assigns role to user)
|
|
500
|
+
- Frontend shows pending change request to group admin
|
|
501
|
+
- Admin clicks Approve -- approval enclave popup -- signs UserContext
|
|
502
|
+
- Frontend submits signed result -- backend commits
|
|
503
|
+
- User refreshes token -- doken now includes group-member:{groupId}
|
|
504
|
+
|
|
505
|
+
3. ENCRYPT (sender):
|
|
506
|
+
- Fetch signed encryption policy from server
|
|
507
|
+
- IAMService.doEncrypt([{ data, tags: ["group:{groupId}"] }], signedPolicy)
|
|
508
|
+
- ORKs verify: sender's doken has group-member:{groupId} in AccessProof
|
|
509
|
+
- VVK encrypts with scoped tag
|
|
510
|
+
|
|
511
|
+
4. DECRYPT (recipient):
|
|
512
|
+
- Fetch signed policy from server (by policyKey stored with message)
|
|
513
|
+
- IAMService.doDecrypt([{ encrypted, tags: ["group:{groupId}"] }], signedPolicy)
|
|
514
|
+
- ORKs verify: recipient's doken has group-member:{groupId} in AccessProof
|
|
515
|
+
- VVK decrypts -- plaintext only in recipient's browser
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
**Why this prevents unauthorized access:**
|
|
519
|
+
- Tags without a matching UserContext are rejected by the ORKs
|
|
520
|
+
- UserContexts are VVK-signed and bound to a specific user's `tideUserKey`
|
|
521
|
+
- A user cannot forge or reuse another user's UserContext
|
|
522
|
+
- Role assignment goes through IGA governance with manual approval enclave signing
|
|
523
|
+
- Backend cannot bypass approval -- `requiresApprovalPopup: true` is enforced
|
|
524
|
+
|
|
525
|
+
---
|
|
526
|
+
|
|
527
|
+
## IGA Role Assignment Flow (Manual Approval Required)
|
|
528
|
+
|
|
529
|
+
Role assignments that produce VVK-signed UserContexts MUST be manually approved through the approval enclave in the admin's browser. You cannot automate this from a backend script.
|
|
530
|
+
|
|
531
|
+
What happens when you try to auto-approve:
|
|
532
|
+
|
|
533
|
+
```
|
|
534
|
+
POST /tide-admin/change-set/sign
|
|
535
|
+
Response: { "requiresApprovalPopup": true, "changeSetDraftRequests": "base64..." }
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
The `requiresApprovalPopup: true` response means the backend CANNOT complete the signing. The VVK-signed UserContext binds a specific user's `tideUserKey` to specific roles. This signing must happen through the ORK threshold network via the Tide enclave running in the admin's browser.
|
|
539
|
+
|
|
540
|
+
**Always use `/batch` endpoints for approval:**
|
|
541
|
+
|
|
542
|
+
```js
|
|
543
|
+
// 1. Backend creates IGA draft (role assignment)
|
|
544
|
+
POST /admin/realms/{realm}/users/{userId}/role-mappings/realm
|
|
545
|
+
|
|
546
|
+
// 2. Frontend fetches pending requests
|
|
547
|
+
GET /admin/realms/{realm}/tide-admin/change-set/users/requests
|
|
548
|
+
|
|
549
|
+
// 3. Frontend calls sign/batch (one call for all selected)
|
|
550
|
+
POST /admin/realms/{realm}/tide-admin/change-set/sign/batch
|
|
551
|
+
{ changeSets: [{ changeSetId, changeSetType, actionType, policyRoleId? }] }
|
|
552
|
+
|
|
553
|
+
// 4. Collect all that need enclave, pass to ONE approval popup
|
|
554
|
+
const enclaveRequests = results
|
|
555
|
+
.filter(r => r.requiresApprovalPopup)
|
|
556
|
+
.map(r => ({ id: r.changesetId, request: base64ToBytes(r.changeSetDraftRequests) }));
|
|
557
|
+
const approvals = await tc.requestTideOperatorApproval(enclaveRequests);
|
|
558
|
+
|
|
559
|
+
// 5. Submit each signed review via add-review (FormData, NOT JSON)
|
|
560
|
+
POST /admin/realms/{realm}/tideAdminResources/add-review
|
|
561
|
+
|
|
562
|
+
// 6. Commit all in batch
|
|
563
|
+
POST /admin/realms/{realm}/tide-admin/change-set/commit/batch
|
|
564
|
+
{ changeSets: [{ changeSetId, changeSetType, actionType }] }
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
**The add-review endpoint uses FormData, not JSON:**
|
|
568
|
+
|
|
569
|
+
```js
|
|
570
|
+
const formData = new FormData();
|
|
571
|
+
formData.append("changeSetId", id);
|
|
572
|
+
formData.append("actionType", actionType);
|
|
573
|
+
formData.append("changeSetType", changeSetType);
|
|
574
|
+
formData.append("requests", btoa(String.fromCharCode(...signedBytes)));
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
After commit: the user must refresh their token (or re-login) for the new role to appear in their doken. Use `IAMService.forceUpdateToken()` or `useTideCloak().forceRefreshToken()`.
|
|
578
|
+
|
|
579
|
+
---
|
|
580
|
+
|
|
581
|
+
## CRITICAL: Import Paths and PolicySignRequest
|
|
582
|
+
|
|
583
|
+
These imports are the #1 source of policy-signing failures. Get them wrong and the code crashes before reaching the ORK.
|
|
584
|
+
|
|
585
|
+
**Package-boundary rule**: `@tidecloak/nextjs` is the Next.js hooks/provider layer. It does NOT export `Models`, `PolicySignRequest`, `BaseTideRequest`, `ApprovalType`, or `ExecutionType`. Importing them from `@tidecloak/nextjs` returns `undefined`, producing `Cannot destructure property 'Policy' of 'Models' as it is undefined`. Import `Models` from `@tideorg/js` and `PolicySignRequest` from `heimdall-tide`.
|
|
586
|
+
|
|
587
|
+
**Correct imports:**
|
|
588
|
+
|
|
589
|
+
```js
|
|
590
|
+
// Models: from @tideorg/js (NOT @tidecloak/nextjs)
|
|
591
|
+
const { Models } = await import("@tideorg/js");
|
|
592
|
+
const { Policy, ApprovalType, ExecutionType, BaseTideRequest } = Models;
|
|
593
|
+
// Models.Policy is the Policy CLASS, not a namespace
|
|
594
|
+
// ApprovalType, ExecutionType are direct exports from Models alongside Policy
|
|
595
|
+
// NOT: const { ApprovalType } = Models.Policy; // WRONG -- Policy is a class, not a namespace
|
|
596
|
+
|
|
597
|
+
// PolicySignRequest: from heimdall-tide (NOT @tidecloak/nextjs)
|
|
598
|
+
const { PolicySignRequest } = await import("heimdall-tide");
|
|
599
|
+
const policyRequest = PolicySignRequest.New(policy);
|
|
600
|
+
policyRequest.addForsetiContractToUpload(CONTRACT_SOURCE);
|
|
601
|
+
policyRequest.setCustomExpiry(604800); // 1 week
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
Do NOT try to build the policy signing request manually with `BaseTideRequest` + `TideMemory`. The ORK's `PolicySignRequestFactory` expects a specific internal structure that `PolicySignRequest.New(policy)` produces correctly. Building this manually produces different byte layouts that the ORK cannot parse.
|
|
605
|
+
|
|
606
|
+
**After createTideRequest, decode the result:**
|
|
607
|
+
|
|
608
|
+
```js
|
|
609
|
+
// createTideRequest returns raw bytes. Decode back to BaseTideRequest
|
|
610
|
+
// before passing to approval. Skipping this causes serialization errors.
|
|
611
|
+
const initializedBytes = await tc.createTideRequest(policyRequest.encode());
|
|
612
|
+
const initializedRequest = BaseTideRequest.decode(initializedBytes);
|
|
613
|
+
|
|
614
|
+
// Pass the re-encoded request to approval
|
|
615
|
+
const approvalResults = await tc.requestTideOperatorApproval([
|
|
616
|
+
{ id: "policy-sign", request: initializedRequest.encode() },
|
|
617
|
+
]);
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
### SDK Internals (Reference)
|
|
621
|
+
|
|
622
|
+
- `@tidecloak/js` exports: `IAMService`, `TideCloak`, `RequestEnclave`, `ApprovalEnclaveNew`, `Tools`, `AdminAPI`
|
|
623
|
+
- `@tideorg/js` exports: `Models` (`Policy`, `BaseTideRequest`, `ApprovalType`, `ExecutionType`), `Contracts`
|
|
624
|
+
- `heimdall-tide` exports: `PolicySignRequest`
|
|
625
|
+
- `Tools` includes: `TideMemory` (Tide's serialization format)
|
|
626
|
+
- `IAMService._tc` gives access to the underlying `TideCloak` instance for `createTideRequest()` and `executeSignRequest()`
|
|
627
|
+
- `RequestEnclave.encrypt(data, policy)` switches flow: `"encrypt"` (self) vs `"policy encrypt"` (VVK) based on whether policy is provided
|
|
628
|
+
- Model IDs must match exactly what the ORK registry expects. Check `ModelRegistry.js` in `@tideorg/js/dist/Models/` for the correct `_name` and `_version`. Policy signing is `"Policy:1"` (not `"PolicySign:1"`).
|
|
629
|
+
|
|
630
|
+
---
|
|
631
|
+
|
|
632
|
+
## Common Mistakes That Cause Index out of range / PreSign Failures
|
|
633
|
+
|
|
634
|
+
1. Destructuring `{ ApprovalType, ExecutionType }` from `Models.Policy` instead of `Models` (Models.Policy is the class, not a namespace)
|
|
635
|
+
2. Building the request manually instead of using `PolicySignRequest.New(policy)`
|
|
636
|
+
3. Not decoding `createTideRequest` result via `BaseTideRequest.decode()` before passing to approval
|
|
637
|
+
4. Calling `addPolicy(policy.toBytes())` on the request during construction (attaching unsigned policy)
|
|
638
|
+
5. Using wrong model ID (e.g. `"PolicySign:1"` instead of `"Policy:1"`)
|
|
639
|
+
6. Using `modelId: "any"` instead of `["PolicyEnabledEncryption:1", "PolicyEnabledDecryption:1"]` in Policy constructor
|
|
640
|
+
7. Not using `PolicySignRequest.New(policy)` from `heimdall-tide` to construct the request
|
|
641
|
+
|
|
642
|
+
**If you get `Index out of range` in `Policy.From` during `PreSign`:** Do NOT try to debug the byte serialization. Instead, copy the exact working pattern from the forseti-crypto-quickstart reference implementation:
|
|
643
|
+
|
|
644
|
+
1. Copy `~/forseti-crypto-quickstart/template-ts-app/components/AuthProvider.tsx` for the `initializeTideRequest`, `approveTideRequests`, `executeTideRequest` wrapper functions
|
|
645
|
+
2. Copy `~/forseti-crypto-quickstart/template-ts-app/app/home/page.tsx` lines 740-830 for the policy creation, initialization, approval, and commit flow
|
|
646
|
+
3. Copy `~/forseti-crypto-quickstart/template-ts-app/lib/forsetiContract.ts` for the contract source and `computeContractId` function
|
|
647
|
+
|
|
648
|
+
The forseti-crypto-quickstart is the reference implementation that works against the live ORK network. If your code diverges from this pattern in any way (different import paths, different function call order, different parameter passing), the ORK byte serialization will fail. Do not improvise.
|
|
649
|
+
|
|
650
|
+
---
|
|
651
|
+
|
|
652
|
+
## Verification Checklist
|
|
653
|
+
|
|
654
|
+
- [ ] `contractId` (SHA-512 of contract source) matches what is passed to `PolicySignRequest.addForsetiContractToUpload`
|
|
655
|
+
- [ ] Policy constructed with `ApprovalType.IMPLICIT` and `ExecutionType.PRIVATE` from `Models` (not `Models.Policy`)
|
|
656
|
+
- [ ] `modelId` is `["PolicyEnabledEncryption:1", "PolicyEnabledDecryption:1"]` (array, not string)
|
|
657
|
+
- [ ] `PolicySignRequest` imported from `heimdall-tide`
|
|
658
|
+
- [ ] `BaseTideRequest.decode()` called on `createTideRequest` result before approval
|
|
659
|
+
- [ ] Admin policy fetched from `GET /realms/{realm}/tide-policy-resources/admin-policy` (not the admin API endpoint)
|
|
660
|
+
- [ ] Admin policy bytes are base64-decoded (not raw char codes)
|
|
661
|
+
- [ ] Approval popup opens and admin approves
|
|
662
|
+
- [ ] `executeSignRequest` called with `waitForAll = true`
|
|
663
|
+
- [ ] `policy.signature = signatures[0]` assigned before `policy.toBytes()`
|
|
664
|
+
- [ ] Signed policy bytes stored server-side (not localStorage)
|
|
665
|
+
- [ ] Voucher gate roles assigned to users (generic `_tide_x.selfencrypt`/`_tide_x.selfdecrypt` for policy-based, or per-tag for self-encryption)
|
|
666
|
+
- [ ] Forseti contract role (`shared-data-access`) assigned to users
|
|
667
|
+
- [ ] Users logged out and back in after role changes
|
|
668
|
+
- [ ] `IAMService.doEncrypt(data, signedPolicyBytes)` succeeds
|
|
669
|
+
- [ ] `IAMService.doDecrypt(data, signedPolicyBytes)` succeeds for authorized user
|
|
670
|
+
- [ ] Decrypt fails for user without the Forseti contract role
|
|
671
|
+
- [ ] Group lifecycle: both role policy and encryption policy signed before marking READY
|
|
672
|
+
|
|
673
|
+
---
|
|
674
|
+
|
|
675
|
+
## Common Failures
|
|
676
|
+
|
|
677
|
+
### "Policy supplied has not been signed"
|
|
678
|
+
|
|
679
|
+
**Cause:** Using raw `policy.toBytes()` without completing the 5-step signing flow, or only completing some steps.
|
|
680
|
+
**Fix:** Complete all 5 steps. Verify `policy.signature = signatures[0]` is called before `policy.toBytes()`.
|
|
681
|
+
|
|
682
|
+
### "The type or namespace name 'IAccessPolicy' could not be found"
|
|
683
|
+
|
|
684
|
+
**Cause:** Missing `using Ork.Forseti.Sdk;` in the contract source.
|
|
685
|
+
**Fix:** Add the `using` directive as the first line of the contract.
|
|
686
|
+
|
|
687
|
+
### "Policy refers to wrong contract"
|
|
688
|
+
|
|
689
|
+
**Cause:** `contractId` (SHA-512 hash) does not match the deployed contract source. Common reasons: whitespace or `using` differences between local and deployed copies, OR the hash is lowercase but the ORK stores it uppercase. The ORK comparison is **case-sensitive**.
|
|
690
|
+
**Fix:** Fetch `contractHash` from `GET /tide-admin/forseti-contracts` instead of computing locally. If computing locally, the hex string MUST be uppercase (`.toUpperCase()`).
|
|
691
|
+
|
|
692
|
+
### `Index out of range` in `Policy.From` during `PreSign`
|
|
693
|
+
|
|
694
|
+
**Cause:** One of the 7 common mistakes listed above. Most likely: wrong import path for `ApprovalType`/`ExecutionType`, or not using `PolicySignRequest.New()`.
|
|
695
|
+
**Fix:** Copy the exact pattern from forseti-crypto-quickstart. Do not debug byte serialization.
|
|
696
|
+
|
|
697
|
+
### "session ID ... has not been allowed a voucher action of type vendordecrypt"
|
|
698
|
+
|
|
699
|
+
**Cause:** User is missing a `_tide_*` voucher gate role that matches the tag used in encrypt/decrypt calls.
|
|
700
|
+
**Fix:** For self-encryption, assign `_tide_{tag}.selfdecrypt`. For policy-based encryption, assign a generic voucher gate (e.g., `_tide_x.selfdecrypt` with tag `x`). User must re-login after role assignment.
|
|
701
|
+
|
|
702
|
+
### `{ status: "none" }` when fetching admin policy
|
|
703
|
+
|
|
704
|
+
**Cause:** Using the wrong endpoint `GET /admin/realms/{realm}/tide-admin/realm-policy`.
|
|
705
|
+
**Fix:** Use `GET /realms/{realm}/tide-policy-resources/admin-policy`.
|
|
706
|
+
|
|
707
|
+
### Admin policy bytes start with `[65, 81, 65, 65, ...]`
|
|
708
|
+
|
|
709
|
+
**Cause:** Passing base64 text as raw byte values instead of decoding.
|
|
710
|
+
**Fix:** `Buffer.from(b64, "base64")` on the backend before sending to frontend.
|
|
711
|
+
|
|
712
|
+
### `requiresApprovalPopup: true` from backend role assignment
|
|
713
|
+
|
|
714
|
+
**Cause:** Trying to auto-approve IGA role assignments from backend code.
|
|
715
|
+
**Fix:** IGA role assignments must go through the approval enclave in the admin's browser. Use the batch endpoint flow described in the IGA Role Assignment section.
|
|
716
|
+
|
|
717
|
+
### Decrypt fails silently after adding roles
|
|
718
|
+
|
|
719
|
+
**Cause:** User's doken does not reflect the new roles yet.
|
|
720
|
+
**Fix:** Call `IAMService.forceUpdateToken()` or have the user re-login after role changes.
|
|
721
|
+
|
|
722
|
+
---
|
|
723
|
+
|
|
724
|
+
## Do Not Do This
|
|
725
|
+
|
|
726
|
+
### Do Not Mix Self-Encryption with Policy-Governed Encryption
|
|
727
|
+
|
|
728
|
+
```js
|
|
729
|
+
// WRONG: using doEncrypt from useTideCloak() for shared data
|
|
730
|
+
const { doEncrypt } = useTideCloak();
|
|
731
|
+
const encrypted = await doEncrypt([{ data: "shared", tags: ["shared"] }]);
|
|
732
|
+
// This uses self-encryption. Only you can decrypt. No policy, no sharing.
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
Use `IAMService.doEncrypt(data, signedPolicyBytes)` for shared data.
|
|
736
|
+
|
|
737
|
+
### Do Not Skip Voucher Gate Roles
|
|
738
|
+
|
|
739
|
+
Even with a valid Forseti contract role, the ORK refuses to participate without `_tide_*` voucher gate roles. Both role types are required. For policy-based encryption, a generic voucher gate (e.g., `_tide_x.selfencrypt`/`_tide_x.selfdecrypt`) is sufficient — you do not need per-tag `_tide_{tag}.encrypt`/`.decrypt` roles.
|
|
740
|
+
|
|
741
|
+
### Do Not Sign Policies Lazily
|
|
742
|
+
|
|
743
|
+
Signing policies on first use causes confusing errors for non-admin users. Sign both policies during admin setup before marking the group as ready.
|
|
744
|
+
|
|
745
|
+
### Do Not Store Unsigned Policy Bytes
|
|
746
|
+
|
|
747
|
+
If a signing attempt fails partway through and you store the result, all subsequent encrypt/decrypt calls silently fail. Only store bytes after verifying the full 5-step flow completed.
|
|
748
|
+
|
|
749
|
+
---
|
|
750
|
+
|
|
751
|
+
## References
|
|
752
|
+
|
|
753
|
+
- [add-rbac-nextjs.md](add-rbac-nextjs.md) - Role-based access control
|
|
754
|
+
- [protect-api-nextjs.md](protect-api-nextjs.md) - Server-side JWT verification
|
|
755
|
+
- Source: `~/forseti-crypto-quickstart/template-ts-app/` - Reference implementation (simplest working example)
|
|
756
|
+
- Source: `~/tidecloak-test-cases/test-app/src/lib/database/policyDb.ts` - EXPLICIT + PRIVATE multi-party approval reference
|