tangled
alpha
login
or
join now
natalie.sh
/
bspds-sandbox
forked from
lewis.moe/bspds-sandbox
0
fork
atom
PDS software with bells & whistles you didn’t even know you needed. will move this to its own account when ready.
0
fork
atom
overview
issues
pulls
pipelines
Use jacquard commit signing
lewis.moe
2 months ago
f95bdcae
ed3d8331
+187
-127
7 changed files
expand all
collapse all
unified
split
Cargo.lock
Cargo.toml
src
api
repo
record
utils.rs
tests
commit_signing.rs
common
mod.rs
import_with_verification.rs
jwt_security.rs
+1
Cargo.lock
···
6279
6279
"bs58",
6280
6280
"bytes",
6281
6281
"chrono",
6282
6282
+
"ciborium",
6282
6283
"cid",
6283
6284
"ctor",
6284
6285
"dotenvy",
+1
Cargo.toml
···
62
62
[features]
63
63
external-infra = []
64
64
[dev-dependencies]
65
65
+
ciborium = "0.2"
65
66
ctor = "0.6.3"
66
67
testcontainers = "0.26.2"
67
68
testcontainers-modules = { version = "0.14.0", features = ["postgres"] }
+15
-54
src/api/repo/record/utils.rs
···
2
2
use bytes::Bytes;
3
3
use cid::Cid;
4
4
use jacquard::types::{integer::LimitedU32, string::Tid};
5
5
+
use jacquard_repo::commit::Commit;
5
6
use jacquard_repo::storage::BlockStore;
6
6
-
use k256::ecdsa::{Signature, SigningKey, signature::Signer};
7
7
-
use serde::Serialize;
7
7
+
use k256::ecdsa::SigningKey;
8
8
use serde_json::json;
9
9
+
use std::str::FromStr;
9
10
use uuid::Uuid;
10
11
11
11
-
/*
12
12
-
* Why custom commit signing instead of jacquard's Commit::sign()?
13
13
-
*
14
14
-
* Jacquard previously had a bug in how it created unsigned bytes for signing:
15
15
-
* it set sig to empty bytes and serialized (6-field CBOR map), while the
16
16
-
* ATProto spec creates a struct *without* the sig field (5-field CBOR map).
17
17
-
* These produce different CBOR bytes, so signatures didn't verify with relays.
18
18
-
*
19
19
-
* The bug has been fixed in jacquard, but the fix is untested here.
20
20
-
* TODO: Switch back to jacquard's Commit::sign() and verify it works.
21
21
-
*/
22
22
-
23
23
-
#[derive(Serialize)]
24
24
-
struct UnsignedCommit<'a> {
25
25
-
data: Cid,
26
26
-
did: &'a str,
27
27
-
prev: Option<Cid>,
28
28
-
rev: &'a str,
29
29
-
version: i64,
30
30
-
}
31
31
-
32
12
pub fn create_signed_commit(
33
13
did: &str,
34
14
data: Cid,
···
36
16
prev: Option<Cid>,
37
17
signing_key: &SigningKey,
38
18
) -> Result<(Vec<u8>, Bytes), String> {
39
39
-
let unsigned = UnsignedCommit {
40
40
-
data,
41
41
-
did,
42
42
-
prev,
43
43
-
rev,
44
44
-
version: 3,
45
45
-
};
46
46
-
let unsigned_bytes = serde_ipld_dagcbor::to_vec(&unsigned)
47
47
-
.map_err(|e| format!("Failed to serialize unsigned commit: {:?}", e))?;
48
48
-
let sig: Signature = signing_key.sign(&unsigned_bytes);
49
49
-
let sig_bytes = Bytes::copy_from_slice(&sig.to_bytes());
50
50
-
#[derive(Serialize)]
51
51
-
struct SignedCommit<'a> {
52
52
-
data: Cid,
53
53
-
did: &'a str,
54
54
-
prev: Option<Cid>,
55
55
-
rev: &'a str,
56
56
-
#[serde(with = "serde_bytes")]
57
57
-
sig: &'a [u8],
58
58
-
version: i64,
59
59
-
}
60
60
-
let signed = SignedCommit {
61
61
-
data,
62
62
-
did,
63
63
-
prev,
64
64
-
rev,
65
65
-
sig: &sig_bytes,
66
66
-
version: 3,
67
67
-
};
68
68
-
let signed_bytes = serde_ipld_dagcbor::to_vec(&signed)
19
19
+
let did = jacquard::types::string::Did::new(did)
20
20
+
.map_err(|e| format!("Invalid DID: {:?}", e))?;
21
21
+
let rev = jacquard::types::string::Tid::from_str(rev)
22
22
+
.map_err(|e| format!("Invalid TID: {:?}", e))?;
23
23
+
let unsigned = Commit::new_unsigned(did, data, rev, prev);
24
24
+
let signed = unsigned
25
25
+
.sign(signing_key)
26
26
+
.map_err(|e| format!("Failed to sign commit: {:?}", e))?;
27
27
+
let sig_bytes = signed.sig().clone();
28
28
+
let signed_bytes = signed
29
29
+
.to_cbor()
69
30
.map_err(|e| format!("Failed to serialize signed commit: {:?}", e))?;
70
31
Ok((signed_bytes, sig_bytes))
71
32
}
···
423
384
let uri = format!("at://{}/{}/{}", did, collection, rkey);
424
385
Ok((uri, result.commit_cid))
425
386
}
426
426
-
use std::str::FromStr;
387
387
+
427
388
pub async fn sequence_identity_event(
428
389
state: &AppState,
429
390
did: &str,
+121
tests/commit_signing.rs
···
1
1
+
use cid::Cid;
2
2
+
use jacquard::types::{integer::LimitedU32, string::Tid};
3
3
+
use jacquard_repo::commit::Commit;
4
4
+
use k256::ecdsa::SigningKey;
5
5
+
use std::str::FromStr;
6
6
+
7
7
+
#[test]
8
8
+
fn test_commit_signing_produces_valid_signature() {
9
9
+
let signing_key = SigningKey::random(&mut rand::thread_rng());
10
10
+
11
11
+
let did = "did:plc:testuser123456789abcdef";
12
12
+
let data_cid =
13
13
+
Cid::from_str("bafyreib2rxk3ryblouj3fxza5jvx6psmwewwessc4m6g6e7pqhhkwqomfi").unwrap();
14
14
+
let rev = Tid::now(LimitedU32::MIN);
15
15
+
16
16
+
let did_typed = jacquard::types::string::Did::new(did).unwrap();
17
17
+
let unsigned = Commit::new_unsigned(did_typed, data_cid, rev, None);
18
18
+
let signed = unsigned.sign(&signing_key).unwrap();
19
19
+
20
20
+
let pubkey_bytes = signing_key.verifying_key().to_encoded_point(true);
21
21
+
let pubkey = jacquard::types::crypto::PublicKey {
22
22
+
codec: jacquard::types::crypto::KeyCodec::Secp256k1,
23
23
+
bytes: std::borrow::Cow::Owned(pubkey_bytes.as_bytes().to_vec()),
24
24
+
};
25
25
+
26
26
+
signed.verify(&pubkey).expect("signature should verify");
27
27
+
}
28
28
+
29
29
+
#[test]
30
30
+
fn test_commit_signing_with_prev() {
31
31
+
let signing_key = SigningKey::random(&mut rand::thread_rng());
32
32
+
33
33
+
let did = "did:plc:testuser123456789abcdef";
34
34
+
let data_cid =
35
35
+
Cid::from_str("bafyreib2rxk3ryblouj3fxza5jvx6psmwewwessc4m6g6e7pqhhkwqomfi").unwrap();
36
36
+
let prev_cid =
37
37
+
Cid::from_str("bafyreigxmvutyl3k5m4guzwxv3xf34gfxjlykgfdqkjmf32vwb5vcjxlui").unwrap();
38
38
+
let rev = Tid::now(LimitedU32::MIN);
39
39
+
40
40
+
let did_typed = jacquard::types::string::Did::new(did).unwrap();
41
41
+
let unsigned = Commit::new_unsigned(did_typed, data_cid, rev, Some(prev_cid));
42
42
+
let signed = unsigned.sign(&signing_key).unwrap();
43
43
+
44
44
+
let pubkey_bytes = signing_key.verifying_key().to_encoded_point(true);
45
45
+
let pubkey = jacquard::types::crypto::PublicKey {
46
46
+
codec: jacquard::types::crypto::KeyCodec::Secp256k1,
47
47
+
bytes: std::borrow::Cow::Owned(pubkey_bytes.as_bytes().to_vec()),
48
48
+
};
49
49
+
50
50
+
signed.verify(&pubkey).expect("signature should verify");
51
51
+
}
52
52
+
53
53
+
#[test]
54
54
+
fn test_unsigned_commit_has_5_fields() {
55
55
+
let did = "did:plc:test";
56
56
+
let data_cid =
57
57
+
Cid::from_str("bafyreib2rxk3ryblouj3fxza5jvx6psmwewwessc4m6g6e7pqhhkwqomfi").unwrap();
58
58
+
let rev = Tid::from_str("3masrxv55po22").unwrap();
59
59
+
60
60
+
let did_typed = jacquard::types::string::Did::new(did).unwrap();
61
61
+
let unsigned = Commit::new_unsigned(did_typed, data_cid, rev, None);
62
62
+
63
63
+
let unsigned_bytes = serde_ipld_dagcbor::to_vec(&unsigned).unwrap();
64
64
+
65
65
+
let decoded: ciborium::Value = ciborium::from_reader(&unsigned_bytes[..]).unwrap();
66
66
+
if let ciborium::Value::Map(map) = decoded {
67
67
+
assert_eq!(
68
68
+
map.len(),
69
69
+
5,
70
70
+
"Unsigned commit must have exactly 5 fields (data, did, prev, rev, version) - no sig field"
71
71
+
);
72
72
+
let keys: Vec<String> = map
73
73
+
.iter()
74
74
+
.filter_map(|(k, _)| {
75
75
+
if let ciborium::Value::Text(s) = k {
76
76
+
Some(s.clone())
77
77
+
} else {
78
78
+
None
79
79
+
}
80
80
+
})
81
81
+
.collect();
82
82
+
assert!(keys.contains(&"data".to_string()));
83
83
+
assert!(keys.contains(&"did".to_string()));
84
84
+
assert!(keys.contains(&"prev".to_string()));
85
85
+
assert!(keys.contains(&"rev".to_string()));
86
86
+
assert!(keys.contains(&"version".to_string()));
87
87
+
assert!(
88
88
+
!keys.contains(&"sig".to_string()),
89
89
+
"Unsigned commit must NOT contain sig field"
90
90
+
);
91
91
+
} else {
92
92
+
panic!("Expected CBOR map");
93
93
+
}
94
94
+
}
95
95
+
96
96
+
#[test]
97
97
+
fn test_create_signed_commit_helper() {
98
98
+
use tranquil_pds::api::repo::record::utils::create_signed_commit;
99
99
+
100
100
+
let signing_key = SigningKey::random(&mut rand::thread_rng());
101
101
+
let did = "did:plc:testuser123456789abcdef";
102
102
+
let data_cid =
103
103
+
Cid::from_str("bafyreib2rxk3ryblouj3fxza5jvx6psmwewwessc4m6g6e7pqhhkwqomfi").unwrap();
104
104
+
let rev = Tid::now(LimitedU32::MIN).to_string();
105
105
+
106
106
+
let (signed_bytes, sig) = create_signed_commit(did, data_cid, &rev, None, &signing_key)
107
107
+
.expect("signing should succeed");
108
108
+
109
109
+
assert!(!signed_bytes.is_empty());
110
110
+
assert_eq!(sig.len(), 64);
111
111
+
112
112
+
let commit = Commit::from_cbor(&signed_bytes).expect("should parse as valid commit");
113
113
+
114
114
+
let pubkey_bytes = signing_key.verifying_key().to_encoded_point(true);
115
115
+
let pubkey = jacquard::types::crypto::PublicKey {
116
116
+
codec: jacquard::types::crypto::KeyCodec::Secp256k1,
117
117
+
bytes: std::borrow::Cow::Owned(pubkey_bytes.as_bytes().to_vec()),
118
118
+
};
119
119
+
120
120
+
commit.verify(&pubkey).expect("signature should verify");
121
121
+
}
+26
-34
tests/common/mod.rs
···
305
305
.await
306
306
.expect("Failed to get verification code");
307
307
308
308
-
let verification_code = body_text
309
309
-
.lines()
310
310
-
.find(|line| line.contains("verification code:") || line.contains("code is:"))
311
311
-
.and_then(|line| {
312
312
-
if line.contains("verification code:") {
313
313
-
line.split("verification code:")
314
314
-
.nth(1)
315
315
-
.map(|s| s.trim().to_string())
316
316
-
} else {
317
317
-
line.split("code is:").nth(1).map(|s| s.trim().to_string())
318
318
-
}
308
308
+
let lines: Vec<&str> = body_text.lines().collect();
309
309
+
let verification_code = lines
310
310
+
.iter()
311
311
+
.enumerate()
312
312
+
.find(|(_, line)| {
313
313
+
line.contains("verification code is:") || line.contains("code is:")
319
314
})
320
320
-
.unwrap_or_else(|| {
315
315
+
.and_then(|(i, _)| lines.get(i + 1).map(|s| s.trim().to_string()))
316
316
+
.or_else(|| {
321
317
body_text
322
322
-
.lines()
323
323
-
.find(|line| line.trim().starts_with("MX") && line.contains('-'))
324
324
-
.map(|s| s.trim().to_string())
325
325
-
.unwrap_or_default()
326
326
-
});
318
318
+
.split_whitespace()
319
319
+
.find(|word| {
320
320
+
word.contains('-') && word.chars().filter(|c| *c == '-').count() >= 3
321
321
+
})
322
322
+
.map(|s| s.to_string())
323
323
+
})
324
324
+
.unwrap_or_else(|| body_text.clone());
327
325
328
326
let confirm_payload = json!({
329
327
"did": did,
···
480
478
.fetch_one(&pool)
481
479
.await
482
480
.expect("Failed to get verification from comms_queue");
483
483
-
let verification_code = body_text
484
484
-
.lines()
485
485
-
.find(|line| line.contains("verification code:") || line.contains("code is:"))
486
486
-
.and_then(|line| {
487
487
-
if line.contains("verification code:") {
488
488
-
line.split("verification code:")
489
489
-
.nth(1)
490
490
-
.map(|s| s.trim().to_string())
491
491
-
} else if line.contains("code is:") {
492
492
-
line.split("code is:").nth(1).map(|s| s.trim().to_string())
493
493
-
} else {
494
494
-
None
495
495
-
}
481
481
+
let lines: Vec<&str> = body_text.lines().collect();
482
482
+
let verification_code = lines
483
483
+
.iter()
484
484
+
.enumerate()
485
485
+
.find(|(_, line)| {
486
486
+
line.contains("verification code is:") || line.contains("code is:")
496
487
})
497
497
-
.unwrap_or_else(|| {
488
488
+
.and_then(|(i, _)| lines.get(i + 1).map(|s| s.trim().to_string()))
489
489
+
.or_else(|| {
498
490
body_text
499
491
.split_whitespace()
500
492
.find(|word| {
501
493
word.contains('-') && word.chars().filter(|c| *c == '-').count() >= 3
502
494
})
503
503
-
.unwrap_or(&body_text)
504
504
-
.to_string()
505
505
-
});
495
495
+
.map(|s| s.to_string())
496
496
+
})
497
497
+
.unwrap_or_else(|| body_text.clone());
506
498
507
499
let confirm_payload = json!({
508
500
"did": did,
+8
-22
tests/import_with_verification.rs
···
3
3
use common::*;
4
4
use ipld_core::ipld::Ipld;
5
5
use jacquard::types::{integer::LimitedU32, string::Tid};
6
6
-
use k256::ecdsa::{Signature, SigningKey, signature::Signer};
6
6
+
use jacquard_repo::commit::Commit;
7
7
+
use k256::ecdsa::SigningKey;
7
8
use reqwest::StatusCode;
8
9
use serde_json::json;
9
10
use sha2::{Digest, Sha256};
10
11
use sqlx::PgPool;
11
12
use std::collections::BTreeMap;
13
13
+
use std::str::FromStr;
12
14
use wiremock::matchers::{method, path};
13
15
use wiremock::{Mock, MockServer, ResponseTemplate};
14
16
···
89
91
}
90
92
91
93
fn create_signed_commit(did: &str, data_cid: &Cid, signing_key: &SigningKey) -> (Vec<u8>, Cid) {
92
92
-
let rev = Tid::now(LimitedU32::MIN).to_string();
93
93
-
let unsigned = Ipld::Map(BTreeMap::from([
94
94
-
("data".to_string(), Ipld::Link(*data_cid)),
95
95
-
("did".to_string(), Ipld::String(did.to_string())),
96
96
-
("prev".to_string(), Ipld::Null),
97
97
-
("rev".to_string(), Ipld::String(rev.clone())),
98
98
-
("sig".to_string(), Ipld::Bytes(vec![])),
99
99
-
("version".to_string(), Ipld::Integer(3)),
100
100
-
]));
101
101
-
let unsigned_bytes = serde_ipld_dagcbor::to_vec(&unsigned).unwrap();
102
102
-
let signature: Signature = signing_key.sign(&unsigned_bytes);
103
103
-
let sig_bytes = signature.to_bytes().to_vec();
104
104
-
let signed = Ipld::Map(BTreeMap::from([
105
105
-
("data".to_string(), Ipld::Link(*data_cid)),
106
106
-
("did".to_string(), Ipld::String(did.to_string())),
107
107
-
("prev".to_string(), Ipld::Null),
108
108
-
("rev".to_string(), Ipld::String(rev)),
109
109
-
("sig".to_string(), Ipld::Bytes(sig_bytes)),
110
110
-
("version".to_string(), Ipld::Integer(3)),
111
111
-
]));
112
112
-
let signed_bytes = serde_ipld_dagcbor::to_vec(&signed).unwrap();
94
94
+
let rev = Tid::now(LimitedU32::MIN);
95
95
+
let did = jacquard::types::string::Did::new(did).expect("valid DID");
96
96
+
let unsigned = Commit::new_unsigned(did, *data_cid, rev, None);
97
97
+
let signed = unsigned.sign(signing_key).expect("signing failed");
98
98
+
let signed_bytes = signed.to_cbor().expect("serialization failed");
113
99
let cid = make_cid(&signed_bytes);
114
100
(signed_bytes, cid)
115
101
}
+15
-17
tests/jwt_security.rs
···
692
692
"SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_verification' ORDER BY created_at DESC LIMIT 1",
693
693
did
694
694
).fetch_one(&pool).await.unwrap();
695
695
-
let code = body_text
696
696
-
.lines()
697
697
-
.find(|line| line.contains("verification code:") || line.contains("code is:"))
698
698
-
.and_then(|line| {
699
699
-
if line.contains("verification code:") {
700
700
-
line.split("verification code:")
701
701
-
.nth(1)
702
702
-
.map(|s| s.trim().to_string())
703
703
-
} else {
704
704
-
line.split("code is:").nth(1).map(|s| s.trim().to_string())
705
705
-
}
695
695
+
let lines: Vec<&str> = body_text.lines().collect();
696
696
+
let code = lines
697
697
+
.iter()
698
698
+
.enumerate()
699
699
+
.find(|(_, line)| {
700
700
+
line.contains("verification code is:") || line.contains("code is:")
706
701
})
707
707
-
.unwrap_or_else(|| {
702
702
+
.and_then(|(i, _)| lines.get(i + 1).map(|s| s.trim().to_string()))
703
703
+
.or_else(|| {
708
704
body_text
709
709
-
.lines()
710
710
-
.find(|line| line.trim().starts_with("MX") && line.contains('-'))
711
711
-
.map(|s| s.trim().to_string())
712
712
-
.unwrap_or_default()
713
713
-
});
705
705
+
.split_whitespace()
706
706
+
.find(|word| {
707
707
+
word.contains('-') && word.chars().filter(|c| *c == '-').count() >= 3
708
708
+
})
709
709
+
.map(|s| s.to_string())
710
710
+
})
711
711
+
.unwrap_or_else(|| body_text.clone());
714
712
715
713
let confirm = http_client
716
714
.post(format!("{}/xrpc/com.atproto.server.confirmSignup", url))