Compare changes

Choose any two refs to compare.

+2
.gitignore
···
target/
cli/history.txt
···
target/
cli/history.txt
+
cli/operation.json
+
cli/previous_operation.json
+22 -2
Cargo.lock
···
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
[[package]]
name = "base58"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6107fe1be6682a68940da878d9e9f5e90ca5745b3dec9fd1bb393c8777d4f581"
[[package]]
name = "base64ct"
···
[[package]]
name = "cid"
version = "0.11.1"
-
source = "git+https://github.com/edouardparis/rust-cid?branch=fix-dep-serde_bytes-features#d8aeda78e2dd4784dbea3086a6ca85fa88782d2d"
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"
dependencies = [
"cbor4ii",
"ipld-core",
···
version = "0.0.1"
dependencies = [
"base58",
"hex-literal",
"postcard",
"serde",
"serde_ipld_dagcbor",
···
name = "vnd-atproto-cli"
version = "0.0.1"
dependencies = [
"base58",
"clap",
"hidapi",
"ledger-transport-hid",
"rustyline",
"serde",
"shellwords",
"tokio",
"vnd-atproto-client",
···
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
[[package]]
+
name = "base32"
+
version = "0.5.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076"
+
+
[[package]]
name = "base58"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6107fe1be6682a68940da878d9e9f5e90ca5745b3dec9fd1bb393c8777d4f581"
+
+
[[package]]
+
name = "base64"
+
version = "0.22.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
···
[[package]]
name = "cid"
version = "0.11.1"
+
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/ipld/serde_ipld_dagcbor?branch=master#d7f93d06ce6a19867abeea78d02d6ded6c476b81"
dependencies = [
"cbor4ii",
"ipld-core",
···
version = "0.0.1"
dependencies = [
"base58",
+
"cid",
"hex-literal",
+
"k256",
"postcard",
"serde",
"serde_ipld_dagcbor",
···
name = "vnd-atproto-cli"
version = "0.0.1"
dependencies = [
+
"base32",
"base58",
+
"base64",
+
"cid",
"clap",
"hidapi",
"ledger-transport-hid",
"rustyline",
"serde",
+
"serde_ipld_dagcbor",
+
"serde_json",
+
"sha2",
"shellwords",
"tokio",
"vnd-atproto-client",
+4 -2
Cargo.toml
···
stack_size = 65536
[dependencies]
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 }
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" }
[dev-dependencies]
hex-literal = "0.4.1"
···
stack_size = 65536
[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/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/multiformats/rust-cid", branch = "master" }
[dev-dependencies]
hex-literal = "0.4.1"
+6
cli/Cargo.toml
···
[dependencies]
base58 = "0.2.0"
client = { package = "vnd-atproto-client", path = "../client"}
clap = { version = "4.5.31", features = ["derive"] }
rustyline = "15.0.0"
shellwords = "1.1.0"
hidapi = "2.6.3"
ledger-transport-hid = "0.11.0"
serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] }
tokio = { version = "1.38.1", features = ["io-util", "macros", "net", "rt", "rt-multi-thread", "sync"] }
···
[dependencies]
base58 = "0.2.0"
+
base32 = "0.5.1"
+
base64 = "0.22.1"
+
sha2 = "0.10.8"
client = { package = "vnd-atproto-client", path = "../client"}
clap = { version = "4.5.31", features = ["derive"] }
rustyline = "15.0.0"
shellwords = "1.1.0"
hidapi = "2.6.3"
ledger-transport-hid = "0.11.0"
+
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"
+
}
+
}
+66
cli/src/main.rs
···
use base58::ToBase58;
use clap::{CommandFactory, Parser, Subcommand};
use rustyline::completion::{Completer, Pair};
use rustyline::error::ReadlineError;
···
use rustyline::hint::Hinter;
use rustyline::validate::{ValidationContext, ValidationResult, Validator};
use rustyline::{Context, Editor, Helper};
use hidapi::HidApi;
use ledger_transport_hid::TransportNativeHID;
···
};
use std::borrow::Cow;
use std::sync::Arc;
#[derive(Parser, Debug)]
···
key_index: Option<u32>,
#[clap(long, default_missing_value = "true", num_args = 0..=1)]
display: bool,
},
Exit,
}
···
.await?;
println!("did:key:z{}", key.to_base58());
}
CliCommand::Exit => {
return Err("Exiting".into());
}
}
Ok(())
}
#[tokio::main(flavor = "multi_thread")]
···
use base58::ToBase58;
+
use base64::Engine;
+
use cid::multihash;
use clap::{CommandFactory, Parser, Subcommand};
use rustyline::completion::{Completer, Pair};
use rustyline::error::ReadlineError;
···
use rustyline::hint::Hinter;
use rustyline::validate::{ValidationContext, ValidationResult, Validator};
use rustyline::{Context, Editor, Helper};
+
use sha2::Digest;
use hidapi::HidApi;
use ledger_transport_hid::TransportNativeHID;
···
};
use std::borrow::Cow;
+
use std::fs::File;
+
use std::io::Read;
use std::sync::Arc;
#[derive(Parser, Debug)]
···
key_index: Option<u32>,
#[clap(long, default_missing_value = "true", num_args = 0..=1)]
display: bool,
+
},
+
SignPlcOperation {
+
#[clap(long)]
+
key_index: Option<u32>,
+
/// Path to operation json file
+
#[clap(long)]
+
operation: String,
+
/// Path to previous operation json file
+
#[clap(long)]
+
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,
}
···
.await?;
println!("did:key:z{}", key.to_base58());
}
+
CliCommand::SignPlcOperation {
+
key_index,
+
operation,
+
previous,
+
new_plc_did,
+
} => {
+
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
+
};
+
let sig = app_client
+
.sign_plc_operation(key_index.unwrap_or(0), operation.clone(), previous)
+
.await
+
.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(&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?;
return Err("Exiting".into());
}
}
Ok(())
+
}
+
+
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 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 }
···
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/ipld/serde_ipld_dagcbor", branch = "master", default-features = false }
+24
client/src/lib.rs
···
use common::message::{Request, Response};
use sdk::vanadium_client::{VAppClient, VAppExecutionError};
···
let response_raw = self.send_message(&msg).await?;
match Self::parse_response(&response_raw).await? {
Response::DidKey(key) => Ok(key),
_ => Err(AtprotoAppClientError::InvalidResponse("Invalid response")),
}
}
···
+
pub use common::message::{PlcOperation, SignedPlcOperation};
use common::message::{Request, Response};
use sdk::vanadium_client::{VAppClient, VAppExecutionError};
···
let response_raw = self.send_message(&msg).await?;
match Self::parse_response(&response_raw).await? {
Response::DidKey(key) => Ok(key),
+
_ => Err(AtprotoAppClientError::InvalidResponse("Invalid response")),
+
}
+
}
+
+
pub async fn sign_plc_operation(
+
&mut self,
+
key_index: u32,
+
operation: PlcOperation,
+
previous: Option<SignedPlcOperation>,
+
) -> Result<Vec<u8>, AtprotoAppClientError> {
+
let msg = postcard::to_allocvec(&Request::SignPlcOperation {
+
key_index,
+
previous,
+
operation,
+
})
+
.map_err(|e| {
+
eprintln!("{:?}", e);
+
AtprotoAppClientError::GenericError("Failed to serialize SignPlcOperation request")
+
})?;
+
+
let response_raw = self.send_message(&msg).await?;
+
match Self::parse_response(&response_raw).await? {
+
Response::Signature(sig) => Ok(sig),
_ => Err(AtprotoAppClientError::InvalidResponse("Invalid response")),
}
}
+64 -8
common/src/message.rs
···
use alloc::{string::String, vec::Vec};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub enum Request {
Exit,
-
GetDidKey { index: u32, display: bool },
-
// SignPlcOperation {
-
// key_index: u32,
-
// previous: Option<PlcOperation>,
-
// operation: PlcOperation,
-
// },
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
-
pub enum PlcOperation {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub enum Response {
DidKey(Vec<u8>),
-
// Signature(Vec<u8>),
Error(String),
}
···
use alloc::{string::String, vec::Vec};
use serde::{Deserialize, Serialize};
+
#[allow(clippy::large_enum_variant)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub enum Request {
Exit,
+
GetDidKey {
+
index: u32,
+
display: bool,
+
},
+
SignPlcOperation {
+
key_index: u32,
+
operation: PlcOperation,
+
previous: Option<SignedPlcOperation>,
+
},
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
+
#[serde(rename_all = "camelCase")]
+
pub struct PlcOperation {
+
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>,
+
}
+
+
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)]
+
#[serde(rename_all = "camelCase")]
+
pub struct VerificationMethods {
+
pub atproto: String,
+
}
+
+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
+
pub struct Services {
+
pub atproto_pds: Service,
+
}
+
+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
+
#[serde(rename_all = "camelCase")]
+
pub struct Service {
+
pub r#type: String,
+
pub endpoint: String,
+
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub enum Response {
DidKey(Vec<u8>),
+
Signature(Vec<u8>),
Error(String),
}
+214 -18
src/main.rs
···
extern crate alloc;
-
use alloc::{string::ToString, vec::Vec};
-
use common::message::{Request, Response};
use sdk::{
App,
-
curve::{Curve, EcfpPrivateKey, Secp256k1, ToPublicKey},
};
sdk::bootstrap!();
const HARDENED_PATH: u32 = '๐Ÿฆ‹' as u32 | (1 << 31);
#[cfg(not(test))]
-
fn display_key(pubkey: &[u8], index: u32) -> bool {
-
use alloc::string::ToString;
-
use base58::ToBase58;
use sdk::ux::TagValue;
-
-
let did_key = alloc::format!("did:key:z{}", pubkey.to_base58());
sdk::ux::review_pairs(
"Verify Atproto DID key",
···
value: alloc::format!("m/{}'/{}", '๐Ÿฆ‹' as u32, index),
},
],
-
&did_key,
"Confirm",
false,
)
}
#[cfg(test)]
-
fn display_key(pubkey: &[u8], index: u32) -> bool {
true
}
pub fn get_did_key(index: u32, display: bool) -> Result<Response, &'static str> {
if index > 256 {
-
return Err("Derivation path is too long");
}
let hd_node = sdk::curve::Secp256k1::derive_hd_node(&[HARDENED_PATH, index])?;
let privkey: EcfpPrivateKey<Secp256k1, 32> = EcfpPrivateKey::new(*hd_node.privkey);
let pubkey = privkey.to_public_key();
-
let uncompressed_pubkey = pubkey.as_ref().to_bytes();
-
-
let mut key = Vec::with_capacity(34);
-
key.push(0xE7);
-
key.push(uncompressed_pubkey[64] % 2 + 0x02);
-
key.extend_from_slice(&uncompressed_pubkey[1..33]);
if display && !display_key(&key, index) {
return Err("Rejected by the user");
}
-
Ok(Response::DidKey(key))
}
fn process(_app: &mut App, request: &[u8]) -> Vec<u8> {
···
Request::GetDidKey { index, display } => {
get_did_key(index, display).unwrap_or_else(|e| Response::Error(e.to_string()))
}
};
postcard::to_allocvec(&response).unwrap()
}
···
extern crate alloc;
+
use alloc::{
+
string::{String, ToString},
+
vec::Vec,
+
};
+
use base58::ToBase58;
+
use common::message::{PlcOperation, Request, Response, SignedPlcOperation};
use sdk::{
App,
+
curve::{Curve, EcfpPrivateKey, EcfpPublicKey, Secp256k1, ToPublicKey},
+
hash::Hasher,
};
sdk::bootstrap!();
const HARDENED_PATH: u32 = '๐Ÿฆ‹' as u32 | (1 << 31);
+
struct PublicDidKey(Vec<u8>);
+
+
impl PublicDidKey {
+
fn new(pubkey: &EcfpPublicKey<Secp256k1, 32>) -> Self {
+
let bytes = pubkey.as_ref().to_bytes();
+
let mut key = Vec::with_capacity(34);
+
key.push(0xE7);
+
key.push(0x01);
+
key.push(bytes[64] % 2 + 0x02);
+
key.extend_from_slice(&bytes[1..33]);
+
PublicDidKey(key)
+
}
+
}
+
+
impl ToString for PublicDidKey {
+
fn to_string(&self) -> String {
+
alloc::format!("did:key:z{}", self.0.to_base58())
+
}
+
}
+
#[cfg(not(test))]
+
fn display_key(pubkey: &PublicDidKey, index: u32) -> bool {
use sdk::ux::TagValue;
sdk::ux::review_pairs(
"Verify Atproto DID key",
···
value: alloc::format!("m/{}'/{}", '๐Ÿฆ‹' as u32, index),
},
],
+
&pubkey.to_string(),
"Confirm",
false,
)
}
#[cfg(test)]
+
fn display_key(pubkey: &PublicDidKey, index: u32) -> bool {
true
}
pub fn get_did_key(index: u32, display: bool) -> Result<Response, &'static str> {
if index > 256 {
+
return Err("Index is too long");
}
let hd_node = sdk::curve::Secp256k1::derive_hd_node(&[HARDENED_PATH, index])?;
let privkey: EcfpPrivateKey<Secp256k1, 32> = EcfpPrivateKey::new(*hd_node.privkey);
let pubkey = privkey.to_public_key();
+
let key = PublicDidKey::new(&pubkey);
if display && !display_key(&key, index) {
return Err("Rejected by the user");
}
+
Ok(Response::DidKey(key.0))
+
}
+
+
#[cfg(not(test))]
+
fn all_operation_tags(operation: &PlcOperation) -> Vec<sdk::ux::TagValue> {
+
use sdk::ux::TagValue;
+
let mut tags: Vec<TagValue> = operation
+
.rotation_keys
+
.iter()
+
.map(|value| TagValue {
+
tag: "Rotation key:".into(),
+
value: value.to_string(),
+
})
+
.collect();
+
+
tags.push(TagValue {
+
tag: "Verification method (atproto):".into(),
+
value: operation.verification_methods.atproto.to_string(),
+
});
+
+
for tag in operation.also_known_as.iter().map(|value| TagValue {
+
tag: "Known as:".into(),
+
value: value.to_string(),
+
}) {
+
tags.push(tag);
+
}
+
+
tags.push(TagValue {
+
tag: "Service (atproto pds):".into(),
+
value: operation.services.atproto_pds.endpoint.to_string(),
+
});
+
+
tags
+
}
+
+
#[cfg(not(test))]
+
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",
+
"",
+
&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<SignedPlcOperation>,
+
) -> Result<Response, &'static str> {
+
if key_index > 256 {
+
return Err("Index is too long");
+
}
+
+
if operation.r#type != "plc_operation" {
+
return Err("Wrong payload type");
+
}
+
+
let hd_node = sdk::curve::Secp256k1::derive_hd_node(&[HARDENED_PATH, key_index])?;
+
let privkey: EcfpPrivateKey<Secp256k1, 32> = EcfpPrivateKey::new(*hd_node.privkey);
+
let did_key = PublicDidKey::new(&privkey.to_public_key());
+
+
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");
+
}
+
+
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");
+
}
+
+
let msg =
+
serde_ipld_dagcbor::to_vec(&operation).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 sig = privkey.ecdsa_sign_hash(&digest).unwrap();
+
let sig = k256::ecdsa::Signature::from_der(&sig).unwrap();
+
Ok(Response::Signature(sig.to_bytes().to_vec()))
}
fn process(_app: &mut App, request: &[u8]) -> Vec<u8> {
···
Request::GetDidKey { index, display } => {
get_did_key(index, display).unwrap_or_else(|e| Response::Error(e.to_string()))
}
+
Request::SignPlcOperation {
+
key_index,
+
previous,
+
operation,
+
} => sign_plc_operation(key_index, operation, previous)
+
.unwrap_or_else(|e| Response::Error(e.to_string())),
};
postcard::to_allocvec(&response).unwrap()
}