aboutsummaryrefslogtreecommitdiffstats
path: root/docs
Commit message (Collapse)AuthorAgeFilesLines
* screen: what a node receives judged, policy or nottv3 days1-5/+6
| | | | | | | | | | | | | | | | | | | | | Review 2, N7: a node screened received changes only while its own document had a policy it enforces. A joiner's empty document has none, so its first sync, as much history as the node it joins through chose, was applied unjudged, and under signatures required unvouched; the same for any node while the policy's own change arrived with later ones. Sync messages, deltas and downloaded changes are now always screened, each change judged by the policy at its dependencies (1135), so the verdicts are the swarm's. While neither the document nor the changes hold a policy (none touches a "policy" key: inspect::touch_key), only an epoch's root is judged and nothing is placed: bench_screening, after 1188, measured placing everything at about as much again as applying (564 ms + 543 ms for 100 000 changes), judging by the rules at about five times (+2.5 s), which a document with a policy pays anyway. access.md no longer lists the joiner's first sync, nor nodes agreeing only as the policy reaches them (judged at the dependencies, they agree), as left to do; changes::load_delta is for tests now.
* store: a write inside a list checked where the rules judge ittv3 days1-1/+3
| | | | | | | | | | | | | | Review 2, N9: the API checked a write's capability on the path asked (servers/list/3/name), the rules on the list it is in (servers/list): a user whose capability lay inside a list had every such write pass the API and refused by the node's own judgement (403, after rolling the document back). inspect::placed places a write's path in the document by the rule the inspector places operations by (Place::child: cut after the first list or text on the way), and the store's writes check capabilities there. One rule for both; a test checks placed against the paths the inspector finds in the change such a write makes.
* bench: what screening costs while catching uptv3 days1-2/+2
| | | | | | | | | | | Review 2, N7, measured first: a node whose document has no policy (a joiner's empty one) applies what it receives unjudged. Screening it too would cost finding every change's paths and judging it. bench_screening takes 1 000, 10 000 and 100 000 changes into an empty document, as one delta brings them: unjudged, as Automerge applies them, against screened (access::screen::apply), with no policy in them (each passes, read at its dependencies, after its paths were found) and with the policy their first change wrote (each judged by the rules).
* strict: no strict remake of a swarm strict alreadytv3 days1-1/+3
| | | | | | | | | | | | | Review 2, N2: nothing checked that a strict remake's base wasn't strict already. In a strict swarm the registry, the policy and the bans change only as a user who may change them signs; a strict remake committed by a majority of members, each with keys of its own, could drop any of them (unban, remove managers, empty userSigned and the sealed declarations), and the honest minority would follow. A node now takes a strict remake (proposing or voting) only in a swarm not strict yet: turning strict is done once, by a remake; after it, by signatures.
* refusals: a big drop waits for a second member only where there is onetv3 days1-4/+6
| | | | | | | | | | | | Review 2, N8: a node with one neighbour (a swarm of two, a leaf) never hears a second member name the change: its big drop was held for good, an error logged at every REFUSALS. It now waits only where it has another neighbour than the one telling it; with one, one word is all there is. access.md also says what the wait is: it guards against one peer, not two members saying the same (any node with the swarm key registers), and never makes a node drop what its own rules take.
* remake: aborts written as the user who asks; no abort by timetv3 days2-8/+8
| | | | | | | | | | | | | | | | | Review 2, N3: 1150 let any member write an abort decision once a remake had expired a grace period ago, judged by the change's own time, the writer's word. A member could race the coordinator's legitimate commit with an abort dated as it liked; both taken, Automerge shows one, and nodes that acted on the commit split from those that didn't. The two writers of such aborts are operator requests whose endpoints already need write on _continuum/remakes: aborting an expired remake (DELETE /admin/remakes/{id}) and superseding one (POST /admin/remakes?force=supersede). Both now commit as the user who asks (the proposal and its vote with them, for a supersede), which the rules take as one who may write the decision. The clause judging an abort by its time goes, and with it the time the rules carried. An expired remake is still aborted through any member, by an operator.
* strict: a manifest serves one remake, at its basetv3 days1-1/+2
| | | | | | | | | | | | | | Review 2, N1: a strict manifest wasn't bound to its base. Any member could propose an old, legitimately signed one again at the current heads; nodes would verify it and vote, and every value at a signed place changed or added since (a ban, a new user, a rotated key, a changed userSigned or sealed declaration) would be deleted, nothing after the base being left to replay. The manifest now names the epoch and the heads it was made at, and a node takes it (proposing or voting) only for a remake of that epoch with exactly that base. GET /admin/strict-manifest makes it so; continuum-signed strict proposes at those heads already.
* docs: what a junk logout doestv3 days1-1/+5
| | | | | | | | | Review 2, N11: a value at _continuum/logouts/<name> that isn't a time (read as damaged: every token revoked), or one far ahead, refuses every token of the user, new ones too, until it is replaced. Any member can write it (finding 10). The rules can't see the value (only that it is written), so access.md says it, and how it is set right: the user logs out again by password, or one who manages users revokes its tokens.
* remake: no --follow-strict-remakes; a strict remake failing says whotv3 days2-11/+8
| | | | | | | | | | | | | | | | | A strict remake takes a majority of members whose --strict-keys take its manifest's signer: no node votes for one without, so --follow-strict-remakes (1173) goes. A node that doesn't vote still switches once the others commit, as with any remake. So that an operator can tell why one fails: a strict remake aborted for want of a majority (the voting period over, or a majority out of reach) says in its reason (GET /admin/remakes/{id}, and the coordinator's log) which members didn't vote, and that a member votes for one only with its manifest signed by a key of its --strict-keys. Each of those logs why itself (`Not voting for a strict remake`). The sim a_strict_remake_too_few_take: only a takes the manifest; the remake aborts, naming b and c.
* remake: turning a running swarm strict, keeping what an operator signedtv3 days4-4/+82
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Until a swarm is strict, the user a change names is its node's word, so any member could have planted registry entries, policy, bans or user-signed values as any user, plain or signed by a key it put there itself; writing the strict policy trusted all of it as it was. A remake of a new kind, `strict`, turns a swarm strict instead: * Its content is a manifest (access::strict): the swarm, the strict policy, and each value kept at the places values are signed, by the SHA-256 of its JSON, signed by an operator (ssh-keygen -Y sign -n continuum-strict). * Every node builds the next epoch as compaction does, keeping at the signed places only the values the manifest names, as they were at the base, and writing its policy (RemakeKind::protocol, the one kind that changes protocol data). Writes after the base reach the new epoch only by replays, judged by the strict policy. * A node votes for one only with its manifest signed by one of --strict-keys, keys given out of the document; without any, for none (logged once, `Not voting for a strict remake`), unless --follow-strict-remakes. A coordinator proposes only what it would vote for (403). Once a majority commits one, every node switches, as with any remake: refusing to switch would only cut a node off. * GET /admin/strict-manifest gives the manifest keeping everything held now, the values for review and the heads they are at; continuum-signed strict shows them, signs once confirmed and proposes at those heads. The sim a_swarm_turning_strict_through_a_remake: a member plants a registry entry as a manager; the operator's manifest leaves it out; a and b, given the key, commit; c, without, follows; the entry is gone and the policy strict everywhere.
* dashboard: a user's menu, with its credentials and logging outtv3 days1-3/+6
| | | | | | | | | | | | | | | | | | The user's name, top right, opens a menu: * "credentials…": the user's keys (GET /auth/keys), one added with its .pub line or removed but the last (/auth/keys/{id}); under a strict policy, how to sign them anew (continuum-signed key-add, key-remove); and where its password is: each node's --htpasswd, its operator's. * "log out": this browser's session ends. Where the page got in by what it can't end (the browser's password, the proxy's), it is greyed (aria-disabled, not disabled, so a click still lands), its tooltip saying how to log out, and a click says so in a dialog. The log out button and the text beside the name from 1165 go into it. Checked in Chromium against stubbed endpoints: the menu, both log out cases, the credentials dialog, no errors, nothing the content policy refuses.
* users: a user can't remove its own last keytv3 days1-1/+2
| | | | | | | | A user managing its own keys (1168) could remove the last, and then neither sign nor log in by key. It can't now: DELETE /auth/keys/{id} of the last answers 409, the rules take a user's change of its own keys only with one left after it, and under a strict policy its own entry signed only with one key at least. One who may manage users still may.
* auth: no basic authentication offered to the dashboardtv3 days1-1/+2
| | | | | | | | | What is meant for browsers logs in on its page (/login), with the passwords of --htpasswd; basic authentication's challenge stays for the API, so curl -u keeps working. 1164 kept it from a page's own requests; now nothing under /admin/dashboard is challenged either, whatever the request: a browser opening the page without a user is sent to /login, anything else answered 401 without a challenge.
* refusals: a drop taking much waits for a second member, and showstv3 days2-1/+6
| | | | | | | | | | | | | | | | | | A node told (REFUSALS) that it holds a change the swarm's rules refuse, and whose own rules refuse it too, dropped it and everything built on it on one peer's word, however much that was; its own writes among them went to the quarantine, the others' nowhere. * A drop taking more than 100 changes, or more than ten and a tenth of the document, now waits until a second member names the same change: the node's rules and one peer's word could both be off. Logged as an error, counted in /admin/stats (sync.dropsHeld). * The others' changes dropped are listed with the refused (GET /admin/refused, "taken unjudged, dropped"), for operators to see. The cheap half of asking the others (future-work.md, Telling a malicious change from a wrong policy).
* users: a user manages its own keystv3 days2-1/+49
| | | | | | | | | | | | | | | | | Only a user who may manage users could change a user's keys: a user couldn't add a new laptop's key, or remove a lost one. A user now manages its own keys, whatever else it may do: * GET /auth/keys: its keys, its entry, and the envelope holding it under a strict policy (`signed`), to sign against. * PUT /auth/keys/{id} with an authorized_keys line, DELETE /auth/keys/{id}: one change, as the user. The rules take a write of _continuum/users/<name>/keys/… from the user it names. * Under a strict policy, the user signs its entry with only its keys changed, with a key it holds already (envelope::check takes that from the user itself), PUT to its own entry even without write on the registry; continuum-signed key-add and key-remove do so.
* login: logins by key capped per usertv3 days2-3/+15
| | | | | | | | | | | | Review 6: a node remembers the challenges used until they expire, 10 000 at most, and refused every login by key once full. One user holding a key could make 10 000 logins within five minutes and keep everyone else out. A user may now log in by key at most --key-logins-per-user times (100) on a node within a challenge's lifetime, past which that user waits and others don't; the 10 000 stay as the cap for all users together. The lifetime, five minutes until now, is --challenge-lifetime.
* dashboard: log out of this browser, and say what can't betv3 days2-2/+6
| | | | | | | | | | | | | | The dashboard had no way to log out; only the login page did. It now shows the user beside the connection, and "log out" where the page got in by its session: that ends this browser's session (DELETE /auth/session), other sessions of the user going on, and opens the login page. Where it got in by what it can't end, the browser's password or the proxy's, it says so instead (the token answer now names `login`, how the user proved it). After logging out, a page still let in by the browser's password says so too. A list of a user's sessions, to end one by one, is in future-work.md.
* docs: a signed null keeps the prev chain goingtv3 days1-1/+4
| | | | | | Review 3.1: a signed null stays as an envelope, so the value written after it names it in prev and an envelope from before the deletion can't be put back. api.md now says so.
* docs: what the review found that staystv3 days2-5/+22
| | | | | | | | | | | | | | | | Review findings 10 and 12, and 3.1/3.3: what stays as it is, said. * Any member can log any user out: the user a logout names is its node's word, and a logout is no deciding path, even under strict. * Used challenges are remembered in memory: after a restart, one captured within its five minutes could be taken once more. * A registry turns strict as it is: whatever it holds then counts, plain values and envelopes, checked or not. Look it over first. * A value that isn't an envelope starts its chain anew (prev empty). * A strict registry whose only manager lost its keys changes no more: keep two managers, or two keys. * The handed-over token stays in the address bar and history where the other dashboard never loads, for its five minutes.
* login: the session cookie Secure behind a TLS proxytv3 days1-1/+4
| | | | | | | | | | | Review finding 12, browsers: the session cookie had no Secure flag. Behind a TLS proxy, the token it carries went over plain HTTP if someone typed http://. POST /auth/session now sets Secure when the request came over TLS, as the proxy says (X-Forwarded-Proto, or Forwarded's proto, of the first hop). A client saying so itself only keeps its own cookie off plain HTTP. operations.md's nginx example sets the header.
* dashboard: a content security policy, framed by nonetv3 days1-1/+2
| | | | | | | | | | | | | | | | | | Review finding 12, browsers: the dashboard holds a bearer token in memory and edits the document, but had no content security policy and could be framed by any page of the same site (another port on the same host, anything sharing a domain behind a proxy), acting with its user's token. The dashboard and the login page now come with a policy that runs only their own inline script (by its hash, computed when first served), loads nothing from elsewhere, and lets no page frame them (frame-ancestors 'none', X-Frame-Options DENY). Fetches stay open to any origin, as the members' dashboards are wherever the member list says. The browser check (check.js) loads the page from a file, without the header, so the policy wants one look in a browser.
* envelope: a signed value names its swarmtv3 days3-7/+10
| | | | | | | | | | | | Review 3.1, part of finding 12: a signed value's payload bound path, user, time and prev, but not the swarm. A user with the same name and key in two swarms could have an envelope replayed from one into the other, wherever the path holds no envelope yet (prev ""). The payload now names the swarm (`swarm`), and a node takes an envelope only for its own. /auth/whoami says the swarm, and continuum-signed and the example in api.md sign it. Wire version 44: nodes before this refuse such envelopes, and those after refuse the old ones.
* token: a banned node's tokens aren't takentv3 days1-3/+3
| | | | | | | | | Review finding 11: token::verify checked only that the issuer is a member, and a ban leaves the member entry. A compromised node's tokens stayed valid after it was banned, until it was forgotten too. A token issued by a banned node is now refused (BannedIssuer), as soon as the ban reaches the node judging it.
* dashboard: hand over a token reading the member list onlytv3 days2-5/+6
| | | | | | | | | | | | | Review finding 8, second part: opening another member's dashboard handed it a token reading everything (READ_ALL). A member writes its own dashboard URL, so a malicious one received a read-everything token under the operator's identity: /admin/config, /admin/changes, /admin/log and the rest, on every node the user reaches. The token handed over now reads the member list only (READ_MEMBERS, as sent to subscribed nodes), which is enough to draw the map; the user logs in at the other dashboard to see more. With 1151 it lasts five minutes and isn't refreshed past them.
* login: a token asked to live less ends its session with ittv3 days2-3/+4
| | | | | | | | | | | | Review finding 8, first part: a refreshed token's lifetime came from the refresh request, and it kept the session's end. So the five-minute read-all token a dashboard hands to another member's refreshed into a 12-hour one, renewable for seven days. A token asked to live less (`lifetime`, at login or refresh) now ends its session with it: nobody refreshes it past its end, its holder included, and a session begun from it (POST /auth/session) ends then too.
* rules: an expired remake aborted by any membertv3 days2-2/+8
| | | | | | | | | | | | | | | | Under a policy, a remake's decision was taken from its coordinator only. A node aborting an expired remake whose coordinator is gone (DELETE /admin/remakes, after abortableAt) was refused by its own judgement (403), so such a remake could never be decided. The decision is now also taken from a user who may write it (the protocol writers), and an abort from any member once the remake expired a grace period ago (abortableAt, by the coordinator's grace period in the proposal). The rules have no clock of their own, so that is judged by the time the change says it was made: the writer's word, so a member could abort a remake early, as it could ask its coordinator to, but never commit one. Before then, an abort stays a request the coordinator applies.
* docs: a member's entry, as the rules have ittv3 days1-3/+4
| | | | | | | Review 1.3: access.md said a user who may write _continuum/members may only remove another member's entry, but the rule lets them write any of it (addresses, dashboard). Those are operators, who may set an entry right through the store, so the doc now says what the rule does.
* rules: a carried remake record taken from the coordinator its snapshot namestv3 days1-1/+2
| | | | | | | | | | | | | | | | | Review finding 7, second part: a record carried into a new epoch whole (proposal and decision in one change) was taken from any member, for any remake ID. A member could write a forged record, decision included, for a fresh ID in a single change. The snapshot's epoch metadata now names the remake's coordinator, as the quorum built it. The carried-record exception holds only in a later epoch, only for the remake that made it, and only from the coordinator its snapshot names, where there is no record yet. Wire version 43: the snapshot, and so the epoch ID, changes. The review suggested carrying the record inside the snapshot instead. The snapshot can't hold the decision, which names the snapshot's own hash. So the snapshot vouches for who may carry the record instead.
* rules: protocol maps judged by what is done to themtv3 days1-2/+7
| | | | | | | | | | | | | Review finding 7, first part: the protocol-map rules looked at paths, not actions. Any member could delete _continuum/members, /votes or /remakes whole, or rewrite another remake's proposal (its coordinator) and then write its decision. What holds the members' entries, votes and remake records, and a remake's record itself, are now made by a member only where there is none; a proposal is written only where there is none. Replacing or removing any of them is left to a user who may write there (the protocol writers). An abort asked for stays any member's.
* screen: a signature vouches for its change and what it builds ontv3 days2-4/+8
| | | | | | | | | | | | | | | Review finding 5: vouching by (actor, seq) isn't bound to a hash. A member can make a change under another member's actor at a seq that member's signature covers. The change is then taken as vouched, and if it breaks a rule, the honest member is refused as a whole and logged as the one whose own change broke the rules. A signature now vouches for its change and the changes of its actor it builds on: in the document, as before; among received changes, those it reaches through the batch's dependencies. A received change under an actor at a seq whose change the document holds (a signature verified up to it) is a fork: refused as forged, never as its actor's node's, not told back in REFUSALS, and logged with the peer that brought it.
* sync: a member refused as a whole, synced with again once it changed documenttv3 days1-2/+3
| | | | | | | | | | | | A node refusing a member as a whole sent it nothing, and lifted that only when a change of the member's was taken again. But a member that dropped what was refused (REFUSALS) starts its sync sessions over, and a fresh session sends no changes until the other side answers: the two waited for each other, and the member stayed refused. The refusal now notes the member's run and epoch_seq when it began; a member with another document since (it dropped what was refused, or rejoined) is synced with again.
* remake: replays judged before they go intv3 days1-0/+4
| | | | | | | | | | | | | | | A switching or rejoining node replays late writes as fresh changes naming each write's user, and nothing judged them before they left: a replay every node would refuse (a revoked capability, a second envelope at a signed path, readers changed) was refused everywhere, and with it everything the node wrote after it. Under a policy, replay_newest now judges each group it commits as every node will (rules::judge_held, in the new epoch); one they would refuse is taken back (forked at the heads before it, same actor) and its paths quarantined with the reason. replay_newest takes the epoch. Review finding 3 (remake replays).
* access: a node judges its own changes before they leavetv3 days1-0/+6
| | | | | | | | | | | | | | | | | | | | The API checks the path a request asks for; the rules judge everything the change does. Where they disagreed, the honest node's change was refused by every other node, and with signatures required the node was refused as a whole. The plan had both gates run the same judgement. Under a policy, each change a node makes (its own writes and those through the API) is now judged by rules::judge as receivers will judge it, before it is signed or sent; one every node would refuse is rolled back (the document forked at the heads before it, same actor: the change never left) and the request refused with 403 (StoreError:: Refused). rules::judge_held, judging a change the document holds, moves from swarm::refusals to access::rules for both. The simulations that play a member past its own checks write with a helper that bypasses them (forge). Review finding 3 (the same judgement at both gates).
* remake: replay from the quarantine where the user may writetv3 days2-4/+16
| | | | | | | | | | POST /admin/quarantine/{id}/replay needs write on _continuum, which never covers user data, and replayed every path of the entry as the user who asked: every node refused the user data in it. It now replays only the paths the user may write and keeps the rest in the entry (left), for one who may; 403 if the user may write none. Review finding 3 (quarantine replays).
* access: forgetting and revoking as the API letstv3 days1-2/+3
| | | | | | | | | | | | | | Two writes the API lets a user make were refused by the rules: * DELETE /peers/{pk}, with write on _continuum/forgotten alone, writes the forget record and removes the member's entry in one change; the rules wanted write on the entry. Removing an entry is now also taken in a change forgetting that member, from a user who may forget it. * DELETE /admin/users/{name}/tokens by a user who manages users writes _continuum/logouts/<name>; the rules wanted write there. One who may write _continuum/users may now write anyone's logouts entry. Review finding 3 (DELETE /peers, revoking tokens).
* access: maps made on the way judged by what they holdtv3 days1-0/+2
| | | | | | | | | | | | A write makes the maps on its way where there are none (store::write_path); the rules judged each made map as a write at its own path, so a user whose capability lies below a map not made yet (write on servers/eu, a first PUT at servers/eu/0) had its first write taken by its node and refused by every other. A map made where there was none, in a change writing within it, is now judged by what is written within it. Review finding 3 (parent maps created on the way).
* access: judged by the policy written undertv3 days1-1/+3
| | | | | | | | | | The rules read the policy at the change's dependencies only for strict mode, so history from before the policy was judged by today's rules when a late node caught up, or when REFUSALS had it judged again. A change written while the document had no policy, or one this node doesn't know, is now judged by none. Review finding 6.
* access: a forgotten node cancels its own recordtv3 days1-0/+1
| | | | | | | | | | A node forgotten swarm-wide while alive cancels its forget record with a change naming no user; the rules judged _continuum/forgotten by the registry alone, so every node refused the cancellation and everything the node wrote after it. A node may now write its own _continuum/forgotten/<id>, as it may its member entry. Review finding 4.
* dashboard: message flashestv3 days1-2/+6
| | | | | messages: off / dots / flash: a message expands at its own speed and leaves an afterglow fading out in the direction it went.
* dashboard: a token handed over to another member's dashboardtv3 days3-3/+10
| | | | | | | | Opening another member's dashboard passes a token reading only, for five minutes, in the URL's fragment; its login page begins a session with it. Tokens may be asked for with a shorter lifetime. Auth plan M3 (hand-off).
* swarm: changes taken unjudged, dropped (REFUSALS)tv3 days4-4/+18
| | | | | | | | | A node refusing changes a peer brought tells it which broke a rule; the peer judges them itself, and if its rules refuse them too, drops them with all built on them (its document forked without them, a fresh actor), quarantining its own writes among them. Wire version 42. Auth plan M6 (rejoin on taint).
* access: members refused as a wholetv3 days4-3/+22
| | | | | | | | | With signatures required, a member whose own change broke a rule is refused as a whole: sent nothing, shown in /peers (refused), in stats (sync.refusedPeers) and on the dashboard, until a change of its is taken again or the epoch changes. Auth plan M6 (diverged standing).
* auth: the session cookie lets in reads onlytv3 days2-2/+6
| | | | | | | | The cookie lets in GET, HEAD and the session's own requests; anything changing something brings the token itself, which no page of another site or port can make a browser send. The dashboard's writes send it. Auth plan M3 (browsers).
* access: refused oncetv3 days1-1/+4
| | | | | | | | A refused change is remembered by its hash: offered again it is refused without decoding, judging or logging, so is one built on it, and a peer offering nothing else is answered later each time, up to a minute. Auth plan M6 (re-sending).
* docs: trust as it standstv3 days1-32/+60
| | | | | | | trust.md: no policy, a policy, strict, sealed values; what members are trusted with at each step, and the logins. Auth plan (docs).
* auth: a login page and browser sessionstv3 days3-2/+66
| | | | | | | | | GET /login without authentication: log in by password or with a token (from a login by key). POST/DELETE /auth/session keep the token as an HttpOnly, SameSite=Strict cookie; the dashboard sends a browser without a user there and renews the session while open. Auth plan M2/M3 (browsers).
* docs: reading keys and declarationstv3 days1-0/+18
| | | | | | | Clients checking signatures or sealing read the registry and _continuum/access: operators grant reading them. Auth plan M7, M8 (docs).
* scripts: sealing and opening in the reference clienttv3 days1-0/+4
| | | | | | | continuum-signed seal and open, with age, for the readers declared or CONTINUUM_READERS. Auth plan M8.
* access: values sealed for their readerstv3 days3-2/+47
| | | | | | | | | | cs1.<age file>: a value encrypted with age to its readers' SSH keys, which no node decrypts. _continuum/access/sealed declares paths whose values must be sealed, for named readers or anyone; nodes check the recipients by age's key tags, at the API and on receipt. Auth plan M8, reworked: sealed values instead of containers, readers as users only (nodes hold no keys).
* scripts: a reference client for signed valuestv3 days1-0/+4
| | | | | | | | scripts/continuum-signed (ssh-keygen, curl, jq): login by key, put and delete signed values, get envelopes, verify one against its user's keys. Auth plan M7.