maarcadetweet: initial commit

AT Protocol PDS + AppView + Tauri Desktop Client, 160-char post limit.

- PDS (Rust + axum + sqlx)
  - Auth: createAccount, createSession, refreshSession
  - Records: createRecord, deleteRecord (race-safe via SELECT FOR UPDATE)
  - Feed: feed.like.create, feed.repost.create
  - Sync: getRepo, getBlocks, getLatestCommit, getRecord (with MST proof), listRepos
  - Identity: resolveHandle
  - MST: spec-conformant (at-mst crate, 27 tests)
  - Repo: signed commits, TID counter (monotonic, 4096 wrap safe)

- AppView (Rust + axum + sqlx)
  - Jetstream consumer (WebSocket, exponential backoff, 38k+ events indexed)
  - REST API: timeline/home (graph-aware), profile, search, post (with thread hydration)
  - Handle-sync worker (did:plc + did:web)
  - JSONB embed storage + thread columns (migration 0003)
  - Like/repost counter cache (migration 0004)

- Tauri 2 + Svelte 5 Desktop Client
  - System tray (Show/Compose/Quit menu)
  - OS notifications (tauri-plugin-notification)
  - Auto-update (tauri-plugin-updater, placeholder endpoint)
  - Window-state (tauri-plugin-window-state)
  - 160-char compose with live counter
  - Image/Link embed rendering
  - LocalStorage-persisted like state
  - Timeline with poll (prepend new posts)
  - Custom TitleBar (transparent, no decorations)
  - Orange/IBM Plex Mono maarcade design

