Handle previous operation

+1
.gitignore
···
target/
cli/history.txt
cli/operation.json
+
cli/previous_operation.json
+4 -2
Cargo.lock
···
[[package]]
name = "cid"
version = "0.11.1"
-
source = "git+https://github.com/edouardparis/rust-cid?branch=fix-dep-serde_bytes-features#d8aeda78e2dd4784dbea3086a6ca85fa88782d2d"
+
source = "git+https://github.com/multiformats/rust-cid?branch=master#6a13cb931d3237e5e1f5635a943edfd166e2e78c"
dependencies = [
"core2",
"multibase",
···
[[package]]
name = "serde_ipld_dagcbor"
version = "0.6.3"
-
source = "git+http://github.com/edouardparis/serde_ipld_dagcbor?branch=scopeguard-no-default-features#82eca93ee126750ad80feedbfe9cee0bf162c587"
+
source = "git+http://github.com/ipld/serde_ipld_dagcbor?branch=master#d7f93d06ce6a19867abeea78d02d6ded6c476b81"
dependencies = [
"cbor4ii",
"ipld-core",
···
version = "0.0.1"
dependencies = [
"base58",
+
"cid",
"hex-literal",
"k256",
"postcard",
···
"base32",
"base58",
"base64",
+
"cid",
"clap",
"hidapi",
"ledger-transport-hid",
+3 -2
Cargo.toml
···
[dependencies]
k256 = { version = "0.13.4", default-features = false, features = ["alloc", "ecdsa-core"] }
+
cid = { version = "0.11.1", default-features = false, features = ['alloc'] }
postcard = { version = "1.1.1", features = ["alloc"] }
-
serde_ipld_dagcbor = { git = "http://github.com/edouardparis/serde_ipld_dagcbor", branch = "scopeguard-no-default-features", default-features = false }
+
serde_ipld_dagcbor = { git = "http://github.com/ipld/serde_ipld_dagcbor", branch = "master", default-features = false }
sdk = { package = "vanadium-app-sdk", git = "https://github.com/LedgerHQ/vanadium"}
common = { package = "vnd-atproto-common", path = "common" }
base58 = "0.2.0"
serde = { version = "1.0", default-features = false, features = ["alloc"] }
[patch.crates-io]
-
cid = { git = "https://github.com/edouardparis/rust-cid", branch = "fix-dep-serde_bytes-features" }
+
cid = { git = "https://github.com/multiformats/rust-cid", branch = "master" }
[dev-dependencies]
hex-literal = "0.4.1"
+2 -1
cli/Cargo.toml
···
shellwords = "1.1.0"
hidapi = "2.6.3"
ledger-transport-hid = "0.11.0"
-
serde_ipld_dagcbor = { git = "http://github.com/edouardparis/serde_ipld_dagcbor", branch = "scopeguard-no-default-features", default-features = false }
+
cid = { version = "0.11.1", default-features = false }
+
serde_ipld_dagcbor = { git = "http://github.com/ipld/serde_ipld_dagcbor", branch = "master", default-features = false }
serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] }
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
tokio = { version = "1.38.1", features = ["io-util", "macros", "net", "rt", "rt-multi-thread", "sync"] }
+20
cli/previous_operation.json
···
+
{
+
"alsoKnownAs": [
+
"at://test-vnd.edouard.paris"
+
],
+
"prev": null,
+
"rotationKeys": [
+
"did:key:zQ3shiNS7Bm7F3AH5NU4uimiSpZ8ryYj365Uq1N7KMsWUaXKp"
+
],
+
"services": {
+
"atproto_pds": {
+
"endpoint": "https://pds.edouard.paris",
+
"type": "AtprotoPersonalDataServer"
+
}
+
},
+
"sig": "kfShJ7A67jZAjZhydcnz3HRnrlE0NqocV2RmjRNlCB5hyj4Qiw51Kn_Bkqj704KYOTEiyhpiVajRc-qVc9ssWw",
+
"type": "plc_operation",
+
"verificationMethods": {
+
"atproto": "did:key:zQ3shiNS7Bm7F3AH5NU4uimiSpZ8ryYj365Uq1N7KMsWUaXKp"
+
}
+
}
+24 -15
cli/src/main.rs
···
use base58::ToBase58;
use base64::Engine;
+
use cid::multihash;
use clap::{CommandFactory, Parser, Subcommand};
use rustyline::completion::{Completer, Pair};
use rustyline::error::ReadlineError;
···
AtprotoAppClient,
};
-
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::fs::File;
use std::io::Read;
···
previous: Option<String>,
#[clap(long, default_missing_value = "true", num_args = 0..=1)]
new_plc_did: bool,
+
},
+
GetCID {
+
/// Path to operation json file
+
#[clap(long)]
+
operation: String,
},
Exit,
}
···
previous,
new_plc_did,
} => {
-
let operation = read_operation_file(&operation)?;
-
let previous = if let Some(path) = previous {
-
Some(read_operation_file(&path)?)
+
let operation: client::PlcOperation = read_json_file(&operation)?;
+
let previous: Option<client::SignedPlcOperation> = if let Some(path) = previous {
+
Some(read_json_file(&path)?)
} else {
None
};
···
.map(|s| base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(s))?;
println!("sig: {}", sig);
if *new_plc_did {
-
let b = serde_ipld_dagcbor::to_vec(&SignedPlcOperation { operation, sig }).unwrap();
+
let b = serde_ipld_dagcbor::to_vec(&operation.signed(sig)).unwrap();
let hash = sha2::Sha256::digest(b);
let s = base32::encode(base32::Alphabet::Rfc4648Lower { padding: true }, &hash);
eprintln!("did:plc:{}", &s[..24]);
}
+
}
+
CliCommand::GetCID { operation } => {
+
let operation: client::SignedPlcOperation = read_json_file(&operation)?;
+
let b = serde_ipld_dagcbor::to_vec(&operation).unwrap();
+
let digest = sha2::Sha256::digest(b);
+
let wrap = multihash::Multihash::wrap(0x12, &digest).unwrap();
+
let cid = cid::Cid::new(cid::Version::V1, 0x71, wrap).unwrap();
+
eprintln!("cid: {}", cid);
}
CliCommand::Exit => {
app_client.exit().await?;
···
Ok(())
}
-
#[derive(Serialize)]
-
pub struct SignedPlcOperation {
-
#[serde(flatten)]
-
operation: client::PlcOperation,
-
sig: String,
-
}
-
-
fn read_operation_file(path: &str) -> Result<client::PlcOperation, Box<dyn std::error::Error>> {
+
fn read_json_file<T>(path: &str) -> Result<T, Box<dyn std::error::Error>>
+
where
+
T: serde::de::DeserializeOwned,
+
{
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
-
let operation: client::PlcOperation = serde_json::from_str(&contents)?;
-
Ok(operation)
+
let data: T = serde_json::from_str(&contents)?;
+
Ok(data)
}
#[tokio::main(flavor = "multi_thread")]
+1 -1
client/Cargo.toml
···
postcard = { version = "1.1.1", features = ["alloc"] }
common = { package = "vnd-atproto-common", path = "../common"}
sdk = { package = "vanadium-client-sdk", git = "https://github.com/LedgerHQ/vanadium"}
-
serde_ipld_dagcbor = { git = "http://github.com/edouardparis/serde_ipld_dagcbor", branch = "scopeguard-no-default-features", default-features = false }
+
serde_ipld_dagcbor = { git = "http://github.com/ipld/serde_ipld_dagcbor", branch = "master", default-features = false }
+4 -3
client/src/lib.rs
···
-
pub use common::message::PlcOperation;
+
pub use common::message::{PlcOperation, SignedPlcOperation};
use common::message::{Request, Response};
use sdk::vanadium_client::{VAppClient, VAppExecutionError};
···
&mut self,
key_index: u32,
operation: PlcOperation,
-
previous: Option<PlcOperation>,
+
previous: Option<SignedPlcOperation>,
) -> Result<Vec<u8>, AtprotoAppClientError> {
let msg = postcard::to_allocvec(&Request::SignPlcOperation {
key_index,
previous,
operation,
})
-
.map_err(|_| {
+
.map_err(|e| {
+
eprintln!("{:?}", e);
AtprotoAppClientError::GenericError("Failed to serialize SignPlcOperation request")
})?;
+27 -1
common/src/message.rs
···
},
SignPlcOperation {
key_index: u32,
-
previous: Option<PlcOperation>,
operation: PlcOperation,
+
previous: Option<SignedPlcOperation>,
},
}
···
pub also_known_as: Vec<String>,
pub services: Services,
pub prev: Option<String>,
+
}
+
+
impl PlcOperation {
+
pub fn signed(self, sig: String) -> SignedPlcOperation {
+
SignedPlcOperation {
+
r#type: self.r#type,
+
rotation_keys: self.rotation_keys,
+
verification_methods: self.verification_methods,
+
also_known_as: self.also_known_as,
+
services: self.services,
+
prev: self.prev,
+
sig,
+
}
+
}
+
}
+
+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
+
#[serde(rename_all = "camelCase")]
+
pub struct SignedPlcOperation {
+
pub r#type: String,
+
pub rotation_keys: Vec<String>,
+
pub verification_methods: VerificationMethods,
+
pub also_known_as: Vec<String>,
+
pub services: Services,
+
pub prev: Option<String>,
+
pub sig: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
+95 -10
src/main.rs
···
vec::Vec,
};
use base58::ToBase58;
-
use common::message::{PlcOperation, Request, Response};
+
use common::message::{PlcOperation, Request, Response, SignedPlcOperation};
use sdk::{
App,
curve::{Curve, EcfpPrivateKey, EcfpPublicKey, Secp256k1, ToPublicKey},
···
}
#[cfg(not(test))]
-
fn operation_tags(operation: &PlcOperation) -> Vec<sdk::ux::TagValue> {
+
fn all_operation_tags(operation: &PlcOperation) -> Vec<sdk::ux::TagValue> {
use sdk::ux::TagValue;
let mut tags: Vec<TagValue> = operation
.rotation_keys
···
}
#[cfg(not(test))]
-
fn display_full_operation(pubkey: &PublicDidKey, index: u32, operation: &PlcOperation) -> bool {
+
fn changed_operation_tags(
+
operation: &PlcOperation,
+
previous: &SignedPlcOperation,
+
) -> Vec<sdk::ux::TagValue> {
+
use sdk::ux::TagValue;
+
let mut tags: Vec<TagValue> = Vec::new();
+
+
if operation.rotation_keys != previous.rotation_keys {
+
tags = operation
+
.rotation_keys
+
.iter()
+
.map(|value| TagValue {
+
tag: "Rotation key:".into(),
+
value: value.to_string(),
+
})
+
.collect();
+
}
+
+
if operation.verification_methods != previous.verification_methods {
+
tags.push(TagValue {
+
tag: "Verification method (atproto):".into(),
+
value: operation.verification_methods.atproto.to_string(),
+
});
+
}
+
+
if operation.also_known_as != previous.also_known_as {
+
for tag in operation.also_known_as.iter().map(|value| TagValue {
+
tag: "Known as:".into(),
+
value: value.to_string(),
+
}) {
+
tags.push(tag);
+
}
+
}
+
+
if operation.services != previous.services {
+
tags.push(TagValue {
+
tag: "Service (atproto pds):".into(),
+
value: operation.services.atproto_pds.endpoint.to_string(),
+
});
+
}
+
+
tags
+
}
+
+
#[cfg(not(test))]
+
fn display_partial_operation(
+
pubkey: &PublicDidKey,
+
index: u32,
+
operation: &PlcOperation,
+
previous: &SignedPlcOperation,
+
) -> bool {
sdk::ux::review_pairs(
"Sign plc operation",
"",
-
&operation_tags(operation),
+
&changed_operation_tags(operation, previous),
&alloc::format!("with key #{} {} ", index, pubkey.to_string()),
"Confirm",
false,
···
}
#[cfg(test)]
+
fn display_partial_operation(
+
_pubkey: &PublicDidKey,
+
_index: u32,
+
_operation: &PlcOperation,
+
_previous: &PlcOperation,
+
) -> bool {
+
true
+
}
+
+
#[cfg(not(test))]
fn display_full_operation(pubkey: &PublicDidKey, index: u32, operation: &PlcOperation) -> bool {
+
sdk::ux::review_pairs(
+
"Sign plc operation",
+
"",
+
&all_operation_tags(operation),
+
&alloc::format!("with key #{} {} ", index, pubkey.to_string()),
+
"Confirm",
+
false,
+
)
+
}
+
+
#[cfg(test)]
+
fn display_full_operation(_pubkey: &PublicDidKey, _index: u32, _operation: &PlcOperation) -> bool {
true
}
pub fn sign_plc_operation(
key_index: u32,
operation: PlcOperation,
-
previous: Option<PlcOperation>,
+
previous: Option<SignedPlcOperation>,
) -> Result<Response, &'static str> {
if key_index > 256 {
return Err("Index is too long");
···
let privkey: EcfpPrivateKey<Secp256k1, 32> = EcfpPrivateKey::new(*hd_node.privkey);
let did_key = PublicDidKey::new(&privkey.to_public_key());
-
if let Some(operation) = previous {
-
if !operation.rotation_keys.contains(&did_key.to_string()) {
+
if let Some(previous) = previous {
+
if !previous.rotation_keys.contains(&did_key.to_string()) {
return Err("Key is not part of the previous operation rotation_keys");
}
-
// todo: check previous
-
}
-
if !display_full_operation(&did_key, key_index, &operation) {
+
let msg =
+
serde_ipld_dagcbor::to_vec(&previous).map_err(|_| "Failed to serialize operation")?;
+
let mut hasher = sdk::hash::Sha256::new();
+
hasher.update(&msg);
+
let mut digest = [0u8; 32];
+
hasher.digest(&mut digest);
+
let wrap = cid::multihash::Multihash::wrap(0x12, &digest).unwrap();
+
let cid = cid::Cid::new(cid::Version::V1, 0x71, wrap).unwrap();
+
if operation.prev != Some(cid.to_string()) {
+
return Err("Prev does not match the previous operation CID");
+
}
+
+
if !display_partial_operation(&did_key, key_index, &operation, &previous) {
+
return Err("Rejected by the user");
+
}
+
} else if !display_full_operation(&did_key, key_index, &operation) {
return Err("Rejected by the user");
}