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:
@@ -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 }
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user