Tests: 231 Rust + 9 vitest = 240 passed.
This commit is contained in:
tomdebone
2026-07-05 20:01:31 +02:00
commit c586fd39c9
134 changed files with 35279 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
[package]
name = "at-crypto"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
description = "Cryptographic primitives for the AT Protocol (k256, p256, CID, multibase)"
[lints.rust]
unsafe_code = "forbid"
[dependencies]
k256 = { workspace = true }
p256 = { workspace = true }
sec1 = { workspace = true }
secp256k1 = { workspace = true }
sha2 = { workspace = true }
blake3 = { workspace = true }
multibase = { workspace = true }
multihash = { workspace = true }
cid = { workspace = true }
rand = { workspace = true }
rand_core = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
hex = { workspace = true }
base64 = { workspace = true }
thiserror = { workspace = true }
anyhow = { workspace = true }
ciborium = { workspace = true }
jsonwebtoken = { workspace = true }
[dev-dependencies]
hex = { workspace = true }
insta = { workspace = true }
chrono = { workspace = true }
+75
View File
@@ -0,0 +1,75 @@
use anyhow::Result;
use cid::Cid;
use multihash::Multihash;
pub type Hash = [u8; 32];
pub const SHA2_256_CODE: u64 = 0x12;
pub const RAW_CODEC: u64 = 0x55;
pub const DAG_CBOR_CODEC: u64 = 0x71;
pub fn sha256(data: &[u8]) -> Hash {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(data);
let out = hasher.finalize();
let mut h = [0u8; 32];
h.copy_from_slice(&out);
h
}
pub fn blake3_hash(data: &[u8]) -> Hash {
let mut h = [0u8; 32];
h.copy_from_slice(blake3::hash(data).as_bytes());
h
}
pub fn cid_for_raw(codec: u64, hash: Hash) -> Result<Cid> {
let mh = Multihash::wrap(SHA2_256_CODE, &hash)?;
Ok(Cid::new_v1(codec, mh))
}
pub fn cid_for_cbor(data: &[u8]) -> Result<Cid> {
cid_for_raw(DAG_CBOR_CODEC, sha256(data))
}
pub fn cid_from_multihash_bytes(bytes: &[u8]) -> Result<Cid> {
Ok(Cid::read_bytes(bytes)?)
}
pub fn cid_to_bytes(cid: &Cid) -> Vec<u8> {
cid.to_bytes()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sha256_known_vector() {
let h = sha256(b"hello world");
assert_eq!(
hex::encode(h),
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
);
}
#[test]
fn cid_cbor_roundtrip() {
let data = b"some cbor-encoded block";
let c = cid_for_cbor(data).unwrap();
let s = c.to_string();
assert!(s.starts_with("bafyre") || s.starts_with("bafy"));
let c2: Cid = s.parse().unwrap();
assert_eq!(c, c2);
}
#[test]
fn cid_bytes_roundtrip() {
let data = b"abc";
let c = cid_for_cbor(data).unwrap();
let bytes = cid_to_bytes(&c);
let c2 = cid_from_multihash_bytes(&bytes).unwrap();
assert_eq!(c, c2);
}
}
+82
View File
@@ -0,0 +1,82 @@
use anyhow::Result;
use k256::{
elliptic_curve::sec1::ToEncodedPoint,
PublicKey, SecretKey,
};
use crate::multibase_util::encode_b58btc;
pub const MULTICODEC_SECP256K1_PUB: u64 = 0xe7;
pub fn pubkey_to_multibase(pubkey: &k256::PublicKey) -> Result<String> {
let point = pubkey.to_encoded_point(true);
let bytes = point.as_bytes();
let mut prefixed = Vec::with_capacity(bytes.len() + 2);
let codec = (MULTICODEC_SECP256K1_PUB as u16).to_be_bytes();
prefixed.extend_from_slice(&codec);
prefixed.extend_from_slice(bytes);
Ok(encode_b58btc(&prefixed))
}
pub fn verifying_key_to_multibase(vk: &k256::ecdsa::VerifyingKey) -> Result<String> {
let pk: k256::PublicKey = vk.into();
pubkey_to_multibase(&pk)
}
pub fn pubkey_from_multibase(s: &str) -> Result<k256::PublicKey> {
let raw = crate::multibase_util::decode_multibase(s)?;
anyhow::ensure!(raw.len() > 2, "multibase too short");
let codec = u16::from_be_bytes([raw[0], raw[1]]);
anyhow::ensure!(
codec as u64 == MULTICODEC_SECP256K1_PUB,
"not a secp256k1 pubkey"
);
let key = PublicKey::from_sec1_bytes(&raw[2..])?;
Ok(key)
}
pub fn did_key_from_pubkey(pubkey: &k256::PublicKey) -> Result<String> {
let mb = pubkey_to_multibase(pubkey)?;
Ok(format!("did:key:{}", mb))
}
pub fn did_from_pubkey(pubkey: &k256::PublicKey) -> Result<String> {
did_key_from_pubkey(pubkey)
}
pub fn signing_pubkey_to_did(secret: &SecretKey) -> Result<String> {
did_key_from_pubkey(&secret.public_key())
}
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializedKey {
#[serde(rename = "type")]
pub key_type: String,
pub value: String,
}
impl SerializedKey {
pub fn from_k256(secret: &SecretKey) -> Result<Self> {
let mb = pubkey_to_multibase(&secret.public_key())?;
Ok(Self {
key_type: "Multikey".into(),
value: mb,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use k256::SecretKey;
#[test]
fn did_key_format() {
let sk = SecretKey::from_slice(&[1u8; 32]).unwrap();
let did = signing_pubkey_to_did(&sk).unwrap();
assert!(did.starts_with("did:key:z"));
assert!(did.len() > 50);
}
}
+146
View File
@@ -0,0 +1,146 @@
use anyhow::Result;
use k256::ecdsa::{signature::Signer, Signature as K256Signature, SigningKey};
use p256::ecdsa::{Signature as P256Signature, SigningKey as P256SigningKey};
use rand_core::OsRng;
use serde::{Deserialize, Serialize};
use crate::did_key::verifying_key_to_multibase;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct K256Keypair {
pub secret_hex: String,
pub public_multibase: String,
}
impl K256Keypair {
pub fn generate() -> Result<Self> {
let sk = SigningKey::random(&mut OsRng);
let secret_hex = hex::encode(sk.to_bytes());
let public_multibase = verifying_key_to_multibase(sk.verifying_key())?;
Ok(Self {
secret_hex,
public_multibase,
})
}
pub fn from_secret_hex(hex_str: &str) -> Result<Self> {
let bytes = hex::decode(hex_str.trim_start_matches("0x"))?;
let sk = SigningKey::from_bytes(bytes.as_slice().into())?;
let public_multibase = verifying_key_to_multibase(sk.verifying_key())?;
Ok(Self {
secret_hex: hex_str.to_string(),
public_multibase,
})
}
pub fn secret_key(&self) -> Result<SigningKey> {
let bytes = hex::decode(self.secret_hex.trim_start_matches("0x"))?;
Ok(SigningKey::from_bytes(bytes.as_slice().into())?)
}
pub fn verifying_key(&self) -> Result<k256::ecdsa::VerifyingKey> {
Ok(*self.secret_key()?.verifying_key())
}
pub fn sign(&self, msg: &[u8]) -> Result<Vec<u8>> {
let sk = self.secret_key()?;
let sig: K256Signature = sk.sign(msg);
Ok(sig.to_bytes().to_vec())
}
pub fn verify(&self, msg: &[u8], sig_bytes: &[u8]) -> Result<bool> {
use k256::ecdsa::signature::Verifier;
let vk = self.verifying_key()?;
let sig = K256Signature::try_from(sig_bytes)?;
Ok(vk.verify(msg, &sig).is_ok())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct P256Keypair {
pub secret_hex: String,
pub public_multibase: String,
}
impl P256Keypair {
pub fn generate() -> Result<Self> {
let sk = P256SigningKey::random(&mut OsRng);
let secret_hex = hex::encode(sk.to_bytes());
let pt = sk.verifying_key().to_encoded_point(true);
let mut prefixed = Vec::with_capacity(pt.as_bytes().len() + 2);
prefixed.extend_from_slice(&0x80_12u16.to_be_bytes());
prefixed.extend_from_slice(pt.as_bytes());
let public_multibase = crate::multibase_util::encode_b58btc(&prefixed);
Ok(Self {
secret_hex,
public_multibase,
})
}
pub fn secret_key(&self) -> Result<P256SigningKey> {
let bytes = hex::decode(self.secret_hex.trim_start_matches("0x"))?;
Ok(P256SigningKey::from_bytes(bytes.as_slice().into())?)
}
pub fn verifying_key(&self) -> Result<p256::ecdsa::VerifyingKey> {
Ok(*self.secret_key()?.verifying_key())
}
pub fn sign(&self, msg: &[u8]) -> Result<Vec<u8>> {
let sk = self.secret_key()?;
let sig: P256Signature = sk.sign(msg);
Ok(sig.to_bytes().to_vec())
}
}
#[derive(Debug, Clone)]
pub struct Signature {
pub r: [u8; 32],
pub s: [u8; 32],
}
impl Signature {
pub fn from_der(der: &[u8]) -> Result<Self> {
let sig = K256Signature::from_der(der)?;
Self::from_k256(&sig)
}
pub fn from_k256(sig: &K256Signature) -> Result<Self> {
let bytes = sig.to_bytes();
anyhow::ensure!(bytes.len() == 64, "bad k256 sig length");
let mut r = [0u8; 32];
let mut s = [0u8; 32];
r.copy_from_slice(&bytes[..32]);
s.copy_from_slice(&bytes[32..]);
Ok(Self { r, s })
}
}
#[cfg(test)]
mod tests {
use super::*;
use p256::elliptic_curve::sec1::ToEncodedPoint;
#[test]
fn k256_sign_verify_roundtrip() {
let kp = K256Keypair::generate().unwrap();
let msg = b"hello atproto";
let sig = kp.sign(msg).unwrap();
assert!(kp.verify(msg, &sig).unwrap());
assert!(!kp.verify(b"tampered", &sig).unwrap());
}
#[test]
fn p256_sign_roundtrip() {
let kp = P256Keypair::generate().unwrap();
let sig = kp.sign(b"refresh token").unwrap();
assert_eq!(sig.len(), 64);
}
#[test]
fn encoded_point_compiles() {
let kp = P256Keypair::generate().unwrap();
let vk = kp.verifying_key().unwrap();
let _ = vk.to_encoded_point(true);
}
}
+101
View File
@@ -0,0 +1,101 @@
use anyhow::{anyhow, Result};
use base64::Engine;
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
use p256::pkcs8::{EncodePrivateKey, LineEnding};
use serde::{Deserialize, Serialize};
use crate::ecdsa::P256Keypair;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JwtClaims {
pub iss: String,
pub sub: String,
pub aud: String,
pub exp: i64,
pub iat: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub jti: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
}
pub fn issue_jwt(keypair: &P256Keypair, claims: &JwtClaims) -> Result<String> {
let sk = keypair.secret_key()?;
let pem = sk
.to_pkcs8_pem(LineEnding::LF)
.map_err(|e| anyhow!("pkcs8 pem: {e}"))?;
let enc = EncodingKey::from_ec_pem(pem.as_bytes())
.map_err(|e| anyhow!("jwt enc: {e}"))?;
let token = encode(&Header::new(Algorithm::ES256), claims, &enc)
.map_err(|e| anyhow!("jwt encode: {e}"))?;
Ok(token)
}
pub fn verify_jwt(token: &str, pubkey_multibase: &str) -> Result<JwtClaims> {
let (x, y) = p256_pubkey_multibase_to_xy(pubkey_multibase)?;
let x_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(x);
let y_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(y);
let dec = DecodingKey::from_ec_components(&x_b64, &y_b64)
.map_err(|e| anyhow!("jwt dec key: {e}"))?;
let mut validation = Validation::new(Algorithm::ES256);
validation.leeway = 30;
validation.validate_aud = false;
let data = decode::<JwtClaims>(token, &dec, &validation).map_err(|e| anyhow!("jwt dec: {e}"))?;
Ok(data.claims)
}
pub fn p256_pubkey_multibase_to_xy(mb: &str) -> Result<([u8; 32], [u8; 32])> {
let raw = crate::multibase_util::decode_multibase(mb)?;
if raw.len() < 66 {
return Err(anyhow!("p-256 multikey too short"));
}
if raw[0] != 0x80 || raw[1] != 0x12 {
return Err(anyhow!("not a p-256 multikey"));
}
let mut x = [0u8; 32];
let mut y = [0u8; 32];
x.copy_from_slice(&raw[2..34]);
y.copy_from_slice(&raw[34..66]);
Ok((x, y))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn issue_and_verify() {
let kp = P256Keypair::generate().unwrap();
let vk = kp.verifying_key().unwrap();
let pt = vk.to_encoded_point(false);
let x = pt.x().unwrap();
let y = pt.y().unwrap();
let mut mb_raw = vec![0x80, 0x12];
mb_raw.extend_from_slice(x);
mb_raw.extend_from_slice(y);
let mb = crate::multibase_util::encode_b58btc(&mb_raw);
let now = chrono::Utc::now().timestamp();
let claims = JwtClaims {
iss: "did:plc:test".into(),
sub: "did:plc:test".into(),
aud: "did:web:appview.example".into(),
iat: now,
exp: now + 3600,
jti: None,
scope: Some("com.atproto.access".into()),
};
let token = issue_jwt(&kp, &claims).unwrap();
let parsed = verify_jwt(&token, &mb).unwrap();
assert_eq!(parsed.sub, claims.sub);
}
#[test]
fn decode_pkcs8_pem_roundtrip() {
use p256::pkcs8::DecodePrivateKey;
let kp = P256Keypair::generate().unwrap();
let sk = kp.secret_key().unwrap();
let pem = sk.to_pkcs8_pem(LineEnding::LF).unwrap();
let reloaded = p256::SecretKey::from_pkcs8_pem(pem.as_str()).unwrap();
assert_eq!(sk.to_bytes(), reloaded.to_bytes());
}
}
+15
View File
@@ -0,0 +1,15 @@
pub mod cid;
pub mod did_key;
pub mod ecdsa;
pub mod jwt;
pub mod multibase_util;
pub mod plc_op;
pub mod signing;
pub use cid::{cid_for_cbor, cid_for_raw};
pub use ::cid::Cid;
pub use did_key::{did_from_pubkey, did_key_from_pubkey};
pub use ecdsa::{K256Keypair, P256Keypair, Signature};
pub use jwt::{issue_jwt, verify_jwt, JwtClaims};
pub use plc_op::{PlcOperation, PlcOpSigner};
pub use signing::{sign_dag_cbor, verify_dag_cbor, SignedCommit};
+47
View File
@@ -0,0 +1,47 @@
use anyhow::Result;
use multibase::{decode as mb_decode, encode as mb_encode, Base};
pub fn encode_b58btc(bytes: &[u8]) -> String {
mb_encode(Base::Base58Btc, bytes)
}
pub fn encode_b64url(bytes: &[u8]) -> String {
use base64::Engine;
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes)
}
pub fn encode_b64std(bytes: &[u8]) -> String {
use base64::Engine;
base64::engine::general_purpose::STANDARD.encode(bytes)
}
pub fn decode_multibase(s: &str) -> Result<Vec<u8>> {
let (_, bytes) = mb_decode(s)?;
Ok(bytes)
}
pub fn decode_b64url(s: &str) -> Result<Vec<u8>> {
use base64::Engine;
Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(s.as_bytes())?)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn b58btc_roundtrip() {
let v = b"hello world";
let s = encode_b58btc(v);
let d = decode_multibase(&s).unwrap();
assert_eq!(d, v);
}
#[test]
fn b64url_roundtrip() {
let v = b"some bytes";
let s = encode_b64url(v);
let d = decode_b64url(&s).unwrap();
assert_eq!(d, v);
}
}
+133
View File
@@ -0,0 +1,133 @@
use anyhow::Result;
use k256::ecdsa::{signature::Signer, Signature as K256Signature, SigningKey};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use crate::cid::cid_for_cbor;
#[allow(unused_imports)]
use crate::did_key::verifying_key_to_multibase;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum PlcOperation {
#[serde(rename = "plc_tombstone")]
Tombstone { prev: Option<String> },
#[serde(rename = "plc_operation")]
Op {
prev: Option<String>,
sigs: Vec<String>,
#[serde(flatten)]
op: PlcOpInner,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlcOpInner {
#[serde(rename = "type")]
pub op_type: String,
pub services: serde_json::Value,
pub identifier: String,
pub rotation_keys: Vec<String>,
pub verification_methods: serde_json::Value,
pub also_known_as: Vec<String>,
}
impl PlcOperation {
pub fn create(
handle: &str,
signing_key: &SigningKey,
rotation_key_pub_mb: &str,
pds_endpoint: &str,
) -> Result<Self> {
let inner = create_unsigned_op(handle, rotation_key_pub_mb, pds_endpoint);
let sig = sign_op(signing_key, &inner)?;
Ok(Self::Op {
prev: None,
sigs: vec![sig],
op: inner,
})
}
}
pub fn create_unsigned_op(handle: &str, rotation_key_pub_mb: &str, pds_endpoint: &str) -> PlcOpInner {
PlcOpInner {
op_type: "plc_operation".into(),
identifier: handle.to_string(),
rotation_keys: vec![rotation_key_pub_mb.to_string()],
verification_methods: json!({
"atproto": format!("did:key:{}", rotation_key_pub_mb),
}),
also_known_as: vec![format!("at://{}", handle)],
services: json!({
"atproto_pds": {
"type": "AtprotoPersonalDataServer",
"endpoint": pds_endpoint,
}
}),
}
}
pub fn sign_op(signing_key: &SigningKey, op: &PlcOpInner) -> Result<String> {
let canonical = json!({
"type": op.op_type,
"identifier": op.identifier,
"rotationKeys": op.rotation_keys,
"verificationMethods": op.verification_methods,
"alsoKnownAs": op.also_known_as,
"services": op.services,
});
let mut buf = Vec::new();
ciborium::into_writer(&canonical, &mut buf)?;
let sig: K256Signature = signing_key.sign(&buf);
Ok(hex::encode(sig.to_bytes()))
}
pub trait PlcOpSigner {
fn sign(&self, op: &PlcOpInner) -> Result<String>;
}
pub struct K256PlcOpSigner<'a>(pub &'a SigningKey);
impl<'a> PlcOpSigner for K256PlcOpSigner<'a> {
fn sign(&self, op: &PlcOpInner) -> Result<String> {
sign_op(self.0, op)
}
}
pub fn op_cid(op: &Value) -> Result<String> {
let mut buf = Vec::new();
ciborium::into_writer(op, &mut buf)?;
Ok(cid_for_cbor(&buf)?.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use k256::SecretKey;
#[test]
fn create_op_signs_and_contains_handle() {
let sk = SecretKey::from_slice(&[3u8; 32]).unwrap();
let rot_mb = crate::did_key::pubkey_to_multibase(&sk.public_key()).unwrap();
let signing = SigningKey::from(sk);
let op = PlcOperation::create(
"alice.maarcadetweet.local",
&signing,
&rot_mb,
"https://pds.example",
)
.unwrap();
let serialized = serde_json::to_value(&op).unwrap();
let inner = serialized
.get("op")
.or_else(|| serialized.get("services").and_then(|_| Some(&serialized)))
.unwrap();
let identifier = inner
.get("identifier")
.unwrap()
.as_str()
.unwrap();
assert_eq!(identifier, "alice.maarcadetweet.local");
assert!(serialized.get("sigs").is_some());
}
}
+100
View File
@@ -0,0 +1,100 @@
use anyhow::Result;
use k256::ecdsa::{signature::Signer, Signature as K256Signature, SigningKey, VerifyingKey};
use serde_json::Value;
use crate::cid::cid_for_cbor;
#[allow(unused_imports)]
use crate::did_key::verifying_key_to_multibase;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SignedCommit {
pub cid: String,
pub signed_bytes: Vec<u8>,
}
pub fn sign_dag_cbor(
signing_key: &SigningKey,
payload: &Value,
) -> Result<SignedCommit> {
let mut buf = Vec::new();
let stripped = strip_dag_cbor_signing_bytes(payload)?;
ciborium::into_writer(&stripped, &mut buf)?;
let sig: K256Signature = signing_key.sign(&buf);
let sig_low = sig.normalize_s().unwrap_or(sig);
let sig_bytes = sig_low.to_bytes();
let mut final_doc = stripped.clone();
if let Some(obj) = final_doc.as_object_mut() {
obj.insert("sig".into(), Value::String(hex::encode(sig_bytes)));
}
let mut final_buf = Vec::new();
ciborium::into_writer(&final_doc, &mut final_buf)?;
let cid = cid_for_cbor(&final_buf)?;
Ok(SignedCommit {
cid: cid.to_string(),
signed_bytes: final_buf,
})
}
pub fn verify_dag_cbor(signed_bytes: &[u8]) -> Result<VerifyingKey> {
use k256::ecdsa::signature::Verifier;
let value: Value = ciborium::from_reader(signed_bytes)?;
let obj = value
.as_object()
.ok_or_else(|| anyhow::anyhow!("not an object"))?;
let sig_hex = obj
.get("sig")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("no sig"))?;
let sig_bytes = hex::decode(sig_hex)?;
let sig = K256Signature::try_from(sig_bytes.as_slice())?;
let mut without_sig = obj.clone();
without_sig.remove("sig");
let mut unsigned_buf = Vec::new();
ciborium::into_writer(&Value::Object(without_sig), &mut unsigned_buf)?;
let pk_bytes = obj
.get("pubkey")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("no pubkey"))?;
let pk = crate::did_key::pubkey_from_multibase(pk_bytes)?;
let vk = VerifyingKey::from(&pk);
vk.verify(&unsigned_buf, &sig)?;
Ok(vk)
}
fn strip_dag_cbor_signing_bytes(v: &Value) -> Result<Value> {
let mut out = v.clone();
if let Some(obj) = out.as_object_mut() {
obj.remove("sig");
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use k256::SecretKey;
use serde_json::json;
#[test]
fn sign_and_verify_commit() {
let sk_bytes = [7u8; 32];
let sk = SecretKey::from_slice(&sk_bytes).unwrap();
let mb = crate::did_key::pubkey_to_multibase(&sk.public_key()).unwrap();
let signing = SigningKey::from(sk);
let payload = json!({
"did": "did:plc:abc",
"version": 3,
"prev": null,
"data": { "test": true },
"pubkey": mb,
});
let signed = sign_dag_cbor(&signing, &payload).unwrap();
assert!(signed.cid.starts_with("bafy"));
verify_dag_cbor(&signed.signed_bytes).unwrap();
}
}