Skip to main content

interop_cli/
interop_cli.rs

1use 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}