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
|
|
@ -361,6 +361,56 @@ pub fn verify_control_visibility(
|
|||
vk.verify_strict(&control_visibility_bytes(post_id, &canon, timestamp_ms), &sig).is_ok()
|
||||
}
|
||||
|
||||
/// Canonical bytes for a Profile-post signature: length-prefixed display_name
|
||||
/// and bio, 32-byte avatar_cid (or zeros), then timestamp_ms. Length prefixes
|
||||
/// prevent extension/reordering attacks.
|
||||
fn profile_post_bytes(
|
||||
display_name: &str,
|
||||
bio: &str,
|
||||
avatar_cid: &Option<[u8; 32]>,
|
||||
timestamp_ms: u64,
|
||||
) -> Vec<u8> {
|
||||
let dn = display_name.as_bytes();
|
||||
let bio_bytes = bio.as_bytes();
|
||||
let mut buf = Vec::with_capacity(5 + 8 + dn.len() + 8 + bio_bytes.len() + 32 + 8);
|
||||
buf.extend_from_slice(b"prof:");
|
||||
buf.extend_from_slice(&(dn.len() as u64).to_le_bytes());
|
||||
buf.extend_from_slice(dn);
|
||||
buf.extend_from_slice(&(bio_bytes.len() as u64).to_le_bytes());
|
||||
buf.extend_from_slice(bio_bytes);
|
||||
let avatar = avatar_cid.unwrap_or([0u8; 32]);
|
||||
buf.extend_from_slice(&avatar);
|
||||
buf.extend_from_slice(×tamp_ms.to_le_bytes());
|
||||
buf
|
||||
}
|
||||
|
||||
pub fn sign_profile(
|
||||
seed: &[u8; 32],
|
||||
display_name: &str,
|
||||
bio: &str,
|
||||
avatar_cid: &Option<[u8; 32]>,
|
||||
timestamp_ms: u64,
|
||||
) -> Vec<u8> {
|
||||
let signing_key = SigningKey::from_bytes(seed);
|
||||
let sig = signing_key.sign(&profile_post_bytes(display_name, bio, avatar_cid, timestamp_ms));
|
||||
sig.to_bytes().to_vec()
|
||||
}
|
||||
|
||||
pub fn verify_profile(
|
||||
author: &NodeId,
|
||||
display_name: &str,
|
||||
bio: &str,
|
||||
avatar_cid: &Option<[u8; 32]>,
|
||||
timestamp_ms: u64,
|
||||
signature: &[u8],
|
||||
) -> bool {
|
||||
if signature.len() != 64 { return false; }
|
||||
let sig_bytes: [u8; 64] = match signature.try_into() { Ok(b) => b, Err(_) => return false };
|
||||
let sig = ed25519_dalek::Signature::from_bytes(&sig_bytes);
|
||||
let Ok(vk) = VerifyingKey::from_bytes(author) else { return false };
|
||||
vk.verify_strict(&profile_post_bytes(display_name, bio, avatar_cid, timestamp_ms), &sig).is_ok()
|
||||
}
|
||||
|
||||
/// Verify an ed25519 delete signature: the author's public key signed the post_id.
|
||||
pub fn verify_delete_signature(author: &NodeId, post_id: &PostId, signature: &[u8]) -> bool {
|
||||
if signature.len() != 64 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue