Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/docs/Post Platform Guide/signing-for-a-company.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ Rules your tool should follow (verifiers ignore records that break them):
- **Role:** signed by a director, and `createdBy` must be that director.
- **Delegation from a role:** signed by a director, with `grantedBy` set to that director and scopes inside the role's.
- **Re-delegation:** signed by the parent's delegate, the parent must allow re-delegation, and scopes must be inside the parent's.
- **Revoking:** write a new signed version with `status: "revoked"` and `revokedBy` set to the signer (a director or the grantor). Revoking a parent fails everything below it without touching those records, but you may also write signed revocations for them so the data reads clearly.
- **Revoking:** write a new signed version with `status: "revoked"` and `revokedBy` set to the signer (a director, or a grantor who hasn't been revoked). By default this stops only that person: what they granted before stays valid. Set `revocationCascade: true` to also revoke everything handed on from them. Narrowing a delegation or role always narrows everything below it.
- **Never delete** these records. Deletion doesn't revoke anything, because verifiers read history.

## Logins
Expand Down
50 changes: 36 additions & 14 deletions docs/docs/W3DS Protocol/Company-Delegation.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ Bob signs an NDA "for Acme" with **his own** wallet key. The signature names Acm
- Bob's delegation was signed by someone entitled to grant it,
- that grant goes back, hop by hop, to a role created by one of Acme's directors,
- the directors are the ones Acme's Company record was born with, or later changed to by a sitting director,
- nothing on the way has been revoked or widened.
- nothing on the way was revoked before granting what is below it (or revoked with cascade),
- what was signed is within every link's **current** scopes.

**The eVault enforces none of this.** It stores whatever it is given, and all authority is decided by the verifier from signatures and the eVault's version history.

Expand Down Expand Up @@ -63,7 +64,7 @@ Exactly one of `roleId` or `parentDelegationId` is set.
- **From a role:** granted (signed) by a director. `scopes` must be a subset of the role's.
- **From a parent delegation:** granted by the parent's delegate, only if the parent has `mayRedelegate: true`. `scopes` must be a subset of the parent's, and the validity window must lie within the parent's.
- `grantedBy` is the signer. Immutable after creation: `companyEName`, `delegateEName`, `roleId`, `parentDelegationId`, `grantedBy`.
- **Revoking:** a director or the grantor writes a signed version with `status: "revoked"` and `revokedBy` set to themselves. Revocation is final.
- **Revoking:** a director, or a grantor whose own authority hasn't been revoked, writes a signed version with `status: "revoked"` and `revokedBy` set to themselves. Revocation is final. See [Firing and cascading](#firing-and-cascading).

## Scopes

Expand Down Expand Up @@ -130,29 +131,50 @@ Every payload in this model starts with `w3ds-`. Login verifiers (`verifyLoginSi
- is validly signed and bound to its id,
- is not dated before the previous valid version, and not after the eVault stored it (5 minutes of clock skew allowed),
- keeps its immutable fields,
- was signed by someone entitled **when it was stored**: a director for roles and role grants, the parent's delegate for re-delegations, a director or the grantor for revocations.
- was signed by someone entitled **when it was stored**:
- a director, for roles and for grants from a role that wasn't yet revoked,
- the delegate of a parent that wasn't yet revoked, for re-delegations,
- a director, or a grantor whose own authority wasn't yet revoked, for revocations.

Unsigned, forged, copied or written-back versions are skipped as if absent. Grants made by a director remain valid after that director leaves the board.
5. Walk the chain to the role (at most 16 links). Every link must be:
- active and within its validity window,
- for the same company,
- narrowing its parent,
- granted by the parent's delegate,
- from a parent that allows re-delegation.
6. Check the payload against the chain:

Each resolved record also carries the eVault's storage times of its first valid grant and of its revocation. Those times decide firing, not dates the signer wrote.
5. Walk the chain to the role (at most 16 links):
- The signer's own delegation must be active and within its validity window.
- Every link above must be for the same company, granted by its parent's delegate, and from a parent that allows re-delegation.
- A revoked link above still counts **if it was revoked after granting what is below it, without cascading** (see below).
6. Effective scopes are the **intersection of every link's current scopes**, role included.
7. Check the payload against the chain:
- the right company, signer and delegation,
- the scope is in the chain's scopes,
- the scope is in the effective scopes,
- the scope is not core.

**Revocation cascades implicitly.** Revoking or narrowing a role or delegation fails the chain of everything handed on from it at the next verification. Nothing else needs to be written.
### Firing and cascading

- **Revoking a person stops only them.** If Bob is fired, Bob can't sign any more. But a delegation Bob granted to Dave **before** he was fired stays valid, and so does everything Dave handed on. "Before" means the eVault stored Dave's grant before it stored Bob's revocation.
- **Revoking a role** works the same way. Holders granted before the revocation keep their delegations, and no new grants can be made from the role.
- **Cascading is explicit.** A revocation with `revocationCascade: true` also revokes everything handed on from that record. One signed record does it; the children are not touched.
- **Anything signed after the revocation doesn't count.** A fired Bob can't grant, update or revoke anything.
- **Holders below a fired link can keep handing on.** Dave can still re-delegate, if his own record allows it.
- **A grant can never be wider than its source was when it was made.** If Bob holds only NDAs when he grants Dave NDAs and invoices, that grant never counts, even if Bob is widened later.
- **Narrowing always follows the parent now.** If Bob's delegation (or the role) loses invoices, everyone below Bob loses invoices at the next verification, whatever they were originally given.

```mermaid
flowchart LR
A{"parent revoked?"} -- no --> OK["counts"]
A -- yes --> B{"revocationCascade?"}
B -- yes --> X1["✗ REVOKED"]
B -- no --> C{"child granted before<br/>the revocation?"}
C -- yes --> OK2["counts"]
C -- no --> X2["✗ REVOKED"]
```

### Why a trace fails

| Code | Meaning |
|---|---|
| `NOT_FOUND` | No valid version: never granted by anyone entitled, or copied from another record |
| `REVOKED` | This link, or one above it, was revoked |
| `NOT_A_SUBSET` | Scopes wider than the parent's or role's |
| `REVOKED` | The signer was revoked, or a link above was revoked before granting what is below it, or revoked with cascade |
| `REDELEGATION_NOT_ALLOWED` | The parent (or role) did not allow passing it on |
| `WRONG_GRANTOR` | Not granted by the parent's delegate |
| `EXPIRED` / `NOT_YET_VALID` | Outside its validity window |
Expand Down
5 changes: 3 additions & 2 deletions examples/company-delegation-demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ Dana founds Acme and makes Bob Head of Finance; Bob passes it to Dave, and Dave
| Tim passes his badge to Mallory | refused at Mallory: Tim couldn't pass it on |
| Tim's signature replayed as a login | refused: reserved payload |
| Mallory writes herself onto the board | refused: nobody entitled granted it |
| Dana revokes Bob, Tim signs again | refused at Bob: revoked |
| Dana fires Bob, then Bob and Tim sign | Bob refused; Tim valid: Bob was fired after granting |
| Dana revokes Dave and everything he handed on | refused at Dave: revoked with cascade |

**Start fresh** (slide 0) clears Acme and Act 1 provisions a fresh company eVault; on the dashboard, **Start over with a new Acme** does both. Wallets are reused. Revoking Bob is permanent for that Acme, so the acts that need Tim's chain lock until you start fresh.
**Start fresh** (slide 0) clears Acme and Act 1 provisions a fresh company eVault; on the dashboard, **Start over with a new Acme** does both. Wallets are reused. Acts heal themselves: if Acme is missing, or Dave was already revoked with cascade, playing an act founds a fresh Acme first.

## Headless

Expand Down
6 changes: 3 additions & 3 deletions examples/company-delegation-demo/public/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ <h2>What happened</h2>
<script>
const EXPECT = {
"tim-nda": "valid", "tim-invoice": "refused", "tim-passes-on": "refused",
"login-replay": "refused", "mallory-board": "refused", "revoke-bob": "refused",
"login-replay": "refused", "mallory-board": "refused", "revoke-bob": "valid", "revoke-dave": "refused",
};
const results = {};
const $ = (id) => document.getElementById(id);
Expand Down Expand Up @@ -138,8 +138,8 @@ <h2>What happened</h2>
$("scenarios").innerHTML = state.scenarios.map((s) => {
const r = results[s.id];
const badge = r ? `<span class="tag ${r.passed ? "ok" : "bad"}">${r.passed ? "as expected" : "unexpected"}</span>` : "";
const locked = ["tim-nda", "tim-invoice", "tim-passes-on"].includes(s.id) && !!results["revoke-bob"];
return `<div><button data-id="${s.id}" ${locked ? "title='Bob is revoked: plays on a fresh Acme'" : ""}>${esc(s.label)} <span class="tag">expect ${EXPECT[s.id]}</span></button>
const locked = ["tim-nda", "tim-invoice", "tim-passes-on", "revoke-bob", "revoke-dave"].includes(s.id) && !!results["revoke-dave"];
return `<div><button data-id="${s.id}" ${locked ? "title='Dave is revoked: plays on a fresh Acme'" : ""}>${esc(s.label)} <span class="tag">expect ${EXPECT[s.id]}</span></button>
${r ? `<div class="result">${badge} ${esc(r.summary)}</div>` : ""}</div>`;
}).join("");
$("scenarios").querySelectorAll("button").forEach((b) => b.addEventListener("click", () => run(b.dataset.id)));
Expand Down
46 changes: 35 additions & 11 deletions examples/company-delegation-demo/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ <h1>Who signs for Acme? <span style="font-weight:400;color:var(--muted)">· comp
const d = start + i * BEAT;
lines.push({ text: `${i + 1}. ${s.text}${s.ok === true ? " ✓" : s.ok === false ? " ✗" : ""}`, cls: s.ok === false ? "bad" : s.ok === true ? "ok" : "muted", d });
if (s.ok === false && s.reason) lines.push({ text: ` ↳ ${s.reason}`, cls: "bad", d: d + .2 });
if (s.note) lines.push({ text: ` ↳ ${s.note}`, cls: "", d: d + .2 });
});
body += carried("paper", c.paper, [POS[c.signer] + 20, 330], LAND, .2);
// ticks and crosses land on the people the trace passes through
Expand Down Expand Up @@ -530,26 +531,49 @@ <h1>Who signs for Acme? <span style="font-weight:400;color:var(--muted)">· comp
},
{
kicker: "Act 10", id: "revoke-bob",
title: "Dana revokes Bob",
text: `Dana signs a revocation of Bob's delegation. Nobody touches Dave's or Tim's records, yet their authority came <b>through</b> Bob's. Tim signs again, and the trace breaks at Bob.`,
title: "Dana fires Bob",
text: `Dana signs a revocation of Bob's delegation, without cascading. Bob can't sign for Acme any more. But Dave's grant was signed by Bob <b>while he was still Head of Finance</b>, so it stands, and so does Tim's. Firing someone stops them, not the work they already handed on.`,
scene: (o) => {
const done = !!o && busy !== "revoke-bob";
return traceScene(o, {
signer: "tim", cast: ["dave", "bob", "dana"], paper: "NDA",
say: done ? "Can I still sign?" : "Another NDA for Acme.",
before: done ? say("dana", "Bob is out.", .2) : carried("paper", "revoke Bob", [POS.dana + 20, 330], CAB_DROP, .3),
before: done
? say("dana", "Bob, you're fired.", .2) + say("bob", "I can't sign for Acme any more.", .6)
: carried("paper", "fire Bob", [POS.dana + 20, 330], CAB_DROP, .3),
held: done
? [["bob", "revoked", true, .4], ["dave", "falls with it", true, .7], ["tim", "falls with it", true, 1.0]]
? [["bob", "fired", true, .4], ["dave", "Finance officer"], ["tim", "NDA signer"]]
: [["bob", "Head of Finance"], ["dave", "Finance officer"], ["tim", "NDA signer"]],
bad: "The chain breaks at Bob. Refused.",
badCaption: "Revoke one link and everything handed on from it falls too.",
ok: "Bob's out, but Dave's grant came first. Tim may sign.",
okCaption: "Firing Bob stops Bob. What he handed on while valid still stands.",
bad: "Refused.", badCaption: "Refused.",
});
},
},
{
kicker: "Act 11", id: "revoke-dave",
title: "Dana revokes Dave, and everything he handed on",
text: `Sometimes the company wants it all gone, for example if Dave misused it. Dana revokes Dave's delegation <b>with cascade</b>: one signed record that also revokes everything Dave handed on. Nobody touches Tim's record, yet his trace now breaks at Dave.`,
scene: (o) => {
const done = !!o && busy !== "revoke-dave";
return traceScene(o, {
signer: "tim", cast: ["dave", "bob", "dana"], paper: "NDA",
say: done ? "Can I still sign?" : "Another NDA for Acme.",
before: done
? say("dana", "Dave, and everyone under you: out.", .2)
: carried("paper", "revoke Dave + all below", [POS.dana + 20, 330], CAB_DROP, .3),
held: done
? [["dave", "revoked + cascade", true, .4], ["tim", "falls with it", true, .8]]
: [["dave", "Finance officer"], ["tim", "NDA signer"]],
bad: "Dave's revocation took everything he handed on. Refused.",
badCaption: "With cascade, one revocation takes everything handed on below it.",
});
},
},
{
kicker: "Curtain",
title: "What just happened",
text: `Acme's eVault stored everything, Mallory's and Tim's bad grants included, and enforced nothing. Every outcome came from tracing signatures back through Acme's history to the board.`,
text: `Acme's eVault stored everything, Mallory's and Tim's bad grants included, and enforced nothing. Every outcome came from tracing signatures back through Acme's history to the board. Firing someone stopped only them; a cascading revocation took everything below.`,
recap: true,
scene: () => world([...PEOPLE, "acme", "laptop"], {
cabinet: cabinetLines(),
Expand All @@ -563,9 +587,9 @@ <h1>Who signs for Acme? <span style="font-weight:400;color:var(--muted)">· comp
const params = new URLSearchParams(location.search);
let index = clamp(Number(params.get("slide") || 0), 0, slides.length - 1);
const current = () => slides[index];
/* Revoking Bob breaks Tim's chain for good on this Acme. */
const NEEDS_BOB = ["tim-nda", "tim-invoice", "tim-passes-on"];
const locked = (id) => NEEDS_BOB.includes(id) && !!outcomes()["revoke-bob"];
/* Revoking Dave with cascade breaks Tim's chain for good on this Acme. */
const NEEDS_DAVE = ["tim-nda", "tim-invoice", "tim-passes-on", "revoke-bob", "revoke-dave"];
const locked = (id) => NEEDS_DAVE.includes(id) && !!outcomes()["revoke-dave"];

function render() {
const s = current();
Expand All @@ -588,7 +612,7 @@ <h1>Who signs for Acme? <span style="font-weight:400;color:var(--muted)">· comp
} else if (s.id) {
controls += `<button class="go" id="act" ${busy ? "disabled" : ""}>${busy === s.id ? "Signing and verifying…" : o ? "Play it again" : "Play this scene"}</button>`;
if (!ready()) controls += `<span class="note">No Acme yet: playing this founds one first.</span>`;
else if (locked(s.id)) controls += `<span class="note">Bob was revoked: playing this founds a fresh Acme first.</span>`;
else if (locked(s.id)) controls += `<span class="note">Dave was revoked with cascade: playing this founds a fresh Acme first.</span>`;
}
controls += `<div class="nav"><button id="back" ${index === 0 ? "disabled" : ""}>← Back</button><button id="next" ${index === slides.length - 1 ? "disabled" : ""}>Next →</button></div>`;
document.getElementById("controls").innerHTML = controls;
Expand Down
Loading
Loading