+1
.gitignore
+1
.gitignore
+4
-2
Cargo.lock
+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
+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
+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
+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
+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
+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
+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
+27
-1
common/src/message.rs
···
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
+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