Handle previous operation

+1
.gitignore
··· 1 target/ 2 cli/history.txt 3 cli/operation.json
··· 1 target/ 2 cli/history.txt 3 cli/operation.json 4 + cli/previous_operation.json
+4 -2
Cargo.lock
··· 254 [[package]] 255 name = "cid" 256 version = "0.11.1" 257 - source = "git+https://github.com/edouardparis/rust-cid?branch=fix-dep-serde_bytes-features#d8aeda78e2dd4784dbea3086a6ca85fa88782d2d" 258 dependencies = [ 259 "core2", 260 "multibase", ··· 1265 [[package]] 1266 name = "serde_ipld_dagcbor" 1267 version = "0.6.3" 1268 - source = "git+http://github.com/edouardparis/serde_ipld_dagcbor?branch=scopeguard-no-default-features#82eca93ee126750ad80feedbfe9cee0bf162c587" 1269 dependencies = [ 1270 "cbor4ii", 1271 "ipld-core", ··· 1612 version = "0.0.1" 1613 dependencies = [ 1614 "base58", 1615 "hex-literal", 1616 "k256", 1617 "postcard", ··· 1628 "base32", 1629 "base58", 1630 "base64", 1631 "clap", 1632 "hidapi", 1633 "ledger-transport-hid",
··· 254 [[package]] 255 name = "cid" 256 version = "0.11.1" 257 + source = "git+https://github.com/multiformats/rust-cid?branch=master#6a13cb931d3237e5e1f5635a943edfd166e2e78c" 258 dependencies = [ 259 "core2", 260 "multibase", ··· 1265 [[package]] 1266 name = "serde_ipld_dagcbor" 1267 version = "0.6.3" 1268 + source = "git+http://github.com/ipld/serde_ipld_dagcbor?branch=master#d7f93d06ce6a19867abeea78d02d6ded6c476b81" 1269 dependencies = [ 1270 "cbor4ii", 1271 "ipld-core", ··· 1612 version = "0.0.1" 1613 dependencies = [ 1614 "base58", 1615 + "cid", 1616 "hex-literal", 1617 "k256", 1618 "postcard", ··· 1629 "base32", 1630 "base58", 1631 "base64", 1632 + "cid", 1633 "clap", 1634 "hidapi", 1635 "ledger-transport-hid",
+3 -2
Cargo.toml
··· 11 12 [dependencies] 13 k256 = { version = "0.13.4", default-features = false, features = ["alloc", "ecdsa-core"] } 14 postcard = { version = "1.1.1", features = ["alloc"] } 15 - serde_ipld_dagcbor = { git = "http://github.com/edouardparis/serde_ipld_dagcbor", branch = "scopeguard-no-default-features", default-features = false } 16 sdk = { package = "vanadium-app-sdk", git = "https://github.com/LedgerHQ/vanadium"} 17 common = { package = "vnd-atproto-common", path = "common" } 18 base58 = "0.2.0" 19 serde = { version = "1.0", default-features = false, features = ["alloc"] } 20 21 [patch.crates-io] 22 - cid = { git = "https://github.com/edouardparis/rust-cid", branch = "fix-dep-serde_bytes-features" } 23 24 [dev-dependencies] 25 hex-literal = "0.4.1"
··· 11 12 [dependencies] 13 k256 = { version = "0.13.4", default-features = false, features = ["alloc", "ecdsa-core"] } 14 + cid = { version = "0.11.1", default-features = false, features = ['alloc'] } 15 postcard = { version = "1.1.1", features = ["alloc"] } 16 + serde_ipld_dagcbor = { git = "http://github.com/ipld/serde_ipld_dagcbor", branch = "master", default-features = false } 17 sdk = { package = "vanadium-app-sdk", git = "https://github.com/LedgerHQ/vanadium"} 18 common = { package = "vnd-atproto-common", path = "common" } 19 base58 = "0.2.0" 20 serde = { version = "1.0", default-features = false, features = ["alloc"] } 21 22 [patch.crates-io] 23 + cid = { git = "https://github.com/multiformats/rust-cid", branch = "master" } 24 25 [dev-dependencies] 26 hex-literal = "0.4.1"
+2 -1
cli/Cargo.toml
··· 14 shellwords = "1.1.0" 15 hidapi = "2.6.3" 16 ledger-transport-hid = "0.11.0" 17 - serde_ipld_dagcbor = { git = "http://github.com/edouardparis/serde_ipld_dagcbor", branch = "scopeguard-no-default-features", default-features = false } 18 serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] } 19 serde_json = { version = "1.0", default-features = false, features = ["alloc"] } 20 tokio = { version = "1.38.1", features = ["io-util", "macros", "net", "rt", "rt-multi-thread", "sync"] }
··· 14 shellwords = "1.1.0" 15 hidapi = "2.6.3" 16 ledger-transport-hid = "0.11.0" 17 + cid = { version = "0.11.1", default-features = false } 18 + serde_ipld_dagcbor = { git = "http://github.com/ipld/serde_ipld_dagcbor", branch = "master", default-features = false } 19 serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] } 20 serde_json = { version = "1.0", default-features = false, features = ["alloc"] } 21 tokio = { version = "1.38.1", features = ["io-util", "macros", "net", "rt", "rt-multi-thread", "sync"] }
+20
cli/previous_operation.json
···
··· 1 + { 2 + "alsoKnownAs": [ 3 + "at://test-vnd.edouard.paris" 4 + ], 5 + "prev": null, 6 + "rotationKeys": [ 7 + "did:key:zQ3shiNS7Bm7F3AH5NU4uimiSpZ8ryYj365Uq1N7KMsWUaXKp" 8 + ], 9 + "services": { 10 + "atproto_pds": { 11 + "endpoint": "https://pds.edouard.paris", 12 + "type": "AtprotoPersonalDataServer" 13 + } 14 + }, 15 + "sig": "kfShJ7A67jZAjZhydcnz3HRnrlE0NqocV2RmjRNlCB5hyj4Qiw51Kn_Bkqj704KYOTEiyhpiVajRc-qVc9ssWw", 16 + "type": "plc_operation", 17 + "verificationMethods": { 18 + "atproto": "did:key:zQ3shiNS7Bm7F3AH5NU4uimiSpZ8ryYj365Uq1N7KMsWUaXKp" 19 + } 20 + }
+24 -15
cli/src/main.rs
··· 1 use base58::ToBase58; 2 use base64::Engine; 3 use clap::{CommandFactory, Parser, Subcommand}; 4 use rustyline::completion::{Completer, Pair}; 5 use rustyline::error::ReadlineError; ··· 18 AtprotoAppClient, 19 }; 20 21 - use serde::{Deserialize, Serialize}; 22 use std::borrow::Cow; 23 use std::fs::File; 24 use std::io::Read; ··· 51 previous: Option<String>, 52 #[clap(long, default_missing_value = "true", num_args = 0..=1)] 53 new_plc_did: bool, 54 }, 55 Exit, 56 } ··· 209 previous, 210 new_plc_did, 211 } => { 212 - let operation = read_operation_file(&operation)?; 213 - let previous = if let Some(path) = previous { 214 - Some(read_operation_file(&path)?) 215 } else { 216 None 217 }; ··· 221 .map(|s| base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(s))?; 222 println!("sig: {}", sig); 223 if *new_plc_did { 224 - let b = serde_ipld_dagcbor::to_vec(&SignedPlcOperation { operation, sig }).unwrap(); 225 let hash = sha2::Sha256::digest(b); 226 let s = base32::encode(base32::Alphabet::Rfc4648Lower { padding: true }, &hash); 227 eprintln!("did:plc:{}", &s[..24]); 228 } 229 } 230 CliCommand::Exit => { 231 app_client.exit().await?; ··· 235 Ok(()) 236 } 237 238 - #[derive(Serialize)] 239 - pub struct SignedPlcOperation { 240 - #[serde(flatten)] 241 - operation: client::PlcOperation, 242 - sig: String, 243 - } 244 - 245 - fn read_operation_file(path: &str) -> Result<client::PlcOperation, Box<dyn std::error::Error>> { 246 let mut file = File::open(path)?; 247 let mut contents = String::new(); 248 file.read_to_string(&mut contents)?; 249 - let operation: client::PlcOperation = serde_json::from_str(&contents)?; 250 - Ok(operation) 251 } 252 253 #[tokio::main(flavor = "multi_thread")]
··· 1 use base58::ToBase58; 2 use base64::Engine; 3 + use cid::multihash; 4 use clap::{CommandFactory, Parser, Subcommand}; 5 use rustyline::completion::{Completer, Pair}; 6 use rustyline::error::ReadlineError; ··· 19 AtprotoAppClient, 20 }; 21 22 use std::borrow::Cow; 23 use std::fs::File; 24 use std::io::Read; ··· 51 previous: Option<String>, 52 #[clap(long, default_missing_value = "true", num_args = 0..=1)] 53 new_plc_did: bool, 54 + }, 55 + GetCID { 56 + /// Path to operation json file 57 + #[clap(long)] 58 + operation: String, 59 }, 60 Exit, 61 } ··· 214 previous, 215 new_plc_did, 216 } => { 217 + let operation: client::PlcOperation = read_json_file(&operation)?; 218 + let previous: Option<client::SignedPlcOperation> = if let Some(path) = previous { 219 + Some(read_json_file(&path)?) 220 } else { 221 None 222 }; ··· 226 .map(|s| base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(s))?; 227 println!("sig: {}", sig); 228 if *new_plc_did { 229 + let b = serde_ipld_dagcbor::to_vec(&operation.signed(sig)).unwrap(); 230 let hash = sha2::Sha256::digest(b); 231 let s = base32::encode(base32::Alphabet::Rfc4648Lower { padding: true }, &hash); 232 eprintln!("did:plc:{}", &s[..24]); 233 } 234 + } 235 + CliCommand::GetCID { operation } => { 236 + let operation: client::SignedPlcOperation = read_json_file(&operation)?; 237 + let b = serde_ipld_dagcbor::to_vec(&operation).unwrap(); 238 + let digest = sha2::Sha256::digest(b); 239 + let wrap = multihash::Multihash::wrap(0x12, &digest).unwrap(); 240 + let cid = cid::Cid::new(cid::Version::V1, 0x71, wrap).unwrap(); 241 + eprintln!("cid: {}", cid); 242 } 243 CliCommand::Exit => { 244 app_client.exit().await?; ··· 248 Ok(()) 249 } 250 251 + fn read_json_file<T>(path: &str) -> Result<T, Box<dyn std::error::Error>> 252 + where 253 + T: serde::de::DeserializeOwned, 254 + { 255 let mut file = File::open(path)?; 256 let mut contents = String::new(); 257 file.read_to_string(&mut contents)?; 258 + let data: T = serde_json::from_str(&contents)?; 259 + Ok(data) 260 } 261 262 #[tokio::main(flavor = "multi_thread")]
+1 -1
client/Cargo.toml
··· 7 postcard = { version = "1.1.1", features = ["alloc"] } 8 common = { package = "vnd-atproto-common", path = "../common"} 9 sdk = { package = "vanadium-client-sdk", git = "https://github.com/LedgerHQ/vanadium"} 10 - serde_ipld_dagcbor = { git = "http://github.com/edouardparis/serde_ipld_dagcbor", branch = "scopeguard-no-default-features", default-features = false }
··· 7 postcard = { version = "1.1.1", features = ["alloc"] } 8 common = { package = "vnd-atproto-common", path = "../common"} 9 sdk = { package = "vanadium-client-sdk", git = "https://github.com/LedgerHQ/vanadium"} 10 + serde_ipld_dagcbor = { git = "http://github.com/ipld/serde_ipld_dagcbor", branch = "master", default-features = false }
+4 -3
client/src/lib.rs
··· 1 - pub use common::message::PlcOperation; 2 use common::message::{Request, Response}; 3 use sdk::vanadium_client::{VAppClient, VAppExecutionError}; 4 ··· 94 &mut self, 95 key_index: u32, 96 operation: PlcOperation, 97 - previous: Option<PlcOperation>, 98 ) -> Result<Vec<u8>, AtprotoAppClientError> { 99 let msg = postcard::to_allocvec(&Request::SignPlcOperation { 100 key_index, 101 previous, 102 operation, 103 }) 104 - .map_err(|_| { 105 AtprotoAppClientError::GenericError("Failed to serialize SignPlcOperation request") 106 })?; 107
··· 1 + pub use common::message::{PlcOperation, SignedPlcOperation}; 2 use common::message::{Request, Response}; 3 use sdk::vanadium_client::{VAppClient, VAppExecutionError}; 4 ··· 94 &mut self, 95 key_index: u32, 96 operation: PlcOperation, 97 + previous: Option<SignedPlcOperation>, 98 ) -> Result<Vec<u8>, AtprotoAppClientError> { 99 let msg = postcard::to_allocvec(&Request::SignPlcOperation { 100 key_index, 101 previous, 102 operation, 103 }) 104 + .map_err(|e| { 105 + eprintln!("{:?}", e); 106 AtprotoAppClientError::GenericError("Failed to serialize SignPlcOperation request") 107 })?; 108
+27 -1
common/src/message.rs
··· 11 }, 12 SignPlcOperation { 13 key_index: u32, 14 - previous: Option<PlcOperation>, 15 operation: PlcOperation, 16 }, 17 } 18 ··· 25 pub also_known_as: Vec<String>, 26 pub services: Services, 27 pub prev: Option<String>, 28 } 29 30 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
··· 11 }, 12 SignPlcOperation { 13 key_index: u32, 14 operation: PlcOperation, 15 + previous: Option<SignedPlcOperation>, 16 }, 17 } 18 ··· 25 pub also_known_as: Vec<String>, 26 pub services: Services, 27 pub prev: Option<String>, 28 + } 29 + 30 + impl PlcOperation { 31 + pub fn signed(self, sig: String) -> SignedPlcOperation { 32 + SignedPlcOperation { 33 + r#type: self.r#type, 34 + rotation_keys: self.rotation_keys, 35 + verification_methods: self.verification_methods, 36 + also_known_as: self.also_known_as, 37 + services: self.services, 38 + prev: self.prev, 39 + sig, 40 + } 41 + } 42 + } 43 + 44 + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] 45 + #[serde(rename_all = "camelCase")] 46 + pub struct SignedPlcOperation { 47 + pub r#type: String, 48 + pub rotation_keys: Vec<String>, 49 + pub verification_methods: VerificationMethods, 50 + pub also_known_as: Vec<String>, 51 + pub services: Services, 52 + pub prev: Option<String>, 53 + pub sig: String, 54 } 55 56 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
+95 -10
src/main.rs
··· 7 vec::Vec, 8 }; 9 use base58::ToBase58; 10 - use common::message::{PlcOperation, Request, Response}; 11 use sdk::{ 12 App, 13 curve::{Curve, EcfpPrivateKey, EcfpPublicKey, Secp256k1, ToPublicKey}, ··· 84 } 85 86 #[cfg(not(test))] 87 - fn operation_tags(operation: &PlcOperation) -> Vec<sdk::ux::TagValue> { 88 use sdk::ux::TagValue; 89 let mut tags: Vec<TagValue> = operation 90 .rotation_keys ··· 116 } 117 118 #[cfg(not(test))] 119 - fn display_full_operation(pubkey: &PublicDidKey, index: u32, operation: &PlcOperation) -> bool { 120 sdk::ux::review_pairs( 121 "Sign plc operation", 122 "", 123 - &operation_tags(operation), 124 &alloc::format!("with key #{} {} ", index, pubkey.to_string()), 125 "Confirm", 126 false, ··· 128 } 129 130 #[cfg(test)] 131 fn display_full_operation(pubkey: &PublicDidKey, index: u32, operation: &PlcOperation) -> bool { 132 true 133 } 134 135 pub fn sign_plc_operation( 136 key_index: u32, 137 operation: PlcOperation, 138 - previous: Option<PlcOperation>, 139 ) -> Result<Response, &'static str> { 140 if key_index > 256 { 141 return Err("Index is too long"); ··· 149 let privkey: EcfpPrivateKey<Secp256k1, 32> = EcfpPrivateKey::new(*hd_node.privkey); 150 let did_key = PublicDidKey::new(&privkey.to_public_key()); 151 152 - if let Some(operation) = previous { 153 - if !operation.rotation_keys.contains(&did_key.to_string()) { 154 return Err("Key is not part of the previous operation rotation_keys"); 155 } 156 - // todo: check previous 157 - } 158 159 - if !display_full_operation(&did_key, key_index, &operation) { 160 return Err("Rejected by the user"); 161 } 162
··· 7 vec::Vec, 8 }; 9 use base58::ToBase58; 10 + use common::message::{PlcOperation, Request, Response, SignedPlcOperation}; 11 use sdk::{ 12 App, 13 curve::{Curve, EcfpPrivateKey, EcfpPublicKey, Secp256k1, ToPublicKey}, ··· 84 } 85 86 #[cfg(not(test))] 87 + fn all_operation_tags(operation: &PlcOperation) -> Vec<sdk::ux::TagValue> { 88 use sdk::ux::TagValue; 89 let mut tags: Vec<TagValue> = operation 90 .rotation_keys ··· 116 } 117 118 #[cfg(not(test))] 119 + fn changed_operation_tags( 120 + operation: &PlcOperation, 121 + previous: &SignedPlcOperation, 122 + ) -> Vec<sdk::ux::TagValue> { 123 + use sdk::ux::TagValue; 124 + let mut tags: Vec<TagValue> = Vec::new(); 125 + 126 + if operation.rotation_keys != previous.rotation_keys { 127 + tags = operation 128 + .rotation_keys 129 + .iter() 130 + .map(|value| TagValue { 131 + tag: "Rotation key:".into(), 132 + value: value.to_string(), 133 + }) 134 + .collect(); 135 + } 136 + 137 + if operation.verification_methods != previous.verification_methods { 138 + tags.push(TagValue { 139 + tag: "Verification method (atproto):".into(), 140 + value: operation.verification_methods.atproto.to_string(), 141 + }); 142 + } 143 + 144 + if operation.also_known_as != previous.also_known_as { 145 + for tag in operation.also_known_as.iter().map(|value| TagValue { 146 + tag: "Known as:".into(), 147 + value: value.to_string(), 148 + }) { 149 + tags.push(tag); 150 + } 151 + } 152 + 153 + if operation.services != previous.services { 154 + tags.push(TagValue { 155 + tag: "Service (atproto pds):".into(), 156 + value: operation.services.atproto_pds.endpoint.to_string(), 157 + }); 158 + } 159 + 160 + tags 161 + } 162 + 163 + #[cfg(not(test))] 164 + fn display_partial_operation( 165 + pubkey: &PublicDidKey, 166 + index: u32, 167 + operation: &PlcOperation, 168 + previous: &SignedPlcOperation, 169 + ) -> bool { 170 sdk::ux::review_pairs( 171 "Sign plc operation", 172 "", 173 + &changed_operation_tags(operation, previous), 174 &alloc::format!("with key #{} {} ", index, pubkey.to_string()), 175 "Confirm", 176 false, ··· 178 } 179 180 #[cfg(test)] 181 + fn display_partial_operation( 182 + _pubkey: &PublicDidKey, 183 + _index: u32, 184 + _operation: &PlcOperation, 185 + _previous: &PlcOperation, 186 + ) -> bool { 187 + true 188 + } 189 + 190 + #[cfg(not(test))] 191 fn display_full_operation(pubkey: &PublicDidKey, index: u32, operation: &PlcOperation) -> bool { 192 + sdk::ux::review_pairs( 193 + "Sign plc operation", 194 + "", 195 + &all_operation_tags(operation), 196 + &alloc::format!("with key #{} {} ", index, pubkey.to_string()), 197 + "Confirm", 198 + false, 199 + ) 200 + } 201 + 202 + #[cfg(test)] 203 + fn display_full_operation(_pubkey: &PublicDidKey, _index: u32, _operation: &PlcOperation) -> bool { 204 true 205 } 206 207 pub fn sign_plc_operation( 208 key_index: u32, 209 operation: PlcOperation, 210 + previous: Option<SignedPlcOperation>, 211 ) -> Result<Response, &'static str> { 212 if key_index > 256 { 213 return Err("Index is too long"); ··· 221 let privkey: EcfpPrivateKey<Secp256k1, 32> = EcfpPrivateKey::new(*hd_node.privkey); 222 let did_key = PublicDidKey::new(&privkey.to_public_key()); 223 224 + if let Some(previous) = previous { 225 + if !previous.rotation_keys.contains(&did_key.to_string()) { 226 return Err("Key is not part of the previous operation rotation_keys"); 227 } 228 229 + let msg = 230 + serde_ipld_dagcbor::to_vec(&previous).map_err(|_| "Failed to serialize operation")?; 231 + let mut hasher = sdk::hash::Sha256::new(); 232 + hasher.update(&msg); 233 + let mut digest = [0u8; 32]; 234 + hasher.digest(&mut digest); 235 + let wrap = cid::multihash::Multihash::wrap(0x12, &digest).unwrap(); 236 + let cid = cid::Cid::new(cid::Version::V1, 0x71, wrap).unwrap(); 237 + if operation.prev != Some(cid.to_string()) { 238 + return Err("Prev does not match the previous operation CID"); 239 + } 240 + 241 + if !display_partial_operation(&did_key, key_index, &operation, &previous) { 242 + return Err("Rejected by the user"); 243 + } 244 + } else if !display_full_operation(&did_key, key_index, &operation) { 245 return Err("Rejected by the user"); 246 } 247