+75
-34
crates/atproto-attestation/src/bin/atproto-attestation-sign.rs
+75
-34
crates/atproto-attestation/src/bin/atproto-attestation-sign.rs
···
8
//!
9
//! ### Remote Attestation
10
//! ```bash
11
-
//! atproto-attestation-sign remote <source_record> <repository_did> <metadata_record>
12
//! ```
13
//!
14
//! ### Inline Attestation
15
//! ```bash
16
-
//! atproto-attestation-sign inline <source_record> <signing_key> <metadata_record>
17
//! ```
18
//!
19
//! ## Arguments
20
//!
21
//! - `source_record`: JSON string or path to JSON file containing the record being attested
22
-
//! - `repository_did`: (Remote mode) DID of the repository that will contain the remote attestation record
23
//! - `signing_key`: (Inline mode) Private key string (did:key format) used to sign the attestation
24
//! - `metadata_record`: JSON string or path to JSON file with attestation metadata used during CID creation
25
//!
···
28
//! ```bash
29
//! # Remote attestation - creates proof record and strongRef
30
//! atproto-attestation-sign remote \
31
//! record.json \
32
-
//! did:plc:xyz123... \
33
//! metadata.json
34
//!
35
//! # Inline attestation - embeds signature in record
36
//! atproto-attestation-sign inline \
37
//! record.json \
38
//! did:key:z42tv1pb3... \
39
//! '{"$type":"com.example.attestation","purpose":"demo"}'
40
//!
41
//! # Read from stdin
42
-
//! cat record.json | atproto-attestation-sign inline \
43
//! - \
44
-
//! did:key:z42tv1pb3... \
45
//! metadata.json
46
//! ```
47
···
75
76
MODES:
77
remote Creates a separate proof record with strongRef reference
78
-
Syntax: remote <source_record> <repository_did> <metadata_record>
79
80
inline Embeds signature bytes directly in the record
81
-
Syntax: inline <source_record> <signing_key> <metadata_record>
82
83
ARGUMENTS:
84
-
source_record JSON string or file path to the record being attested
85
-
repository_did (Remote) DID of repository containing the attestation record
86
-
signing_key (Inline) Private key in did:key format for signing
87
-
metadata_record JSON string or file path with attestation metadata
88
89
EXAMPLES:
90
# Remote attestation (creates proof record + strongRef):
91
atproto-attestation-sign remote \\
92
record.json \\
93
-
did:plc:xyz123abc... \\
94
metadata.json
95
96
# Inline attestation (embeds signature):
97
atproto-attestation-sign inline \\
98
record.json \\
99
did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \\
100
'{\"$type\":\"com.example.attestation\",\"purpose\":\"demo\"}'
101
102
# Read source record from stdin:
103
-
cat record.json | atproto-attestation-sign inline \\
104
- \\
105
-
did:key:z42tv1pb3... \\
106
metadata.json
107
108
OUTPUT:
···
124
/// Create a remote attestation with separate proof record
125
///
126
/// Generates a proof record containing the CID and returns both the proof
127
-
/// record (to be stored in the repository) and the source record with a
128
-
/// strongRef attestation reference.
129
#[command(visible_alias = "r")]
130
Remote {
131
/// Source record JSON string or file path (use '-' for stdin)
132
source_record: String,
133
134
-
/// Repository DID that will contain the remote attestation record
135
-
repository_did: String,
136
137
/// Attestation metadata JSON string or file path
138
metadata_record: String,
···
147
/// Source record JSON string or file path (use '-' for stdin)
148
source_record: String,
149
150
/// Private signing key in did:key format (e.g., did:key:z...)
151
signing_key: String,
152
···
161
162
match args.command {
163
Commands::Remote {
164
source_record,
165
-
repository_did,
166
metadata_record,
167
-
} => handle_remote_attestation(&source_record, &repository_did, &metadata_record)?,
168
169
Commands::Inline {
170
source_record,
171
signing_key,
172
metadata_record,
173
-
} => handle_inline_attestation(&source_record, &signing_key, &metadata_record)?,
174
}
175
176
Ok(())
···
180
///
181
/// Creates a proof record and appends a strongRef to the source record.
182
/// Outputs both the proof record and the updated source record.
183
fn handle_remote_attestation(
184
source_record: &str,
185
-
repository_did: &str,
186
metadata_record: &str,
187
) -> Result<()> {
188
// Load source record and metadata
189
let record_json = load_json_input(source_record)?;
···
198
return Err(anyhow!("Metadata record must be a JSON object"));
199
}
200
201
-
// Validate repository DID
202
-
if !repository_did.starts_with("did:") {
203
return Err(anyhow!(
204
-
"Repository DID must start with 'did:' prefix, got: {}",
205
-
repository_did
206
));
207
}
208
209
-
// Create the remote attestation proof record
210
-
let proof_record = create_remote_attestation(&record_json, &metadata_json)
211
.context("Failed to create remote attestation proof record")?;
212
213
-
// Create the source record with strongRef reference
214
let attested_record =
215
-
create_remote_attestation_reference(&record_json, &proof_record, repository_did)
216
.context("Failed to create remote attestation reference")?;
217
218
// Output both records
···
231
/// Outputs the record with inline attestation.
232
fn handle_inline_attestation(
233
source_record: &str,
234
signing_key: &str,
235
metadata_record: &str,
236
) -> Result<()> {
···
247
return Err(anyhow!("Metadata record must be a JSON object"));
248
}
249
250
// Parse the signing key
251
let key_data = identify_key(signing_key)
252
.with_context(|| format!("Failed to parse signing key: {}", signing_key))?;
253
254
-
// Create inline attestation
255
-
let signed_record = create_inline_attestation(&record_json, &metadata_json, &key_data)
256
-
.context("Failed to create inline attestation")?;
257
258
// Output the signed record
259
println!("{}", serde_json::to_string_pretty(&signed_record)?);
···
8
//!
9
//! ### Remote Attestation
10
//! ```bash
11
+
//! atproto-attestation-sign remote <source_repository_did> <source_record> <attestation_repository_did> <metadata_record>
12
//! ```
13
//!
14
//! ### Inline Attestation
15
//! ```bash
16
+
//! atproto-attestation-sign inline <source_record> <repository_did> <signing_key> <metadata_record>
17
//! ```
18
//!
19
//! ## Arguments
20
//!
21
+
//! - `source_repository_did`: (Remote mode) DID of the repository housing the source record (prevents replay attacks)
22
//! - `source_record`: JSON string or path to JSON file containing the record being attested
23
+
//! - `attestation_repository_did`: (Remote mode) DID of the repository where the attestation proof will be stored
24
+
//! - `repository_did`: (Inline mode) DID of the repository that will house the record (prevents replay attacks)
25
//! - `signing_key`: (Inline mode) Private key string (did:key format) used to sign the attestation
26
//! - `metadata_record`: JSON string or path to JSON file with attestation metadata used during CID creation
27
//!
···
30
//! ```bash
31
//! # Remote attestation - creates proof record and strongRef
32
//! atproto-attestation-sign remote \
33
+
//! did:plc:sourceRepo... \
34
//! record.json \
35
+
//! did:plc:attestationRepo... \
36
//! metadata.json
37
//!
38
//! # Inline attestation - embeds signature in record
39
//! atproto-attestation-sign inline \
40
//! record.json \
41
+
//! did:plc:xyz123... \
42
//! did:key:z42tv1pb3... \
43
//! '{"$type":"com.example.attestation","purpose":"demo"}'
44
//!
45
//! # Read from stdin
46
+
//! cat record.json | atproto-attestation-sign remote \
47
+
//! did:plc:sourceRepo... \
48
//! - \
49
+
//! did:plc:attestationRepo... \
50
//! metadata.json
51
//! ```
52
···
80
81
MODES:
82
remote Creates a separate proof record with strongRef reference
83
+
Syntax: remote <source_repository_did> <source_record> <attestation_repository_did> <metadata_record>
84
85
inline Embeds signature bytes directly in the record
86
+
Syntax: inline <source_record> <repository_did> <signing_key> <metadata_record>
87
88
ARGUMENTS:
89
+
source_repository_did (Remote) DID of repository housing the source record (for replay prevention)
90
+
source_record JSON string or file path to the record being attested
91
+
attestation_repository_did (Remote) DID of repository where attestation proof will be stored
92
+
repository_did (Inline) DID of repository that will house the record (for replay prevention)
93
+
signing_key (Inline) Private key in did:key format for signing
94
+
metadata_record JSON string or file path with attestation metadata
95
96
EXAMPLES:
97
# Remote attestation (creates proof record + strongRef):
98
atproto-attestation-sign remote \\
99
+
did:plc:sourceRepo... \\
100
record.json \\
101
+
did:plc:attestationRepo... \\
102
metadata.json
103
104
# Inline attestation (embeds signature):
105
atproto-attestation-sign inline \\
106
record.json \\
107
+
did:plc:xyz123abc... \\
108
did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \\
109
'{\"$type\":\"com.example.attestation\",\"purpose\":\"demo\"}'
110
111
# Read source record from stdin:
112
+
cat record.json | atproto-attestation-sign remote \\
113
+
did:plc:sourceRepo... \\
114
- \\
115
+
did:plc:attestationRepo... \\
116
metadata.json
117
118
OUTPUT:
···
134
/// Create a remote attestation with separate proof record
135
///
136
/// Generates a proof record containing the CID and returns both the proof
137
+
/// record (to be stored in the attestation repository) and the source record
138
+
/// with a strongRef attestation reference.
139
#[command(visible_alias = "r")]
140
Remote {
141
+
/// DID of the repository housing the source record (for replay attack prevention)
142
+
source_repository_did: String,
143
+
144
/// Source record JSON string or file path (use '-' for stdin)
145
source_record: String,
146
147
+
/// DID of the repository where the attestation proof will be stored
148
+
attestation_repository_did: String,
149
150
/// Attestation metadata JSON string or file path
151
metadata_record: String,
···
160
/// Source record JSON string or file path (use '-' for stdin)
161
source_record: String,
162
163
+
/// Repository DID that will house the record (for replay attack prevention)
164
+
repository_did: String,
165
+
166
/// Private signing key in did:key format (e.g., did:key:z...)
167
signing_key: String,
168
···
177
178
match args.command {
179
Commands::Remote {
180
+
source_repository_did,
181
source_record,
182
+
attestation_repository_did,
183
metadata_record,
184
+
} => {
185
+
handle_remote_attestation(&source_record, &source_repository_did, &metadata_record, &attestation_repository_did)?
186
+
}
187
188
Commands::Inline {
189
source_record,
190
+
repository_did,
191
signing_key,
192
metadata_record,
193
+
} => handle_inline_attestation(&source_record, &repository_did, &signing_key, &metadata_record)?,
194
}
195
196
Ok(())
···
200
///
201
/// Creates a proof record and appends a strongRef to the source record.
202
/// Outputs both the proof record and the updated source record.
203
+
///
204
+
/// - `source_repository_did`: Used for signature binding (prevents replay attacks)
205
+
/// - `attestation_repository_did`: Where the attestation proof record will be stored
206
fn handle_remote_attestation(
207
source_record: &str,
208
+
source_repository_did: &str,
209
metadata_record: &str,
210
+
attestation_repository_did: &str,
211
) -> Result<()> {
212
// Load source record and metadata
213
let record_json = load_json_input(source_record)?;
···
222
return Err(anyhow!("Metadata record must be a JSON object"));
223
}
224
225
+
// Validate repository DIDs
226
+
if !source_repository_did.starts_with("did:") {
227
return Err(anyhow!(
228
+
"Source repository DID must start with 'did:' prefix, got: {}",
229
+
source_repository_did
230
));
231
}
232
233
+
if !attestation_repository_did.starts_with("did:") {
234
+
return Err(anyhow!(
235
+
"Attestation repository DID must start with 'did:' prefix, got: {}",
236
+
attestation_repository_did
237
+
));
238
+
}
239
+
240
+
// Create the remote attestation proof record with source repository binding
241
+
let proof_record = create_remote_attestation(&record_json, &metadata_json, source_repository_did)
242
.context("Failed to create remote attestation proof record")?;
243
244
+
// Create the source record with strongRef reference pointing to attestation repository
245
let attested_record =
246
+
create_remote_attestation_reference(&record_json, &proof_record, attestation_repository_did)
247
.context("Failed to create remote attestation reference")?;
248
249
// Output both records
···
262
/// Outputs the record with inline attestation.
263
fn handle_inline_attestation(
264
source_record: &str,
265
+
repository_did: &str,
266
signing_key: &str,
267
metadata_record: &str,
268
) -> Result<()> {
···
279
return Err(anyhow!("Metadata record must be a JSON object"));
280
}
281
282
+
// Validate repository DID
283
+
if !repository_did.starts_with("did:") {
284
+
return Err(anyhow!(
285
+
"Repository DID must start with 'did:' prefix, got: {}",
286
+
repository_did
287
+
));
288
+
}
289
+
290
// Parse the signing key
291
let key_data = identify_key(signing_key)
292
.with_context(|| format!("Failed to parse signing key: {}", signing_key))?;
293
294
+
// Create inline attestation with repository binding
295
+
let signed_record =
296
+
create_inline_attestation(&record_json, &metadata_json, repository_did, &key_data)
297
+
.context("Failed to create inline attestation")?;
298
299
// Output the signed record
300
println!("{}", serde_json::to_string_pretty(&signed_record)?);
+53
-27
crates/atproto-attestation/src/bin/atproto-attestation-verify.rs
+53
-27
crates/atproto-attestation/src/bin/atproto-attestation-verify.rs
···
8
//!
9
//! ### Verify all signatures in a record
10
//! ```bash
11
-
//! atproto-attestation-verify <record>
12
//! ```
13
//!
14
//! ### Verify a specific attestation against a record
15
//! ```bash
16
-
//! atproto-attestation-verify <record> <attestation>
17
//! ```
18
//!
19
//! ## Parameter Formats
···
27
//!
28
//! ```bash
29
//! # Verify all signatures in a record from file
30
-
//! atproto-attestation-verify ./signed_post.json
31
//!
32
//! # Verify all signatures in a record from AT-URI
33
-
//! atproto-attestation-verify at://did:plc:abc123/app.bsky.feed.post/3k2k4j5h6g
34
//!
35
//! # Verify specific attestation against a record (both from files)
36
-
//! atproto-attestation-verify ./record.json ./attestation.json
37
//!
38
//! # Verify specific attestation (from AT-URI) against record (from file)
39
-
//! atproto-attestation-verify ./record.json at://did:plc:xyz/com.example.attestation/abc123
40
//!
41
//! # Read record from stdin, verify all signatures
42
-
//! cat signed.json | atproto-attestation-verify -
43
//!
44
//! # Verify inline JSON
45
-
//! atproto-attestation-verify '{"$type":"app.bsky.feed.post","text":"Hello","signatures":[...]}'
46
//! ```
47
48
use anyhow::{Context, Result, anyhow};
49
-
use atproto_attestation::{VerificationStatus, verify_all_signatures_with_resolver};
50
use clap::Parser;
51
use serde_json::Value;
52
use std::{
···
60
/// Validates attestation signatures by reconstructing signed content and checking
61
/// ECDSA signatures against embedded public keys. Supports verifying all signatures
62
/// in a record or validating a specific attestation record.
63
#[derive(Parser)]
64
#[command(
65
name = "atproto-attestation-verify",
···
69
A command-line tool for verifying cryptographic signatures of AT Protocol records.
70
71
USAGE:
72
-
atproto-attestation-verify <record> Verify all signatures in record
73
-
atproto-attestation-verify <record> <attestation> Verify specific attestation
74
75
PARAMETER FORMATS:
76
Each parameter accepts JSON strings, file paths, or AT-URIs:
···
81
82
EXAMPLES:
83
# Verify all signatures in a record:
84
-
atproto-attestation-verify ./signed_post.json
85
-
atproto-attestation-verify at://did:plc:abc/app.bsky.feed.post/123
86
87
# Verify specific attestation:
88
-
atproto-attestation-verify ./record.json ./attestation.json
89
-
atproto-attestation-verify ./record.json at://did:plc:xyz/com.example.attestation/abc
90
91
# Read from stdin:
92
-
cat signed.json | atproto-attestation-verify -
93
94
OUTPUT:
95
Single record mode: Reports each signature with ✓ (valid), ✗ (invalid), or ? (unverified)
···
105
/// Record to verify - JSON string, file path, AT-URI, or '-' for stdin
106
record: String,
107
108
/// Optional attestation record to verify against the record - JSON string, file path, or AT-URI
109
attestation: Option<String>,
110
}
···
122
return Err(anyhow!("Record must be a JSON object"));
123
}
124
125
// Determine verification mode
126
match args.attestation {
127
None => {
128
// Mode 1: Verify all signatures in the record
129
-
verify_all_mode(&record).await
130
}
131
Some(attestation_input) => {
132
// Mode 2: Verify specific attestation against record
···
138
return Err(anyhow!("Attestation must be a JSON object"));
139
}
140
141
-
verify_attestation_mode(&record, &attestation).await
142
}
143
}
144
}
···
149
/// - ✓ Valid signature
150
/// - ✗ Invalid signature
151
/// - ? Unverified (e.g., remote attestations requiring proof record fetch)
152
-
async fn verify_all_mode(record: &Value) -> Result<()> {
153
// Create an identity resolver for fetching remote attestations
154
use atproto_identity::resolve::{HickoryDnsResolver, InnerIdentityResolver};
155
use std::sync::Arc;
···
169
identity_resolver,
170
};
171
172
-
let reports = verify_all_signatures_with_resolver(record, None, Some(&record_resolver))
173
-
.await
174
-
.context("Failed to verify signatures")?;
175
176
if reports.is_empty() {
177
return Err(anyhow!("No signatures found in record"));
···
228
///
229
/// The attestation should be a standalone attestation object (e.g., from a remote proof record)
230
/// that will be verified against the record's content.
231
-
async fn verify_attestation_mode(record: &Value, attestation: &Value) -> Result<()> {
232
// The attestation should have a CID field that we can use to verify
233
let attestation_obj = attestation
234
.as_object()
···
240
.and_then(Value::as_str)
241
.ok_or_else(|| anyhow!("Attestation must contain a 'cid' field"))?;
242
243
-
// Prepare the signing record with the attestation metadata
244
let mut signing_metadata = attestation_obj.clone();
245
signing_metadata.remove("cid");
246
signing_metadata.remove("signature");
247
248
-
let signing_record =
249
-
atproto_attestation::prepare_signing_record(record, &Value::Object(signing_metadata))
250
-
.context("Failed to prepare signing record")?;
251
252
// Generate the CID from the signing record
253
let computed_cid =
···
8
//!
9
//! ### Verify all signatures in a record
10
//! ```bash
11
+
//! atproto-attestation-verify <record> <repository_did>
12
//! ```
13
//!
14
//! ### Verify a specific attestation against a record
15
//! ```bash
16
+
//! atproto-attestation-verify <record> <repository_did> <attestation>
17
//! ```
18
//!
19
//! ## Parameter Formats
···
27
//!
28
//! ```bash
29
//! # Verify all signatures in a record from file
30
+
//! atproto-attestation-verify ./signed_post.json did:plc:repo123
31
//!
32
//! # Verify all signatures in a record from AT-URI
33
+
//! atproto-attestation-verify at://did:plc:abc123/app.bsky.feed.post/3k2k4j5h6g did:plc:abc123
34
//!
35
//! # Verify specific attestation against a record (both from files)
36
+
//! atproto-attestation-verify ./record.json did:plc:repo123 ./attestation.json
37
//!
38
//! # Verify specific attestation (from AT-URI) against record (from file)
39
+
//! atproto-attestation-verify ./record.json did:plc:repo123 at://did:plc:xyz/com.example.attestation/abc123
40
//!
41
//! # Read record from stdin, verify all signatures
42
+
//! cat signed.json | atproto-attestation-verify - did:plc:repo123
43
//!
44
//! # Verify inline JSON
45
+
//! atproto-attestation-verify '{"$type":"app.bsky.feed.post","text":"Hello","signatures":[...]}' did:plc:repo123
46
//! ```
47
48
use anyhow::{Context, Result, anyhow};
49
+
use atproto_attestation::VerificationStatus;
50
use clap::Parser;
51
use serde_json::Value;
52
use std::{
···
60
/// Validates attestation signatures by reconstructing signed content and checking
61
/// ECDSA signatures against embedded public keys. Supports verifying all signatures
62
/// in a record or validating a specific attestation record.
63
+
///
64
+
/// The repository DID parameter is now REQUIRED to prevent replay attacks where
65
+
/// attestations might be copied to different repositories.
66
#[derive(Parser)]
67
#[command(
68
name = "atproto-attestation-verify",
···
72
A command-line tool for verifying cryptographic signatures of AT Protocol records.
73
74
USAGE:
75
+
atproto-attestation-verify <record> <repository_did> Verify all signatures
76
+
atproto-attestation-verify <record> <repository_did> <attestation> Verify specific attestation
77
78
PARAMETER FORMATS:
79
Each parameter accepts JSON strings, file paths, or AT-URIs:
···
84
85
EXAMPLES:
86
# Verify all signatures in a record:
87
+
atproto-attestation-verify ./signed_post.json did:plc:repo123
88
+
atproto-attestation-verify at://did:plc:abc/app.bsky.feed.post/123 did:plc:abc
89
90
# Verify specific attestation:
91
+
atproto-attestation-verify ./record.json did:plc:repo123 ./attestation.json
92
+
atproto-attestation-verify ./record.json did:plc:repo123 at://did:plc:xyz/com.example.attestation/abc
93
94
# Read from stdin:
95
+
cat signed.json | atproto-attestation-verify - did:plc:repo123
96
97
OUTPUT:
98
Single record mode: Reports each signature with ✓ (valid), ✗ (invalid), or ? (unverified)
···
108
/// Record to verify - JSON string, file path, AT-URI, or '-' for stdin
109
record: String,
110
111
+
/// Repository DID that houses the record (required for replay attack prevention)
112
+
repository_did: String,
113
+
114
/// Optional attestation record to verify against the record - JSON string, file path, or AT-URI
115
attestation: Option<String>,
116
}
···
128
return Err(anyhow!("Record must be a JSON object"));
129
}
130
131
+
// Validate repository DID
132
+
if !args.repository_did.starts_with("did:") {
133
+
return Err(anyhow!(
134
+
"Repository DID must start with 'did:' prefix, got: {}",
135
+
args.repository_did
136
+
));
137
+
}
138
+
139
// Determine verification mode
140
match args.attestation {
141
None => {
142
// Mode 1: Verify all signatures in the record
143
+
verify_all_mode(&record, &args.repository_did).await
144
}
145
Some(attestation_input) => {
146
// Mode 2: Verify specific attestation against record
···
152
return Err(anyhow!("Attestation must be a JSON object"));
153
}
154
155
+
verify_attestation_mode(&record, &attestation, &args.repository_did).await
156
}
157
}
158
}
···
163
/// - ✓ Valid signature
164
/// - ✗ Invalid signature
165
/// - ? Unverified (e.g., remote attestations requiring proof record fetch)
166
+
async fn verify_all_mode(record: &Value, repository_did: &str) -> Result<()> {
167
// Create an identity resolver for fetching remote attestations
168
use atproto_identity::resolve::{HickoryDnsResolver, InnerIdentityResolver};
169
use std::sync::Arc;
···
183
identity_resolver,
184
};
185
186
+
let reports = atproto_attestation::verify_all_signatures_with_resolver(
187
+
record,
188
+
repository_did,
189
+
None,
190
+
Some(&record_resolver),
191
+
)
192
+
.await
193
+
.context("Failed to verify signatures")?;
194
195
if reports.is_empty() {
196
return Err(anyhow!("No signatures found in record"));
···
247
///
248
/// The attestation should be a standalone attestation object (e.g., from a remote proof record)
249
/// that will be verified against the record's content.
250
+
async fn verify_attestation_mode(
251
+
record: &Value,
252
+
attestation: &Value,
253
+
repository_did: &str,
254
+
) -> Result<()> {
255
// The attestation should have a CID field that we can use to verify
256
let attestation_obj = attestation
257
.as_object()
···
263
.and_then(Value::as_str)
264
.ok_or_else(|| anyhow!("Attestation must contain a 'cid' field"))?;
265
266
+
// Prepare the signing record with the attestation metadata and repository DID
267
let mut signing_metadata = attestation_obj.clone();
268
signing_metadata.remove("cid");
269
signing_metadata.remove("signature");
270
271
+
let signing_record = atproto_attestation::prepare_signing_record(
272
+
record,
273
+
&Value::Object(signing_metadata),
274
+
repository_did,
275
+
)
276
+
.context("Failed to prepare signing record")?;
277
278
// Generate the CID from the signing record
279
let computed_cid =
+181
-43
crates/atproto-attestation/src/lib.rs
+181
-43
crates/atproto-attestation/src/lib.rs
···
126
return Err(AttestationError::SigMetadataMissingType);
127
}
128
129
let dag_cbor_bytes = serde_ipld_dagcbor::to_vec(record)?;
130
let digest = Sha256::digest(&dag_cbor_bytes);
131
let multihash = Multihash::wrap(0x12, &digest)
···
148
/// - Removes any existing `signatures`, `sigs`, and `$sig` fields.
149
/// - Inserts the provided `attestation` metadata as the new `$sig` object.
150
/// - Ensures the metadata contains a string `$type` discriminator.
151
pub fn prepare_signing_record(
152
record: &Value,
153
attestation: &Value,
154
) -> Result<Value, AttestationError> {
155
let mut prepared = record
156
.as_object()
···
170
{
171
return Err(AttestationError::MetadataMissingSigType);
172
}
173
174
sig_metadata.remove("signature");
175
sig_metadata.remove("cid");
···
183
}
184
185
/// Creates an inline attestation by signing the prepared record with the provided key.
186
pub fn create_inline_attestation(
187
record: &Value,
188
attestation_metadata: &Value,
189
signing_key: &KeyData,
190
) -> Result<Value, AttestationError> {
191
-
let signing_record = prepare_signing_record(record, attestation_metadata)?;
192
let cid = create_cid(&signing_record)?;
193
194
let raw_signature = sign(signing_key, &cid.to_bytes())
···
202
203
inline_object.remove("signature");
204
inline_object.remove("cid");
205
inline_object.insert(
206
"signature".to_string(),
207
json!({"$bytes": BASE64.encode(signature_bytes)}),
···
212
213
/// Creates a remote attestation by generating a proof record and strongRef entry.
214
///
215
-
/// Returns a tuple containing:
216
-
/// - Remote proof record containing the CID for storage in a repository.
217
pub fn create_remote_attestation(
218
record: &Value,
219
attestation_metadata: &Value,
220
) -> Result<Value, AttestationError> {
221
let metadata = attestation_metadata
222
.as_object()
···
224
.ok_or(AttestationError::MetadataMustBeObject)?;
225
226
let metadata_value = Value::Object(metadata.clone());
227
-
let signing_record = prepare_signing_record(record, &metadata_value)?;
228
let cid = create_cid(&signing_record)?;
229
230
let mut remote_attestation = metadata.clone();
231
remote_attestation.insert("cid".to_string(), Value::String(cid.to_string()));
232
233
Ok(Value::Object(remote_attestation))
···
355
Ok(Value::Object(result))
356
}
357
358
-
/// Verify a single attestation entry at the specified index without a record resolver.
359
-
///
360
-
/// Inline signatures are reconstructed into `$sig` metadata, a CID is generated,
361
-
/// and the signature bytes are validated against the resolved public key.
362
-
/// Remote attestations will be reported as unverified.
363
///
364
-
/// This is a convenience function for the common case where no record resolver is needed.
365
-
/// For verifying remote attestations, use [`verify_signature_with_resolver`].
366
pub async fn verify_signature(
367
record: &Value,
368
index: usize,
369
key_resolver: Option<&dyn KeyResolver>,
370
) -> Result<VerificationReport, AttestationError> {
371
verify_signature_with_resolver::<atproto_client::record_resolver::HttpRecordResolver>(
372
record,
373
index,
374
key_resolver,
375
None,
376
)
377
.await
378
}
379
380
-
/// Verify a single attestation entry at the specified index with optional record resolver.
381
///
382
-
/// Inline signatures are reconstructed into `$sig` metadata, a CID is generated,
383
-
/// and the signature bytes are validated against the resolved public key.
384
-
/// Remote attestations can be verified if a `record_resolver` is provided to fetch
385
-
/// the proof record via AT-URI. Without a record resolver, remote attestations are
386
-
/// reported as unverified.
387
pub async fn verify_signature_with_resolver<R>(
388
record: &Value,
389
index: usize,
390
key_resolver: Option<&dyn KeyResolver>,
391
record_resolver: Option<&R>,
392
) -> Result<VerificationReport, AttestationError>
···
424
AttestationKind::Remote => {
425
match record_resolver {
426
Some(resolver) => {
427
-
match verify_remote_attestation(record, signature_map, resolver).await {
428
Ok(cid) => VerificationStatus::Valid { cid },
429
Err(error) => VerificationStatus::Invalid { error },
430
}
···
435
}
436
}
437
AttestationKind::Inline => {
438
-
match verify_inline_attestation(record, signature_map, key_resolver).await {
439
Ok(cid) => VerificationStatus::Valid { cid },
440
Err(error) => VerificationStatus::Invalid { error },
441
}
···
451
})
452
}
453
454
-
/// Verify all attestation entries attached to the record without a record resolver.
455
-
///
456
-
/// Returns a report per signature. Structural issues with the record (for
457
-
/// example, a missing `signatures` array) are returned as an error.
458
///
459
-
/// Remote attestations will be reported as unverified. For verifying remote
460
-
/// attestations, use [`verify_all_signatures_with_resolver`].
461
pub async fn verify_all_signatures(
462
record: &Value,
463
key_resolver: Option<&dyn KeyResolver>,
464
) -> Result<Vec<VerificationReport>, AttestationError> {
465
verify_all_signatures_with_resolver::<atproto_client::record_resolver::HttpRecordResolver>(
466
record,
467
key_resolver,
468
None,
469
)
470
.await
471
}
472
473
-
/// Verify all attestation entries attached to the record with optional record resolver.
474
-
///
475
-
/// Returns a report per signature. Structural issues with the record (for
476
-
/// example, a missing `signatures` array) are returned as an error.
477
///
478
-
/// If a `record_resolver` is provided, remote attestations will be fetched and verified.
479
-
/// Otherwise, remote attestations will be reported as unverified.
480
pub async fn verify_all_signatures_with_resolver<R>(
481
record: &Value,
482
key_resolver: Option<&dyn KeyResolver>,
483
record_resolver: Option<&R>,
484
) -> Result<Vec<VerificationReport>, AttestationError>
···
490
491
for index in 0..signatures_array.len() {
492
reports.push(
493
-
verify_signature_with_resolver(record, index, key_resolver, record_resolver).await?,
494
);
495
}
496
···
500
async fn verify_remote_attestation<R>(
501
record: &Value,
502
signature_object: &Map<String, Value>,
503
record_resolver: &R,
504
) -> Result<Cid, AttestationError>
505
where
···
560
.ok_or(AttestationError::RecordMustBeObject)?;
561
proof_metadata.remove("cid");
562
563
-
let signing_record = prepare_signing_record(record, &Value::Object(proof_metadata))?;
564
let computed_cid = create_cid(&signing_record)?;
565
566
// Verify the CID matches
···
577
async fn verify_inline_attestation(
578
record: &Value,
579
signature_object: &Map<String, Value>,
580
key_resolver: Option<&dyn KeyResolver>,
581
) -> Result<Cid, AttestationError> {
582
let key_reference = signature_object
···
604
let mut sig_metadata = signature_object.clone();
605
sig_metadata.remove("signature");
606
607
-
let signing_record = prepare_signing_record(record, &Value::Object(sig_metadata))?;
608
let cid = create_cid(&signing_record)?;
609
let cid_bytes = cid.to_bytes();
610
···
777
778
#[test]
779
fn prepare_signing_record_removes_signatures() -> Result<(), AttestationError> {
780
let record = json!({
781
"$type": "app.bsky.feed.post",
782
"text": "hello",
···
793
"cid": "bafyignored"
794
});
795
796
-
let prepared = prepare_signing_record(&record, &metadata)?;
797
let object = prepared.as_object().unwrap();
798
assert!(object.get("signatures").is_none());
799
assert!(object.get("sigs").is_none());
···
873
"$type": "com.example.inlineSignature"
874
});
875
876
-
let proof_record = create_remote_attestation(&record, &metadata)?;
877
878
let proof_object = proof_record
879
.as_object()
···
895
let private_key = generate_key(KeyType::K256Private)?;
896
let public_key = to_public(&private_key)?;
897
let key_reference = format!("{}", &public_key);
898
899
let base_record = json!({
900
"$type": "app.example.record",
···
907
"purpose": "unit-test"
908
});
909
910
-
let signed = create_inline_attestation(&base_record, &sig_metadata, &private_key)?;
911
912
-
let report = verify_signature(&signed, 0, None).await?;
913
match report.status {
914
VerificationStatus::Valid { .. } => {}
915
other => panic!("expected valid signature, got {:?}", other),
···
924
let public_key = to_public(&private_key)?;
925
let key_multibase = format!("{}", &public_key);
926
let key_reference = "did:plc:resolvertest#atproto".to_string();
927
928
let document = DocumentBuilder::new()
929
.id("did:plc:resolvertest")
···
953
"scope": "resolver"
954
});
955
956
-
let signed = create_inline_attestation(&base_record, &sig_metadata, &private_key)?;
957
958
-
let report = verify_signature(&signed, 0, Some(&key_resolver)).await?;
959
match report.status {
960
VerificationStatus::Valid { .. } => {}
961
other => panic!("expected valid signature, got {:?}", other),
···
966
967
#[tokio::test]
968
async fn verify_all_signatures_reports_remote() -> Result<(), Box<dyn std::error::Error>> {
969
let record = json!({
970
"$type": "app.example.record",
971
"signatures": [
···
977
]
978
});
979
980
-
let reports = verify_all_signatures(&record, None).await?;
981
assert_eq!(reports.len(), 1);
982
match &reports[0].status {
983
VerificationStatus::Unverified { reason } => {
···
994
let private_key = generate_key(KeyType::K256Private)?;
995
let public_key = to_public(&private_key)?;
996
let key_reference = format!("{}", &public_key);
997
998
let base_record = json!({
999
"$type": "app.example.record",
···
1005
"key": key_reference
1006
});
1007
1008
-
let mut signed = create_inline_attestation(&base_record, &sig_metadata, &private_key)?;
1009
if let Some(object) = signed.as_object_mut() {
1010
object.insert("body".to_string(), json!("tampered"));
1011
}
1012
1013
-
let report = verify_signature(&signed, 0, None).await?;
1014
match report.status {
1015
VerificationStatus::Invalid { .. } => {}
1016
other => panic!("expected invalid signature, got {:?}", other),
1017
}
1018
1019
Ok(())
1020
}
···
126
return Err(AttestationError::SigMetadataMissingType);
127
}
128
129
+
if !sig_object
130
+
.get("repository")
131
+
.and_then(Value::as_str)
132
+
.filter(|value| !value.is_empty())
133
+
.is_some()
134
+
{
135
+
return Err(AttestationError::SigMetadataMissingType);
136
+
}
137
+
138
let dag_cbor_bytes = serde_ipld_dagcbor::to_vec(record)?;
139
let digest = Sha256::digest(&dag_cbor_bytes);
140
let multihash = Multihash::wrap(0x12, &digest)
···
157
/// - Removes any existing `signatures`, `sigs`, and `$sig` fields.
158
/// - Inserts the provided `attestation` metadata as the new `$sig` object.
159
/// - Ensures the metadata contains a string `$type` discriminator.
160
+
/// - Ensures the metadata contains a `repository` field with the repository DID to prevent replay attacks.
161
pub fn prepare_signing_record(
162
record: &Value,
163
attestation: &Value,
164
+
repository_did: &str,
165
) -> Result<Value, AttestationError> {
166
let mut prepared = record
167
.as_object()
···
181
{
182
return Err(AttestationError::MetadataMissingSigType);
183
}
184
+
185
+
// CRITICAL: Always set repository field for attestations to prevent replay attacks
186
+
sig_metadata.insert("repository".to_string(), Value::String(repository_did.to_string()));
187
188
sig_metadata.remove("signature");
189
sig_metadata.remove("cid");
···
197
}
198
199
/// Creates an inline attestation by signing the prepared record with the provided key.
200
+
///
201
+
/// Signs the prepared record with the provided key and includes the repository DID
202
+
/// in the `$sig` metadata during CID generation to bind the attestation to a specific repository.
203
pub fn create_inline_attestation(
204
record: &Value,
205
attestation_metadata: &Value,
206
+
repository_did: &str,
207
signing_key: &KeyData,
208
) -> Result<Value, AttestationError> {
209
+
let signing_record = prepare_signing_record(record, attestation_metadata, repository_did)?;
210
let cid = create_cid(&signing_record)?;
211
212
let raw_signature = sign(signing_key, &cid.to_bytes())
···
220
221
inline_object.remove("signature");
222
inline_object.remove("cid");
223
+
inline_object.remove("repository"); // Don't include repository in final attestation object
224
inline_object.insert(
225
"signature".to_string(),
226
json!({"$bytes": BASE64.encode(signature_bytes)}),
···
231
232
/// Creates a remote attestation by generating a proof record and strongRef entry.
233
///
234
+
/// Generates a proof record containing the CID with the repository DID included
235
+
/// in the `$sig` metadata during CID generation to bind the attestation to a specific repository.
236
+
///
237
+
/// Returns the remote proof record for storage in a repository.
238
pub fn create_remote_attestation(
239
record: &Value,
240
attestation_metadata: &Value,
241
+
repository_did: &str,
242
) -> Result<Value, AttestationError> {
243
let metadata = attestation_metadata
244
.as_object()
···
246
.ok_or(AttestationError::MetadataMustBeObject)?;
247
248
let metadata_value = Value::Object(metadata.clone());
249
+
let signing_record = prepare_signing_record(record, &metadata_value, repository_did)?;
250
let cid = create_cid(&signing_record)?;
251
252
let mut remote_attestation = metadata.clone();
253
+
remote_attestation.remove("repository"); // Don't include repository in final proof record
254
remote_attestation.insert("cid".to_string(), Value::String(cid.to_string()));
255
256
Ok(Value::Object(remote_attestation))
···
378
Ok(Value::Object(result))
379
}
380
381
+
/// Verify a single attestation entry with repository binding.
382
///
383
+
/// Validates that the attestation was created for the specified repository DID
384
+
/// to prevent replay attacks.
385
pub async fn verify_signature(
386
record: &Value,
387
index: usize,
388
+
repository_did: &str,
389
key_resolver: Option<&dyn KeyResolver>,
390
) -> Result<VerificationReport, AttestationError> {
391
verify_signature_with_resolver::<atproto_client::record_resolver::HttpRecordResolver>(
392
record,
393
index,
394
+
repository_did,
395
key_resolver,
396
None,
397
)
398
.await
399
}
400
401
+
402
+
403
+
/// Verify a single attestation entry with repository binding and optional record resolver.
404
///
405
+
/// Validates that the attestation was created for the specified repository DID
406
+
/// to prevent replay attacks across different repositories.
407
pub async fn verify_signature_with_resolver<R>(
408
record: &Value,
409
index: usize,
410
+
repository_did: &str,
411
key_resolver: Option<&dyn KeyResolver>,
412
record_resolver: Option<&R>,
413
) -> Result<VerificationReport, AttestationError>
···
445
AttestationKind::Remote => {
446
match record_resolver {
447
Some(resolver) => {
448
+
match verify_remote_attestation(record, signature_map, repository_did, resolver).await {
449
Ok(cid) => VerificationStatus::Valid { cid },
450
Err(error) => VerificationStatus::Invalid { error },
451
}
···
456
}
457
}
458
AttestationKind::Inline => {
459
+
match verify_inline_attestation(record, signature_map, repository_did, key_resolver).await {
460
Ok(cid) => VerificationStatus::Valid { cid },
461
Err(error) => VerificationStatus::Invalid { error },
462
}
···
472
})
473
}
474
475
+
/// Verify all attestation entries with repository binding.
476
///
477
+
/// Validates that attestations were created for the specified repository DID
478
+
/// to prevent replay attacks.
479
pub async fn verify_all_signatures(
480
record: &Value,
481
+
repository_did: &str,
482
key_resolver: Option<&dyn KeyResolver>,
483
) -> Result<Vec<VerificationReport>, AttestationError> {
484
verify_all_signatures_with_resolver::<atproto_client::record_resolver::HttpRecordResolver>(
485
record,
486
+
repository_did,
487
key_resolver,
488
None,
489
)
490
.await
491
}
492
493
+
/// Verify all attestation entries with repository binding and optional record resolver.
494
///
495
+
/// Validates that all attestations were created for the specified repository DID
496
+
/// to prevent replay attacks across different repositories.
497
pub async fn verify_all_signatures_with_resolver<R>(
498
record: &Value,
499
+
repository_did: &str,
500
key_resolver: Option<&dyn KeyResolver>,
501
record_resolver: Option<&R>,
502
) -> Result<Vec<VerificationReport>, AttestationError>
···
508
509
for index in 0..signatures_array.len() {
510
reports.push(
511
+
verify_signature_with_resolver(
512
+
record,
513
+
index,
514
+
repository_did,
515
+
key_resolver,
516
+
record_resolver
517
+
).await?,
518
);
519
}
520
···
524
async fn verify_remote_attestation<R>(
525
record: &Value,
526
signature_object: &Map<String, Value>,
527
+
repository_did: &str,
528
record_resolver: &R,
529
) -> Result<Cid, AttestationError>
530
where
···
585
.ok_or(AttestationError::RecordMustBeObject)?;
586
proof_metadata.remove("cid");
587
588
+
let signing_record = prepare_signing_record(record, &Value::Object(proof_metadata), repository_did)?;
589
let computed_cid = create_cid(&signing_record)?;
590
591
// Verify the CID matches
···
602
async fn verify_inline_attestation(
603
record: &Value,
604
signature_object: &Map<String, Value>,
605
+
repository_did: &str,
606
key_resolver: Option<&dyn KeyResolver>,
607
) -> Result<Cid, AttestationError> {
608
let key_reference = signature_object
···
630
let mut sig_metadata = signature_object.clone();
631
sig_metadata.remove("signature");
632
633
+
let signing_record = prepare_signing_record(record, &Value::Object(sig_metadata), repository_did)?;
634
let cid = create_cid(&signing_record)?;
635
let cid_bytes = cid.to_bytes();
636
···
803
804
#[test]
805
fn prepare_signing_record_removes_signatures() -> Result<(), AttestationError> {
806
+
let repository_did = "did:plc:test";
807
let record = json!({
808
"$type": "app.bsky.feed.post",
809
"text": "hello",
···
820
"cid": "bafyignored"
821
});
822
823
+
let prepared = prepare_signing_record(&record, &metadata, repository_did)?;
824
let object = prepared.as_object().unwrap();
825
assert!(object.get("signatures").is_none());
826
assert!(object.get("sigs").is_none());
···
900
"$type": "com.example.inlineSignature"
901
});
902
903
+
let proof_record = create_remote_attestation(&record, &metadata, "did:plc:test")?;
904
905
let proof_object = proof_record
906
.as_object()
···
922
let private_key = generate_key(KeyType::K256Private)?;
923
let public_key = to_public(&private_key)?;
924
let key_reference = format!("{}", &public_key);
925
+
let repository_did = "did:plc:testrepository123";
926
927
let base_record = json!({
928
"$type": "app.example.record",
···
935
"purpose": "unit-test"
936
});
937
938
+
let signed = create_inline_attestation(
939
+
&base_record,
940
+
&sig_metadata,
941
+
repository_did,
942
+
&private_key,
943
+
)?;
944
945
+
let report = verify_signature(&signed, 0, repository_did, None).await?;
946
match report.status {
947
VerificationStatus::Valid { .. } => {}
948
other => panic!("expected valid signature, got {:?}", other),
···
957
let public_key = to_public(&private_key)?;
958
let key_multibase = format!("{}", &public_key);
959
let key_reference = "did:plc:resolvertest#atproto".to_string();
960
+
let repository_did = "did:plc:resolvertest";
961
962
let document = DocumentBuilder::new()
963
.id("did:plc:resolvertest")
···
987
"scope": "resolver"
988
});
989
990
+
let signed = create_inline_attestation(
991
+
&base_record,
992
+
&sig_metadata,
993
+
repository_did,
994
+
&private_key,
995
+
)?;
996
997
+
let report =
998
+
verify_signature(&signed, 0, repository_did, Some(&key_resolver))
999
+
.await?;
1000
match report.status {
1001
VerificationStatus::Valid { .. } => {}
1002
other => panic!("expected valid signature, got {:?}", other),
···
1007
1008
#[tokio::test]
1009
async fn verify_all_signatures_reports_remote() -> Result<(), Box<dyn std::error::Error>> {
1010
+
let repository_did = "did:plc:example";
1011
let record = json!({
1012
"$type": "app.example.record",
1013
"signatures": [
···
1019
]
1020
});
1021
1022
+
let reports = verify_all_signatures(&record, repository_did, None).await?;
1023
assert_eq!(reports.len(), 1);
1024
match &reports[0].status {
1025
VerificationStatus::Unverified { reason } => {
···
1036
let private_key = generate_key(KeyType::K256Private)?;
1037
let public_key = to_public(&private_key)?;
1038
let key_reference = format!("{}", &public_key);
1039
+
let repository_did = "did:plc:tampertest";
1040
1041
let base_record = json!({
1042
"$type": "app.example.record",
···
1048
"key": key_reference
1049
});
1050
1051
+
let mut signed = create_inline_attestation(
1052
+
&base_record,
1053
+
&sig_metadata,
1054
+
repository_did,
1055
+
&private_key,
1056
+
)?;
1057
if let Some(object) = signed.as_object_mut() {
1058
object.insert("body".to_string(), json!("tampered"));
1059
}
1060
1061
+
let report = verify_signature(&signed, 0, repository_did, None).await?;
1062
match report.status {
1063
VerificationStatus::Invalid { .. } => {}
1064
other => panic!("expected invalid signature, got {:?}", other),
1065
}
1066
+
1067
+
Ok(())
1068
+
}
1069
+
1070
+
#[tokio::test]
1071
+
async fn verify_repository_field_prevents_replay_attack(
1072
+
) -> Result<(), Box<dyn std::error::Error>> {
1073
+
let private_key = generate_key(KeyType::K256Private)?;
1074
+
let public_key = to_public(&private_key)?;
1075
+
let key_reference = format!("{}", &public_key);
1076
+
let original_repository = "did:plc:originalrepo";
1077
+
let attacker_repository = "did:plc:attackerrepo";
1078
+
1079
+
let base_record = json!({
1080
+
"$type": "app.example.record",
1081
+
"body": "Important content"
1082
+
});
1083
+
1084
+
let sig_metadata = json!({
1085
+
"$type": "com.example.inlineSignature",
1086
+
"key": key_reference,
1087
+
"purpose": "original-attestation"
1088
+
});
1089
+
1090
+
// Create attestation for original repository
1091
+
let signed = create_inline_attestation(
1092
+
&base_record,
1093
+
&sig_metadata,
1094
+
original_repository,
1095
+
&private_key,
1096
+
)?;
1097
+
1098
+
// Verify succeeds with correct repository
1099
+
let report =
1100
+
verify_signature(&signed, 0, original_repository, None).await?;
1101
+
match report.status {
1102
+
VerificationStatus::Valid { .. } => {}
1103
+
other => panic!("expected valid signature for original repo, got {:?}", other),
1104
+
}
1105
+
1106
+
// Verify FAILS with different repository (simulating replay attack)
1107
+
let report =
1108
+
verify_signature(&signed, 0, attacker_repository, None).await?;
1109
+
match report.status {
1110
+
VerificationStatus::Invalid { .. } => {}
1111
+
other => panic!(
1112
+
"expected invalid signature for attacker repo, got {:?}",
1113
+
other
1114
+
),
1115
+
}
1116
+
1117
+
Ok(())
1118
+
}
1119
+
1120
+
#[test]
1121
+
fn prepare_signing_record_enforces_repository() -> Result<(), AttestationError> {
1122
+
let record = json!({
1123
+
"$type": "app.example.record",
1124
+
"text": "Test content"
1125
+
});
1126
+
1127
+
let metadata = json!({
1128
+
"$type": "com.example.attestationType",
1129
+
"purpose": "test"
1130
+
});
1131
+
1132
+
let repository_did = "did:plc:testrepo123";
1133
+
1134
+
// Prepare with repository field
1135
+
let prepared = prepare_signing_record(&record, &metadata, repository_did)?;
1136
+
let prepared_obj = prepared.as_object().unwrap();
1137
+
let sig_obj = prepared_obj.get("$sig").unwrap().as_object().unwrap();
1138
+
1139
+
// Verify repository field is set correctly
1140
+
assert_eq!(
1141
+
sig_obj.get("repository").and_then(Value::as_str),
1142
+
Some(repository_did)
1143
+
);
1144
+
1145
+
// Verify $type is preserved
1146
+
assert_eq!(
1147
+
sig_obj.get("$type").and_then(Value::as_str),
1148
+
Some("com.example.attestationType")
1149
+
);
1150
+
1151
+
// Verify original metadata fields are preserved
1152
+
assert_eq!(
1153
+
sig_obj.get("purpose").and_then(Value::as_str),
1154
+
Some("test")
1155
+
);
1156
1157
Ok(())
1158
}