Phase 2d: profile posts signed by the posting identity
Display metadata (display_name, bio, avatar_cid) is no longer broadcast via
the ProfileUpdate direct push when the user edits their name. It travels as
a signed public post with VisibilityIntent::Profile, authored by the posting
identity, and propagates through the normal neighbor-manifest CDN path.
Core pieces:
- `types::ProfilePostContent` — JSON payload serialized into the post's
content field. Ed25519 signature by the posting secret over length-prefixed
display_name + bio + 32-byte avatar_cid (or zeros) + timestamp.
- `crypto::{sign,verify}_profile` with strict length prefixing to prevent
extension attacks.
- New `profile` module: `build_profile_post`, `verify_profile_post`,
`apply_profile_post_if_applicable`. Last-writer-wins by timestamp.
- `control::receive_post` now verifies Profile-intent posts upfront (same
as Control) so bogus signatures never enter storage, and applies them
after store so the `profiles` row updates atomically with the insert.
Node API:
- `Node::set_profile` rewritten: builds a signed Profile post, stores under
intent=Profile, applies it locally (upserts the profiles row keyed by the
posting identity), then propagates via `update_neighbor_manifests_as`.
Stops calling `network.push_profile` — display changes no longer trigger
a direct wire push.
- `Node::my_profile` / `has_profile` read by `default_posting_id` instead of
`node_id`, matching where the row is written now.
ProfileUpdate (0x50) and push_profile stay for now — they still carry
routing-only data (anchors, recent_peers, preferred_peers) via
`sanitized_for_network_broadcast` and are used by `set_anchors` /
`set_public_visible`. Removing the routing fields would be a broader
cleanup; scoped out of this phase.
Tests: roundtrip verify+store, wrong-author rejection (not stored), and
older-timestamp ignored. 114 / 114 core tests pass.
This commit is contained in:
parent
eabdb7ba4f
commit
8b2881d84a
6 changed files with 339 additions and 51 deletions
|
|
@ -88,24 +88,30 @@ pub fn receive_post(
|
|||
visibility: &PostVisibility,
|
||||
intent: Option<&VisibilityIntent>,
|
||||
) -> anyhow::Result<bool> {
|
||||
if matches!(intent, Some(VisibilityIntent::Control)) {
|
||||
// Verify the ControlOp signature before storing. A bogus control post
|
||||
// with an invalid signature should never enter storage.
|
||||
let op: ControlOp = serde_json::from_str(&post.content).map_err(|e| {
|
||||
anyhow::anyhow!("control post content is not a valid ControlOp: {}", e)
|
||||
})?;
|
||||
match &op {
|
||||
ControlOp::DeletePost { post_id, timestamp_ms, signature } => {
|
||||
if !crypto::verify_control_delete(&post.author, post_id, *timestamp_ms, signature) {
|
||||
anyhow::bail!("invalid control-delete signature");
|
||||
// Verify signed intent posts BEFORE storing. Bogus signed posts must
|
||||
// never enter storage and get re-propagated via neighbor-manifest diffs.
|
||||
match intent {
|
||||
Some(VisibilityIntent::Control) => {
|
||||
let op: ControlOp = serde_json::from_str(&post.content).map_err(|e| {
|
||||
anyhow::anyhow!("control post content is not a valid ControlOp: {}", e)
|
||||
})?;
|
||||
match &op {
|
||||
ControlOp::DeletePost { post_id, timestamp_ms, signature } => {
|
||||
if !crypto::verify_control_delete(&post.author, post_id, *timestamp_ms, signature) {
|
||||
anyhow::bail!("invalid control-delete signature");
|
||||
}
|
||||
}
|
||||
}
|
||||
ControlOp::UpdateVisibility { post_id, new_visibility, timestamp_ms, signature } => {
|
||||
if !crypto::verify_control_visibility(&post.author, post_id, new_visibility, *timestamp_ms, signature) {
|
||||
anyhow::bail!("invalid control-visibility signature");
|
||||
ControlOp::UpdateVisibility { post_id, new_visibility, timestamp_ms, signature } => {
|
||||
if !crypto::verify_control_visibility(&post.author, post_id, new_visibility, *timestamp_ms, signature) {
|
||||
anyhow::bail!("invalid control-visibility signature");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(VisibilityIntent::Profile) => {
|
||||
crate::profile::verify_profile_post(post)?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let stored = if let Some(intent) = intent {
|
||||
|
|
@ -115,6 +121,7 @@ pub fn receive_post(
|
|||
};
|
||||
if stored {
|
||||
apply_control_post_if_applicable(s, post, intent)?;
|
||||
crate::profile::apply_profile_post_if_applicable(s, post, intent)?;
|
||||
}
|
||||
Ok(stored)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue