interop_cli/
interop_cli.rs1use std::collections::BTreeMap;
2
3use anp::authentication::{
4 create_did_wba_document, generate_auth_header, generate_http_signature_headers,
5 DidDocumentOptions, DidProfile,
6};
7use anp::{PrivateKeyMaterial, PublicKeyMaterial};
8use serde_json::json;
9
10fn main() {
11 let args = std::env::args().skip(1).collect::<Vec<String>>();
12 if args.is_empty() {
13 eprintln!(
14 "Usage: cargo run --example interop_cli -- <did-fixture|auth-fixture|verify-key-fixture> [options]"
15 );
16 std::process::exit(1);
17 }
18
19 match args[0].as_str() {
20 "did-fixture" => run_did_fixture(&args[1..]),
21 "auth-fixture" => run_auth_fixture(&args[1..]),
22 "verify-key-fixture" => run_verify_key_fixture(&args[1..]),
23 other => {
24 eprintln!("Unsupported subcommand: {}", other);
25 std::process::exit(1);
26 }
27 }
28}
29
30fn run_did_fixture(args: &[String]) {
31 let profile = read_option(args, "--profile").unwrap_or_else(|| "e1".to_string());
32 let hostname = read_option(args, "--hostname").unwrap_or_else(|| "example.com".to_string());
33 let bundle = create_bundle(&hostname, &profile);
34 println!(
35 "{}",
36 serde_json::to_string(&json!({
37 "profile": profile,
38 "did_document": bundle.did_document,
39 "keys": bundle.keys,
40 }))
41 .expect("fixture should serialize")
42 );
43}
44
45fn run_verify_key_fixture(args: &[String]) {
46 let fixture_path = read_option(args, "--fixture").expect("--fixture is required");
47 let content = std::fs::read_to_string(&fixture_path).expect("fixture should be readable");
48 let fixture: serde_json::Value =
49 serde_json::from_str(&content).expect("fixture should be valid JSON");
50 let keys = fixture["keys"]
51 .as_object()
52 .expect("keys should be an object");
53 for (fragment, key_pair) in keys {
54 let private_pem = key_pair["private_key_pem"]
55 .as_str()
56 .expect("private_key_pem should be a string");
57 let public_pem = key_pair["public_key_pem"]
58 .as_str()
59 .expect("public_key_pem should be a string");
60 assert!(
61 private_pem.starts_with("-----BEGIN PRIVATE KEY-----"),
62 "{fragment} private key must be PKCS#8 PEM"
63 );
64 assert!(
65 public_pem.starts_with("-----BEGIN PUBLIC KEY-----"),
66 "{fragment} public key must be SPKI PEM"
67 );
68 assert!(!private_pem.contains("ANP "));
69 assert!(!public_pem.contains("ANP "));
70
71 let private_key = PrivateKeyMaterial::from_pem(private_pem)
72 .expect("private key should parse as standard PEM");
73 let public_key =
74 PublicKeyMaterial::from_pem(public_pem).expect("public key should parse as SPKI PEM");
75 if !matches!(public_key, PublicKeyMaterial::X25519(_)) {
76 let signature = private_key
77 .sign_message(b"cross-language standard pem")
78 .expect("signature should be created");
79 public_key
80 .verify_message(b"cross-language standard pem", &signature)
81 .expect("signature should verify");
82 }
83 }
84 println!(
85 "{}",
86 serde_json::to_string(&json!({"verified": true, "key_count": keys.len()}))
87 .expect("result should serialize")
88 );
89}
90
91fn run_auth_fixture(args: &[String]) {
92 let profile = read_option(args, "--profile").unwrap_or_else(|| "e1".to_string());
93 let hostname = read_option(args, "--hostname").unwrap_or_else(|| "example.com".to_string());
94 let scheme = read_option(args, "--scheme").unwrap_or_else(|| "http".to_string());
95 let service_domain =
96 read_option(args, "--service-domain").unwrap_or_else(|| "api.example.com".to_string());
97 let request_url =
98 read_option(args, "--url").unwrap_or_else(|| format!("https://{}/orders", service_domain));
99 let request_method = read_option(args, "--method").unwrap_or_else(|| "GET".to_string());
100 let body = read_option(args, "--body").unwrap_or_default();
101
102 let bundle = create_bundle(&hostname, &profile);
103 let private_key = bundle
104 .load_private_key("key-1")
105 .expect("private key should load");
106
107 let output = match scheme.as_str() {
108 "legacy" => {
109 let header =
110 generate_auth_header(&bundle.did_document, &service_domain, &private_key, "1.1")
111 .expect("legacy auth header should generate");
112 json!({
113 "profile": profile,
114 "scheme": scheme,
115 "service_domain": service_domain,
116 "did_document": bundle.did_document,
117 "keys": bundle.keys,
118 "headers": {"Authorization": header},
119 })
120 }
121 "http" => {
122 let body_bytes = if body.is_empty() {
123 None
124 } else {
125 Some(body.as_bytes())
126 };
127 let headers = generate_http_signature_headers(
128 &bundle.did_document,
129 &request_url,
130 &request_method,
131 &private_key,
132 Some(&BTreeMap::new()),
133 body_bytes,
134 Default::default(),
135 )
136 .expect("HTTP signature headers should generate");
137 json!({
138 "profile": profile,
139 "scheme": scheme,
140 "service_domain": service_domain,
141 "request_url": request_url,
142 "request_method": request_method,
143 "body": body,
144 "did_document": bundle.did_document,
145 "keys": bundle.keys,
146 "headers": headers,
147 })
148 }
149 other => panic!("Unsupported scheme: {}", other),
150 };
151
152 println!(
153 "{}",
154 serde_json::to_string(&output).expect("fixture should serialize")
155 );
156}
157
158fn create_bundle(hostname: &str, profile: &str) -> anp::authentication::DidDocumentBundle {
159 let did_profile =
160 DidProfile::from_str(profile).expect("profile must be one of: e1, k1, plain_legacy");
161 create_did_wba_document(
162 hostname,
163 DidDocumentOptions::default()
164 .with_profile(did_profile)
165 .with_path_segments(["user", "interop"]),
166 )
167 .expect("DID fixture should be created")
168}
169
170fn read_option(args: &[String], flag: &str) -> Option<String> {
171 args.windows(2)
172 .find(|window| window[0] == flag)
173 .map(|window| window[1].clone())
174}