aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/access.md3
-rw-r--r--docs/api.md10
-rw-r--r--docs/openapi.yaml4
-rwxr-xr-xscripts/continuum-signed6
-rw-r--r--src/access/envelope.rs25
-rw-r--r--src/access/rules.rs1
-rw-r--r--src/access/screen.rs1
-rw-r--r--src/access/sealed.rs3
-rw-r--r--src/api/login.rs2
-rw-r--r--src/api/store.rs2
-rw-r--r--src/document/store.rs3
-rw-r--r--src/simtest/policy.rs6
-rw-r--r--src/transport/wire.rs2
13 files changed, 52 insertions, 16 deletions
diff --git a/docs/access.md b/docs/access.md
index a85dfa4..e8108e2 100644
--- a/docs/access.md
+++ b/docs/access.md
@@ -139,7 +139,8 @@ forgets it. The login page
token pasted, from a login by key, say; the dashboard renews it while open
([`dashboard.md`](dashboard.md)).
-`GET /auth/whoami` says who a request's user is, how it proved it, and what it may do.
+`GET /auth/whoami` says the swarm, who a request's user is, how it proved it, and what
+it may do.
Pages of other origins, the dashboards of other members, may call a node with a token:
its own links (`/admin/dashboard/links/own`, and its events) and messages
diff --git a/docs/api.md b/docs/api.md
index 019f9cf..cf05046 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -281,18 +281,20 @@ their users), a user writes one by signing it with its SSH key: the payload, one
of JSON, then the signature, as `application/continuum-signed`.
```sh
-# The envelope there, if any, and what replaces it names: its SHA-256 (none: "").
+# The swarm it is for; the envelope there, if any, and what replaces it names: its
+# SHA-256 (none: "").
+swarm=$(curl -sf -u alice localhost:3000/auth/whoami | jq -r .swarm)
read=$(curl -sf -u alice -H 'Accept: text/plain' 'localhost:3000/store/config?raw=true')
prev=${read:+$(printf %s "$read" | sha256sum | cut -d' ' -f1)}
-printf '{"path":["config"],"value":{"mode":"fast"},"user":"alice","at":%s,"prev":"%s"}' \
- "$(date +%s000)" "$prev" > payload
+printf '{"swarm":"%s","path":["config"],"value":{"mode":"fast"},"user":"alice","at":%s,"prev":"%s"}' \
+ "$swarm" "$(date +%s000)" "$prev" > payload
ssh-keygen -Y sign -f ~/.ssh/id_ed25519 -n continuum-value payload
{ cat payload; echo; cat payload.sig; } | curl -X PUT localhost:3000/store/config \
-u alice -H 'Content-Type: application/continuum-signed' --data-binary @-
```
The node writes it as its envelope if the authenticated user signed it (`403` if not),
-for this path, with one of its keys, may write there (`403`), and it replaces what is
+for this swarm and this path, with one of its keys, may write there (`403`), and it replaces what is
there (`prev`, the SHA-256 of the envelope it replaces; `409` if it names another:
read again and sign anew), and only where values are signed (`409` elsewhere). It
answers with the value, as a `PUT` does; a signed `null` deletes, with `204`. Under
diff --git a/docs/openapi.yaml b/docs/openapi.yaml
index f0d54a9..13d8a65 100644
--- a/docs/openapi.yaml
+++ b/docs/openapi.yaml
@@ -192,7 +192,7 @@ paths:
schema:
type: string
example: |
- {"path":["config"],"value":{"mode":"fast"},"user":"alice","at":1762400000000,"prev":""}
+ {"swarm":"6f9a…","path":["config"],"value":{"mode":"fast"},"user":"alice","at":1762400000000,"prev":""}
-----BEGIN SSH SIGNATURE-----
U1NIU0lH…
-----END SSH SIGNATURE-----
@@ -981,7 +981,7 @@ paths:
get:
tags: [Auth]
summary: Who you are here
- description: The user, how it proved it (`htpasswd`, `header`, `token`), its registry entry's capabilities, and its token's claims if it came with one.
+ description: The swarm, the user, how it proved it (`htpasswd`, `header`, `token`), its registry entry's capabilities, and its token's claims if it came with one.
responses:
'200':
description: Who.
diff --git a/scripts/continuum-signed b/scripts/continuum-signed
index dc5bd75..6dd1f4c 100755
--- a/scripts/continuum-signed
+++ b/scripts/continuum-signed
@@ -96,10 +96,12 @@ write() {
cv1.*) prev=$(printf %s "$current" | sha256sum | cut -d' ' -f1) ;;
*) prev= ;;
esac
+ swarm=$(curl -sf -H "Authorization: Bearer $CONTINUUM_TOKEN" "$url/auth/whoami" |
+ jq -r .swarm) || die "no swarm from $url"
# The exact bytes signed and sent: one line of JSON, no newline.
- jq -cnj --arg path "$1" --argjson value "$2" --arg user "$user" \
+ jq -cnj --arg swarm "$swarm" --arg path "$1" --argjson value "$2" --arg user "$user" \
--argjson at "$(date +%s)000" --arg prev "$prev" \
- '{path: ($path | split("/")), value: $value, user: $user, at: $at, prev: $prev}' \
+ '{swarm: $swarm, path: ($path | split("/")), value: $value, user: $user, at: $at, prev: $prev}' \
>"$work/payload" || die "not JSON: $2"
sign "$work/payload" continuum-value
{ cat "$work/payload"; echo; cat "$work/payload.sig"; } |
diff --git a/src/access/envelope.rs b/src/access/envelope.rs
index 1804b98..e0c8909 100644
--- a/src/access/envelope.rs
+++ b/src/access/envelope.rs
@@ -108,6 +108,9 @@ fn agree(pattern: &[String], path: &[String]) -> bool {
/// A signed value's payload.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct Payload {
+ /// The swarm it is for: one user, with the same name and key in two swarms, signs
+ /// for one only.
+ pub(crate) swarm: String,
pub(crate) path: Vec<String>,
pub(crate) value: JsonValue,
pub(crate) user: String,
@@ -170,7 +173,7 @@ pub(crate) fn read(envelope: &str) -> Result<Payload, String> {
/// Checks `envelope` as the value at `path`, replacing `previous` (the envelope there
/// before, if any), against the user registry `doc` holds (as the writer saw it): for
-/// that path, naming what it replaces, signed by one of its user's keys, a user who may
+/// its swarm, that path, naming what it replaces, signed by one of its user's keys, a user who may
/// write there.
pub(crate) fn check(
doc: &impl ReadDoc,
@@ -179,6 +182,15 @@ pub(crate) fn check(
previous: Option<&str>,
) -> Result<Signed, String> {
let (bytes, payload, signature) = open(envelope)?;
+ let swarm = store::read_path(
+ doc,
+ &crate::document::meta::path(&[crate::document::meta::SWARM]),
+ )
+ .ok()
+ .flatten();
+ if swarm.as_ref().and_then(JsonValue::as_str) != Some(payload.swarm.as_str()) {
+ return Err(format!("signed for swarm {}, not this one", payload.swarm));
+ }
if payload.path != path {
return Err(format!(
"signed for /{}, not /{}",
@@ -223,6 +235,7 @@ mod tests {
let line = sshsig::PublicKey(key.verifying_key().to_bytes()).line();
let mut doc = Document::new();
change(&mut doc, |doc| {
+ store::write_path(doc, &path("_continuum/swarm"), &json!("s")).unwrap();
let mut alice = registry::entry("root", 1, &[(path("config"), &[Right::Write][..])]);
alice["keys"] = json!({ "laptop": line });
store::write_path(doc, &registry::user_path("alice"), &alice).unwrap();
@@ -240,6 +253,7 @@ mod tests {
fn payload(user: &str, at: &str, value: JsonValue, prev: &str) -> Payload {
Payload {
+ swarm: "s".to_owned(),
path: path(at),
value,
user: user.to_owned(),
@@ -282,6 +296,15 @@ mod tests {
let tampered = first.replacen("cv1.", "cv1.e30", 1);
assert!(check(&doc, &tampered, &path("config"), None).is_err());
assert!(check(&doc, "plain", &path("config"), None).is_err());
+ // For another swarm.
+ let elsewhere = signed(
+ &key,
+ &Payload {
+ swarm: "t".to_owned(),
+ ..payload("alice", "config", json!({"mode": "fast"}), "")
+ },
+ );
+ assert!(check(&doc, &elsewhere, &path("config"), None).is_err());
}
#[test]
diff --git a/src/access/rules.rs b/src/access/rules.rs
index a56e5ae..3eca1c9 100644
--- a/src/access/rules.rs
+++ b/src/access/rules.rs
@@ -888,6 +888,7 @@ mod tests {
);
let sealed = |at: &str, prev: &str| {
let payload = envelope::Payload {
+ swarm: "s".to_owned(),
path: path(at),
value: json!({"mode": "fast"}),
user: "alice".to_owned(),
diff --git a/src/access/screen.rs b/src/access/screen.rs
index 4c640b6..aa16b42 100644
--- a/src/access/screen.rs
+++ b/src/access/screen.rs
@@ -761,6 +761,7 @@ mod tests {
write(&mut theirs, "a", None, "_continuum/members/a", json!({}));
let sealed = |prev: &str, value: serde_json::Value| {
let payload = envelope::Payload {
+ swarm: "s".to_owned(),
path: path("config"),
value,
user: "alice".to_owned(),
diff --git a/src/access/sealed.rs b/src/access/sealed.rs
index 7a8896f..ed5024c 100644
--- a/src/access/sealed.rs
+++ b/src/access/sealed.rs
@@ -347,8 +347,7 @@ mod tests {
assert!(check(&doc, notes, &json!("plain")).is_err());
// Signed: holding a sealed value, or null.
let signed = |value: JsonValue| {
- let payload =
- json!({"path": ["notes"], "value": value, "user": "alice", "at": 1, "prev": ""});
+ let payload = json!({"swarm": "s", "path": ["notes"], "value": value, "user": "alice", "at": 1, "prev": ""});
format!("cv1.{}.c2ln", B64URL.encode(payload.to_string()))
};
assert!(check(&doc, notes, &json!(signed(json!(sealed_for(&[key(9)]))))).is_ok());
diff --git a/src/api/login.rs b/src/api/login.rs
index 31a5d7f..1faadf7 100644
--- a/src/api/login.rs
+++ b/src/api/login.rs
@@ -502,6 +502,7 @@ pub(crate) async fn whoami(
Extension(via): Extension<Via>,
token: Option<Extension<Token>>,
) -> Json<JsonValue> {
+ let swarm = state.swarm_id().await;
let replica = state.doc.read().await;
let entry = registry::user(&replica.doc, &user.0);
let token = token.map(|Extension(token)| {
@@ -516,6 +517,7 @@ pub(crate) async fn whoami(
})
});
Json(json!({
+ "swarm": swarm,
"user": user.0,
"via": via.0,
"registry": registry::exists(&replica.doc),
diff --git a/src/api/store.rs b/src/api/store.rs
index 101957a..81d0046 100644
--- a/src/api/store.rs
+++ b/src/api/store.rs
@@ -1315,7 +1315,7 @@ mod tests {
fn envelope_of(value: JsonValue) -> String {
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
- let payload = json!({"path": ["x"], "value": value, "user": "alice", "at": 1, "prev": ""});
+ let payload = json!({"swarm": "s", "path": ["x"], "value": value, "user": "alice", "at": 1, "prev": ""});
format!("cv1.{}.c2ln", URL_SAFE_NO_PAD.encode(payload.to_string()))
}
diff --git a/src/document/store.rs b/src/document/store.rs
index d323928..e04ef41 100644
--- a/src/document/store.rs
+++ b/src/document/store.rs
@@ -1707,8 +1707,7 @@ mod tests {
fn plain_reads_open_envelopes() {
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
let sealed = |value: JsonValue| {
- let payload =
- json!({"path": ["a"], "value": value, "user": "alice", "at": 1, "prev": ""});
+ let payload = json!({"swarm": "s", "path": ["a"], "value": value, "user": "alice", "at": 1, "prev": ""});
format!("cv1.{}.c2ln", URL_SAFE_NO_PAD.encode(payload.to_string()))
};
let mut doc = crate::document::Document::new();
diff --git a/src/simtest/policy.rs b/src/simtest/policy.rs
index 4873b38..ba32615 100644
--- a/src/simtest/policy.rs
+++ b/src/simtest/policy.rs
@@ -217,7 +217,13 @@ async fn write_signed(
Ok(Some(serde_json::Value::String(text))) if text.starts_with("cv1.") => Some(text),
_ => None,
};
+ let swarm = store::read_path(&node.doc.read().await.doc, &path("_continuum/swarm"))
+ .ok()
+ .flatten()
+ .and_then(|swarm| swarm.as_str().map(str::to_owned))
+ .unwrap_or_default();
let payload = envelope::Payload {
+ swarm,
path: at.clone(),
value,
user: user.to_owned(),
diff --git a/src/transport/wire.rs b/src/transport/wire.rs
index e7bec62..2ab39e5 100644
--- a/src/transport/wire.rs
+++ b/src/transport/wire.rs
@@ -14,7 +14,7 @@ use super::{EncodedMessage, MessageKind, ParsedMessage};
/// The wire format's version, the first byte of every payload. Bump it with every change
/// to a message type or a codec (`golden_bytes` fails then).
-pub(crate) const VERSION: u8 = 43;
+pub(crate) const VERSION: u8 = 44;
/// What the second byte of a payload says.
fn tag(kind: MessageKind) -> u8 {