| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Review 3, R1: what a node received was vouched for only while its own
document's policy required signatures. A joiner's first sync, and
every node's message bringing `required`, went in by the rules alone:
a malicious first peer could plant changes under other members' actors
at their next seqs, their real ones then failing as duplicates for
good.
The signatures a message carries are always offered now, and the
screen always has them: a change waits for one where the policy at the
heads it is judged at requires it (rules::Reads::required, read once
per heads), whatever the receiving document holds. Changes written
under a policy not requiring signatures wait for none, as on every
other node; the check for forged changes runs always. Changes waiting
for a signature are judged again whenever signatures come.
The test of signatures required now takes the history into an empty
document; a new one takes changes written under a policy not requiring
them without any.
|
| |
|
|
|
|
|
|
|
|
| |
A write every node would refuse is rolled back by forking the document
at the heads before it, under the write lock, at a cost that grows with
the document's history. A user could ask for such writes in a loop.
Count each user's refused writes over the last minute. Past
--refused-writes-per-minute (default 10), its writes are answered 429
without being tried, until the oldest refusal leaves the minute.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Review 2, N7, the care it raised: a joining node's empty document took
as the first epoch's root whatever dependency-less change came first,
from whichever peer it joined through; the first epoch's ID is all
zeros, so nothing pinned it. A remake's epoch has its ID, the snapshot
change's hash, which the rules hold its root to.
* Every node names its epoch's root in HELLO and HELLO_REPLY (the
epoch's ID for a remake's, its genesis change for the first; wire
version 45). A node whose document starts from another root of the
same epoch refuses the handshake, logging it: another history under
the swarm's ID, a forged root or a genesis started again.
* A joining node with an empty document takes as its root only the one
it was given (--genesis-root, new), else the one its peers name, and
none while they name different ones (access::screen::RootPin, kept
in Screening across documents): one peer hands it another root only
if it is all the joiner hears, or never, with --genesis-root. A root
refused is listed with the refused, what builds on it with it.
* The genesis node logs the root (`The swarm's root`); GET
/admin/epochs shows it (`root`).
A genesis started again under a remembered swarm ID used to merge with
the nodes of it still running; they now refuse it (another root), and
the docs say to join them with seeds instead.
The epoch's identity stays as it is (generation 0 all zeros): records
and epoch metadata name it so, and every comparison of epochs with
them.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
| |
Review 2, N10: the judgement before a change leaves (1140) and a
replay's (1141) skipped changes when the document *with* the change had
no policy it enforces. A write removing the policy, or setting one the
node doesn't know, then left unjudged, while receivers judge it at its
dependencies, where the policy was enforced. Both now read the policy
at the heads before the change, as receivers do.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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).
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Review 2.2: swap_document forgot every signature. After dropping
changes taken unjudged (or a rejoin into the same epoch), the node
could vouch for none of the changes it kept: under signatures required,
those building on an actor waited for that actor's next signature, and
for an actor that stopped writing, forever.
Signatures::kept_for keeps the signatures of the changes the new
document holds, checks each actor's latest again, and keeps those
waiting for their changes. This node's replayed changes are signed at
once, as before.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A REFUSALS naming the swarm's first change made the node receiving it
judge that change again in a document that isn't empty, refuse it as
"a change without dependencies that isn't the epoch's root", and drop
it with everything built on it: its whole document.
The first epoch's root is now the first change a document holds, as it
comes to an empty one or as it is in one; a change judged again on a
peer's word is never refused for having no dependencies; and
drop_taken refuses to drop an epoch's root, or anything that would
leave nothing kept, logging an error instead.
Review finding 1 (critical).
|
| |
|
|
|
|
|
|
|
| |
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).
|
| |
|
|
|
|
|
|
| |
POST /auth/challenge and /auth/login/key: a node-signed challenge,
valid five minutes and taken once, signed with one of the user's SSH
keys (ssh-keygen -Y sign -n continuum-login), for a token.
Auth plan M7 (key login, M2b).
|
| |
|
|
|
|
|
|
|
| |
Under a policy, a change by a node that isn't a member is refused with
all built on it: a node's registration is written before any write of
its own or through the API, not after (the registration itself
exempt).
Auth plan M6 (fix).
|
| |
|
|
|
|
|
|
| |
"signatures": "required": a change counts only once a signature by its
committer vouches for it, and waits until then; every node then signs
every change it makes.
Auth plan M6.
|
| |
|
|
|
|
|
|
| |
_continuum/access/policy ({"version": 1}) has every node screen every
change it receives, on every path changes come in. A policy a node
doesn't know stops its writes through the API. GET /admin/refused.
Auth plan M6.
|
| |
|
|
|
|
|
| |
Forget records and released quarantines name the user who made them,
so they are judged by its capabilities.
Auth plan M6.
|
| |
|
|
|
|
|
| |
Sync messages carry, for each actor's writes, the latest signature the
sender checked; the receiver checks it once it holds the change.
Auth plan M5.
|
| |
|
|
|
|
|
|
| |
Changes left unsigned are vouched for by the node's next signature:
--sign-every 1s (default) signs the latest within a second, all signs
every change, none only those that must be.
Auth plan M5.
|
| |
|
|
|
|
|
|
|
|
| |
Every node signs the changes it commits with its node key, an Ed25519
signature over the change's hash, kept beside the document. One
signature on an actor's latest change vouches for its earlier ones.
Protocol data and paths listed in _continuum/access/signed are signed,
and writes with ?signed=true.
Auth plan M5.
|
| |
|
|
|
|
|
|
|
| |
The dashboard reads every member's links from the members themselves;
LINKS and LINKS_REPLY, swarm::links and their wire tags go (wire
version bumped). Nodes the browser can't reach are drawn from what the
reachable ones measure.
Auth plan M3.
|
| |
|
|
|
|
|
|
|
| |
GET /admin/dashboard/links/own and its event stream: a node's own links,
as LINKS_REPLY carried them, for other members' dashboards with a
token. CORS for the members' dashboard origins and --cors-origins,
without credentials.
Auth plan M3.
|
| |
|
|
|
|
|
|
|
| |
POST /auth/login (by htpasswd or the proxy), /auth/refresh,
/auth/logout, GET /auth/whoami; every request may bring
Authorization: Bearer instead of a password. --token-lifetime and
--session-lifetime.
Auth plan M2.
|
| |
|
|
|
|
|
|
| |
--bootstrap-users gives a new swarm a registry from its first change,
each first user with everything; other users may do nothing until
added. Without it the swarm has no registry and works as before.
Auth plan M1.
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| |
|
|
|
|
| |
While we wait for a peer's HELLO_REPLY, anything else that comes from it
makes our HELLO go again at once, at most once a second, instead of when
the backoff says.
|
| |
|
|
|
|
|
| |
The heal clock now also restarts when our heads and the peer's have both
changed since the last STATUS that showed them differing. While writes
go on, two neighbours's heads differ because of changes still on their
way, and that alone no longer leads to a heal.
|
| | |
|
| |
|
|
|
| |
An overdue sync answer is now followed by a PING too, and the peer stays
reached once it answers the PING.
|
| | |
|
| | |
|