use std::fs;
use std::io::Write;
use std::path::PathBuf;
use serde::Deserialize;
use vta_sdk::attestation::verify_nitro_assertion;
use vta_sdk::sealed_transfer::{
BootstrapRequest, SealedPayloadV1, armor, bundle_digest, ed25519_seed_to_x25519_secret,
generate_ed25519_keypair, open_bundle,
};
use crate::auth;
use crate::config;
use vta_sdk::hex::lower as hex_lower;
pub async fn run_request(
out: PathBuf,
label: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
let config_dir = config::config_dir()?;
let created = vta_cli_common::sealed_consumer::create_bootstrap_request(&config_dir, label)?;
let json = serde_json::to_string_pretty(&created.request)?;
fs::write(&out, json.as_bytes())?;
println!("Bootstrap request written to {}", out.display());
println!();
println!(" Bundle-Id: {}", created.bundle_id_hex);
println!(" Client DID: {}", created.request.client_did);
println!(" Seed saved: {}", created.secret_path.display());
println!();
println!("Hand the request to the producer. They will return an armored bundle.");
println!("Verify the SHA-256 digest they print to you out-of-band, then run:");
println!(" pnm bootstrap open --bundle <file> --expect-digest <hex>");
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub async fn run_provision_request(
template: String,
vars: Vec<String>,
context_hint: Option<String>,
admin_template: Option<String>,
validity_hours: f64,
label: Option<String>,
out: PathBuf,
) -> Result<(), Box<dyn std::error::Error>> {
use vta_sdk::provision_integration::ProvisionRequestBuilder;
if !validity_hours.is_finite() || validity_hours <= 0.0 {
return Err(format!(
"--validity-hours must be a positive finite number, got {validity_hours}"
)
.into());
}
let validity = chrono::Duration::seconds((validity_hours * 3600.0) as i64);
let mut builder = ProvisionRequestBuilder::new(template).validity(validity);
for raw in &vars {
let (k, v) = parse_var(raw)?;
builder = builder.var(k, v);
}
if let Some(ctx) = context_hint {
builder = builder.context_hint(ctx);
}
if let Some(admin) = admin_template {
builder = builder.admin_template(admin);
}
if let Some(l) = label {
builder = builder.label(l);
}
let config_dir = config::config_dir()?;
let created =
vta_cli_common::sealed_consumer::create_provision_request(&config_dir, builder).await?;
let json = serde_json::to_string_pretty(&created.request)?;
fs::write(&out, json.as_bytes())?;
println!("Provision bootstrap request written to {}", out.display());
println!();
println!(" Bundle-Id: {}", created.bundle_id_hex);
println!(" Client DID: {}", created.client_did);
println!(" Seed saved: {}", created.secret_path.display());
println!();
println!("Hand the request to the VTA operator. They will run:");
println!(" vta bootstrap provision-integration --request <file> --out <bundle>");
println!("(or `pnm bootstrap provision-integration` over REST against a live VTA).");
println!();
println!("Verify the returned digest out-of-band, then:");
println!(" pnm bootstrap open --bundle <file> --expect-digest <hex>");
Ok(())
}
fn parse_var(raw: &str) -> Result<(String, serde_json::Value), Box<dyn std::error::Error>> {
let (key, value) = raw
.split_once('=')
.ok_or_else(|| format!("invalid --var '{raw}': expected KEY=VALUE"))?;
if key.is_empty() {
return Err(format!("invalid --var '{raw}': empty key").into());
}
let parsed = serde_json::from_str::<serde_json::Value>(value)
.unwrap_or_else(|_| serde_json::Value::String(value.to_string()));
Ok((key.to_string(), parsed))
}
pub async fn run_open(
bundle_path: PathBuf,
out: Option<PathBuf>,
expect_digest: Option<String>,
no_verify_digest: bool,
expect_vta_did: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
let config_dir = config::config_dir()?;
let opened = vta_cli_common::sealed_consumer::open_armored_bundle(
&bundle_path,
&config_dir,
expect_digest.as_deref(),
no_verify_digest,
)?;
println!("Sealed bundle opened.");
println!();
println!(" Bundle-Id: {}", opened.bundle_id_hex);
println!(" Digest (sha256): {}", opened.digest);
println!(" Producer DID: {}", opened.producer.producer_did);
println!(" Producer proof: {:?}", opened.producer.proof);
println!();
match &opened.payload {
SealedPayloadV1::AdminCredential(c) => {
println!("Payload: AdminCredential");
println!(" DID: {}", c.did);
println!(" VTA DID: {}", c.vta_did);
if let Some(ref u) = c.vta_url {
println!(" VTA URL: {u}");
}
if out.is_none() {
println!();
println!("Nothing was written. To install this credential either:");
println!(" - re-open with --out <path> for a file-based consumer, or");
println!(
" - use the online flow: pnm bootstrap connect --vta-did <did> \
--expect-pcr0 <hex> [--expect-pcr8 <hex>]"
);
println!(
" (--vta-url <url> is the fallback when the DID log is not yet \
resolvable; it does not pin the VTA's identity)"
);
}
}
SealedPayloadV1::ContextProvision(p) => {
println!("Payload: ContextProvision");
println!(" Context: {} ({})", p.context_id, p.context_name);
println!(" Admin DID: {}", p.admin_did);
if out.is_none() {
println!();
println!(
"Nothing was written — this payload carries an admin credential. \
Re-open with --out <path> to write it as JSON."
);
}
}
SealedPayloadV1::DidSecrets(s) => {
println!("Payload: DidSecrets");
println!(" DID: {}", s.did);
println!(" Secrets: {}", s.secrets.len());
}
SealedPayloadV1::AdminKeySet(keys) => {
println!("Payload: AdminKeySet ({} keys)", keys.len());
for k in keys {
println!(" - {}", k.label);
}
}
SealedPayloadV1::RawPrivateKey(k) => {
println!("Payload: RawPrivateKey ({})", k.key_type);
}
SealedPayloadV1::TemplateBootstrap(p) => {
println!("Payload: TemplateBootstrap");
println!(" Template: {}", p.config.template_name);
println!(" Kind: {}", p.config.template_kind);
println!(" Secrets for: {} DID(s)", p.secrets.len());
println!(" Outputs: {}", p.config.outputs.len());
if let Some(ref u) = p.config.vta_url {
println!(" VTA URL: {u}");
}
match expect_vta_did.as_deref() {
Some(pinned) => {
use vta_sdk::provision_integration::template_verify::verify_template_bootstrap;
verify_template_bootstrap((**p).clone(), pinned, chrono::Duration::minutes(5))?;
println!();
println!(" \x1b[1;32m✓ VC verified against pinned VTA DID\x1b[0m");
}
None => {
println!();
println!(" \x1b[1;33m⚠ VC NOT verified — digest-only trust anchor.\x1b[0m");
println!(
" Re-run with --expect-vta-did <did> to verify the authorization VC."
);
}
}
println!();
println!("Install via the provision-integration flow on the integration host.");
}
SealedPayloadV1::AdminRotation(p) => {
println!("Payload: AdminRotation");
println!(" Admin DID: {}", p.admin.did);
println!(" VTA DID: {}", p.vta_trust.vta_did);
if let Some(ref u) = p.vta_url {
println!(" VTA URL: {u}");
}
println!();
println!(
" \x1b[1;33m⚠ VC verification not yet wired for AdminRotation — digest-only.\x1b[0m"
);
println!();
println!("Install via the provision-integration flow on the integration host.");
}
SealedPayloadV1::IssuedCredential(c) => {
println!("Payload: IssuedCredential");
println!(" Issuer DID: {}", c.issuer_did);
if let Some(ref label) = c.label {
println!(" Label: {label}");
}
let kind = if c.credential.is_string() {
"SD-JWT-VC (compact)"
} else {
"W3C Data-Integrity VC"
};
println!(" Format: {kind}");
println!();
println!(
"Receive this credential into the holder vault via the credential-exchange flow."
);
}
SealedPayloadV1::MessagingBridgeCredentials(b) => {
println!("Payload: MessagingBridgeCredentials");
println!(" Platform: {}", b.platform);
println!(" Fields: {}", b.fields.len());
println!();
println!(
"Load these platform secrets into the messaging-bridge connector's secret store."
);
}
}
if let Some(path) = out {
let bundle = vta_cli_common::sealed_consumer::verify_admin_bundle(
opened,
expect_digest.as_deref(),
expect_vta_did.as_deref(),
)?;
write_credential_bundle(&path, &bundle)?;
println!();
println!("Credential written to {} (0600).", path.display());
}
Ok(())
}
fn write_credential_bundle(
path: &std::path::Path,
bundle: &vta_sdk::credentials::CredentialBundle,
) -> Result<(), Box<dyn std::error::Error>> {
let json = serde_json::to_vec_pretty(bundle)?;
let mut opts = fs::OpenOptions::new();
opts.create(true).write(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
let mut file = opts.open(path)?;
file.write_all(&json)?;
file.write_all(b"\n")?;
drop(file);
if let Err(e) = vta_cli_common::secure_file::restrict_file_to_owner(path) {
eprintln!(
"warning: could not restrict {} to owner ({e}) — the private key may be \
readable by other local users",
path.display()
);
}
Ok(())
}
#[derive(Debug, Deserialize)]
struct BootstrapResponseWire {
bundle: String,
digest: String,
}
async fn resolve_connect_url(
vta_did: &str,
resolver: &affinidi_did_resolver_cache_sdk::DIDCacheClient,
) -> Result<String, Box<dyn std::error::Error>> {
resolve_connect_url_with_policy(
vta_did,
resolver,
vta_sdk::http::EndpointPolicy::process_default(),
)
.await
}
async fn resolve_connect_url_with_policy(
vta_did: &str,
resolver: &affinidi_did_resolver_cache_sdk::DIDCacheClient,
policy: vta_sdk::http::EndpointPolicy,
) -> Result<String, Box<dyn std::error::Error>> {
let url = async {
let resolved = resolver.resolve(vta_did).await?;
let doc = serde_json::to_value(&resolved.doc)?;
if doc["id"].as_str() != Some(vta_did) {
return Err("resolved DID document does not match requested VTA DID".into());
}
vta_sdk::protocol::matching::ServiceCapabilities::from_did_document(&doc)
.rest
.ok_or_else(|| "VTA DID document does not advertise a REST endpoint".into())
}
.await
.map_err(|e: Box<dyn std::error::Error>| {
format!(
"Could not resolve bootstrap endpoint for VTA DID {vta_did}: {e}. \
Resolution is always local here — a configured PNM_RESOLVER_URL / \
[resolver_url] is not used for bootstrap, so a reachable resolver \
sidecar does not satisfy this. Retry once its DID log is published \
and reachable from this machine, or explicitly use --vta-url <URL> \
instead of --vta-did (without DID identity pinning)."
)
})?;
vta_sdk::http::guard_vta_endpoint(&url, policy).map_err(|e| {
format!("VTA DID {vta_did} advertises a REST endpoint that is not safe to call: {e}")
})?;
Ok(url.trim_end_matches('/').to_string())
}
fn check_connect_vta_did(
expected: Option<&str>,
actual: &str,
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(expected) = expected
&& expected != actual
{
return Err(format!(
"bootstrap credential VTA DID {actual} does not match requested VTA DID {expected}; \
refusing to install credentials"
)
.into());
}
Ok(())
}
fn validate_connect_anchor(
expect_digest: Option<&str>,
no_verify_digest: bool,
expect_pcr0: Option<&str>,
expect_pcr8: Option<&str>,
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(pcr0) = expect_pcr0 {
vta_sdk::attestation::validate_expected_pcr(0, pcr0)?;
}
if let Some(pcr8) = expect_pcr8 {
vta_sdk::attestation::validate_expected_pcr(8, pcr8)?;
}
if expect_digest.is_some() || no_verify_digest {
return vta_cli_common::sealed_consumer::validate_digest_flags(
expect_digest,
no_verify_digest,
);
}
if expect_pcr0.is_some() {
return Ok(());
}
Err(
"connect requires --expect-pcr0 <hex>, --expect-digest <hex>, or \
--no-verify-digest (explicit opt-out with a warning). The connect digest is \
generated server-side during this call and cannot be pre-shared; pin \
--expect-pcr0 to the expected enclave image measurement."
.into(),
)
}
#[allow(clippy::too_many_arguments)]
pub async fn run_connect(
vta_did: Option<String>,
vta_url: Option<String>,
expect_digest: Option<String>,
no_verify_digest: bool,
expect_pcr0: Option<String>,
expect_pcr8: Option<String>,
vta_slug: Option<String>,
pnm_config: &mut crate::config::PnmConfig,
) -> Result<(), Box<dyn std::error::Error>> {
validate_connect_anchor(
expect_digest.as_deref(),
no_verify_digest,
expect_pcr0.as_deref(),
expect_pcr8.as_deref(),
)?;
let vta_url = match (vta_did.as_deref(), vta_url) {
(Some(did), None) => {
let resolver = affinidi_did_resolver_cache_sdk::DIDCacheClient::new(
vta_sdk::resolver::build_did_cache_config(None),
)
.await?;
resolve_connect_url(did, &resolver).await?
}
(None, Some(url)) => url,
_ => return Err("provide exactly one of --vta-did or --vta-url".into()),
};
let (ed_seed, ed_pub) = generate_ed25519_keypair();
let nonce: [u8; 16] = rand::random();
let bundle_id_hex = hex_lower(&nonce);
let body = BootstrapRequest::new(ed_pub, nonce, None);
const MAX_BOOTSTRAP_RESPONSE_BYTES: usize = 1024 * 1024;
let url = format!("{}/bootstrap/request", vta_url.trim_end_matches('/'));
let client = vta_sdk::http::rest_client();
let resp = client.post(&url).json(&body).send().await?;
let status = resp.status();
let bytes = vta_sdk::http::read_body_capped(resp, MAX_BOOTSTRAP_RESPONSE_BYTES)
.await
.map_err(|e| format!("bootstrap request ({status}): {e}"))?;
if !status.is_success() {
let body = String::from_utf8_lossy(&bytes);
return Err(format!("bootstrap request failed ({status}): {body}").into());
}
let wire: BootstrapResponseWire = serde_json::from_slice(&bytes)?;
if let Some(expected) = &expect_digest
&& !expected.eq_ignore_ascii_case(&wire.digest)
{
return Err(format!(
"server-reported digest {} does not match expected {}",
wire.digest, expected
)
.into());
}
let bundles = armor::decode(&wire.bundle)?;
if bundles.len() != 1 {
return Err(format!(
"expected exactly one armored bundle from server, got {}",
bundles.len()
)
.into());
}
let bundle = &bundles[0];
if hex_lower(&bundle.bundle_id) != bundle_id_hex {
return Err("server-returned bundle_id does not match our nonce".into());
}
let x_secret = ed25519_seed_to_x25519_secret(&ed_seed);
let opened = open_bundle(&x_secret, bundle, expect_digest.as_deref())?;
let attest = verify_nitro_assertion(&opened.producer, &ed_pub, &nonce)?;
attest
.check_pcrs(expect_pcr0.as_deref(), expect_pcr8.as_deref())
.map_err(|e| {
format!(
"{e}. The attestation is cryptographically valid but the enclave does not \
match the pinned measurement — refusing to bootstrap. Confirm the expected \
PCR against the deployed EIF / KMS key policy."
)
})?;
println!("TEE attestation verified.");
println!(" Enclave module: {}", attest.module_id);
if !attest.pcr0_hex.is_empty() {
let pinned = if expect_pcr0.is_some() {
" (pinned ✓)"
} else {
""
};
println!(" PCR0: {}{pinned}", attest.pcr0_hex);
}
if !attest.pcr8_hex.is_empty() {
let pinned = if expect_pcr8.is_some() {
" (pinned ✓)"
} else {
""
};
println!(" PCR8: {}{pinned}", attest.pcr8_hex);
}
let credential = match opened.payload {
SealedPayloadV1::AdminCredential(c) => c,
other => {
return Err(format!(
"expected AdminCredential payload from online bootstrap, got {}",
variant_name(&other)
)
.into());
}
};
check_connect_vta_did(vta_did.as_deref(), &credential.vta_did)?;
let slug = vta_slug.unwrap_or_else(|| default_slug(&credential.vta_did));
pnm_config.vtas.insert(
slug.clone(),
crate::config::VtaConfig {
name: slug.clone(),
vta_did: Some(credential.vta_did.clone()),
url: None,
mediator_did: None,
},
);
if pnm_config.default_vta.is_none() {
pnm_config.default_vta = Some(slug.clone());
}
crate::config::save_config(pnm_config)?;
let keyring_key = crate::config::vta_keyring_key(&slug);
auth::store_session(
&keyring_key,
&credential.did,
&credential.private_key_multibase,
&credential.vta_did,
)?;
auth::ensure_authenticated(&vta_url, &keyring_key).await?;
println!();
println!("Bootstrap complete.");
println!(" VTA slug: {slug}");
println!(" Client DID: {}", credential.did);
println!(" VTA DID: {}", credential.vta_did);
println!(" Digest: {}", bundle_digest(bundle));
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub async fn run_provision_integration(
client: &vta_sdk::client::VtaClient,
request: PathBuf,
context: Option<String>,
assertion: String,
vc_validity_seconds: Option<i64>,
out: PathBuf,
create_context: bool,
admin_scope: String,
) -> Result<(), Box<dyn std::error::Error>> {
use vta_sdk::provision_integration::http::{
AdminScope, AssertionMode as WireAssertionMode, ProvisionIntegrationRequest,
};
let request_json =
fs::read_to_string(&request).map_err(|e| format!("read {}: {e}", request.display()))?;
let vp_raw: serde_json::Value = serde_json::from_str(&request_json)
.map_err(|e| format!("parse BootstrapRequest (VP): {e}"))?;
let vp: vta_sdk::provision_integration::BootstrapRequest =
serde_json::from_value(vp_raw.clone())
.map_err(|e| format!("parse BootstrapRequest (VP): {e}"))?;
let target_context = resolve_target_context_wire(&vp, context)?;
let assertion_mode = match assertion.as_str() {
"did-signed" | "didsigned" | "did_signed" => WireAssertionMode::DidSigned,
"pinned-only" | "pinnedonly" | "pinned_only" | "pinned" => WireAssertionMode::PinnedOnly,
other => {
return Err(format!(
"invalid --assertion value '{other}' — use 'did-signed' or 'pinned-only'"
)
.into());
}
};
let admin_scope = match admin_scope.as_str() {
"context" => AdminScope::Context,
"unrestricted" => AdminScope::Unrestricted,
other => {
return Err(format!(
"invalid --admin-scope value '{other}' — use 'context' or 'unrestricted'"
)
.into());
}
};
let resp = client
.provision_integration(ProvisionIntegrationRequest {
request: vp_raw,
context: Some(target_context.clone()),
assertion: Some(assertion_mode),
vc_validity_seconds,
create_context,
admin_scope,
})
.await?;
fs::write(&out, resp.bundle.as_bytes()).map_err(|e| format!("write {}: {e}", out.display()))?;
eprintln!(
"Integration provisioned via {} — sealed bundle written to {}",
client.endpoint_label(),
out.display()
);
eprintln!();
eprintln!(" Bundle-Id: {}", resp.summary.bundle_id_hex);
if resp.summary.context_created {
eprintln!(" Context: {target_context} (created inline via --create-context)");
} else if create_context {
eprintln!(
" Context: {target_context} (already existed; --create-context was a no-op)"
);
} else {
eprintln!(" Context: {target_context}");
}
eprintln!(" Client DID: {}", resp.summary.client_did);
if resp.summary.admin_rolled_over {
eprintln!(
" Admin DID: {} (VTA-minted, rolled over from client)",
resp.summary.admin_did
);
if let Some(ref admin_tpl) = resp.summary.admin_template_name {
eprintln!(" Admin template: {admin_tpl}");
}
} else {
eprintln!(" Admin DID: {} (== client)", resp.summary.admin_did);
}
if let Some(ref integration_did) = resp.summary.integration_did {
eprintln!(" Integration DID: {integration_did}");
} else {
eprintln!(" Integration DID: (none — admin-rotation only)");
}
if let (Some(name), Some(kind)) = (
resp.summary.template_name.as_deref(),
resp.summary.template_kind.as_deref(),
) {
eprintln!(" Template: {name} ({kind})");
}
eprintln!(" Secrets: {}", resp.summary.secret_count);
eprintln!(" Outputs: {}", resp.summary.output_count);
let digest_hex = resp
.digest_multibase
.as_deref()
.map(vta_sdk::sealed_transfer::multibase_digest_to_hex)
.transpose()
.unwrap_or(None);
match digest_hex {
Some(hex) => {
eprintln!(" SHA-256 digest: {hex}");
eprintln!();
eprintln!(
"Communicate the digest to the integration's operator out-of-band so they can\n \
verify the bundle on first boot:\n \
pnm bootstrap open --bundle <file> --expect-digest {hex}",
);
}
None => {
eprintln!(" SHA-256 digest: (not supplied by this VTA)");
eprintln!();
}
}
Ok(())
}
fn resolve_target_context_wire(
request: &vta_sdk::provision_integration::BootstrapRequest,
explicit: Option<String>,
) -> Result<String, Box<dyn std::error::Error>> {
use vta_sdk::provision_integration::BootstrapAsk;
let hint = match &request.ask {
BootstrapAsk::TemplateBootstrap(ask) => ask.context_hint.clone(),
BootstrapAsk::AdminRotation(ask) => ask.context_hint.clone(),
};
match (explicit, hint) {
(Some(explicit), Some(hint)) if explicit != hint => Err(format!(
"--context '{explicit}' does not match request contextHint '{hint}' — \
operator and integration must agree on the context before provisioning"
)
.into()),
(Some(explicit), _) => Ok(explicit),
(None, Some(hint)) => Ok(hint),
(None, None) => Err(
"no context specified — pass --context <id> or have the integration's \
BootstrapRequest include a contextHint"
.into(),
),
}
}
fn variant_name(p: &SealedPayloadV1) -> &'static str {
match p {
SealedPayloadV1::AdminCredential(_) => "AdminCredential",
SealedPayloadV1::ContextProvision(_) => "ContextProvision",
SealedPayloadV1::DidSecrets(_) => "DidSecrets",
SealedPayloadV1::AdminKeySet(_) => "AdminKeySet",
SealedPayloadV1::RawPrivateKey(_) => "RawPrivateKey",
SealedPayloadV1::TemplateBootstrap(_) => "TemplateBootstrap",
SealedPayloadV1::AdminRotation(_) => "AdminRotation",
SealedPayloadV1::IssuedCredential(_) => "IssuedCredential",
SealedPayloadV1::MessagingBridgeCredentials(_) => "MessagingBridgeCredentials",
}
}
fn default_slug(vta_did: &str) -> String {
vta_did.rsplit(':').next().unwrap_or("vta").to_string()
}
#[cfg(test)]
mod tests {
use super::parse_var;
use serde_json::Value;
const VTA_DID: &str =
"did:webvh:Qmd1FCL9Vj2vJ433UDfC9MBstK6W6QWSQvYyeNn8va2fai:identity.example.com";
async fn fixture_resolver(doc: Value) -> affinidi_did_resolver_cache_sdk::DIDCacheClient {
let mut resolver = affinidi_did_resolver_cache_sdk::DIDCacheClient::new(
vta_sdk::resolver::build_did_cache_config(None),
)
.await
.unwrap();
resolver
.add_did_document(VTA_DID, serde_json::from_value(doc).unwrap())
.await;
resolver
}
#[tokio::test]
async fn connect_resolves_advertised_rest_host_port_and_path() {
let resolver = fixture_resolver(serde_json::json!({
"id": VTA_DID,
"service": [
{"id": format!("{VTA_DID}#tsp"), "type": "TSPTransport", "serviceEndpoint": "did:example:mediator"},
{"id": format!("{VTA_DID}#custom-rest"), "type": "VTARest", "serviceEndpoint": "https://api.example.com:8443/vta/"}
]
})).await;
assert_eq!(
super::resolve_connect_url(VTA_DID, &resolver)
.await
.unwrap(),
"https://api.example.com:8443/vta"
);
}
#[tokio::test]
async fn connect_guards_did_advertised_rest_endpoints() {
use vta_sdk::http::EndpointPolicy;
for (url, public_allowed, private_allowed) in [
("https://api.example.com:8443/vta/", true, true),
("http://localhost:8100/vta/", true, true),
("http://127.0.0.1:8100/", true, true),
("http://[::1]:8100/", true, true),
("http://api.example.com/", false, false),
("http://10.0.0.5/", false, false),
("https://169.254.169.254/latest/meta-data/", false, false),
("https://[fe80::1]/", false, false),
("https://[fd00:ec2::254]/", false, false),
("https://metadata.google.internal/", false, false),
(
"https://operator:fixture-secret@api.example.com/",
false,
false,
),
("https://[::ffff:10.0.0.5]/", false, false),
("https://[::ffff:169.254.169.254]/", false, false),
("https://0.0.0.0/", false, false),
("https://192.0.2.1/", false, false),
("https://10.0.0.5:8443/vta/", false, true),
("https://192.168.1.10/", false, true),
("https://[fd00::1]/", false, true),
("https://100.64.0.1/", false, true),
("https://vta.internal/", false, true),
("file:///tmp/vta", false, false),
] {
let resolver = fixture_resolver(serde_json::json!({
"id": VTA_DID,
"service": [{
"id": format!("{VTA_DID}#custom-rest"),
"type": "VTARest",
"serviceEndpoint": url,
}],
}))
.await;
for (policy, allowed) in [
(EndpointPolicy::public_only(), public_allowed),
(EndpointPolicy::private_allowed(), private_allowed),
] {
let result =
super::resolve_connect_url_with_policy(VTA_DID, &resolver, policy).await;
if allowed {
assert_eq!(result.unwrap(), url.trim_end_matches('/'));
} else {
let error = result
.expect_err("unsafe DID endpoint must not fall back to its host")
.to_string();
assert!(error.contains(VTA_DID), "{error}");
assert!(!error.contains("fixture-secret"), "{error}");
assert!(!error.contains("--vta-url"), "{error}");
if private_allowed {
assert!(error.contains("VTA_ALLOW_PRIVATE_ENDPOINTS"), "{error}");
assert!(error.contains("--allow-private-endpoints"), "{error}");
}
}
}
}
}
#[tokio::test]
async fn connect_refuses_missing_rest_or_wrong_document_instead_of_guessing_url() {
for doc in [
serde_json::json!({"id": VTA_DID}),
serde_json::json!({"id": "did:web:other.example.com", "service": [
{"id": "did:web:other.example.com#rest", "type": "VTARest", "serviceEndpoint": "https://api.example.com"}
]}),
] {
let resolver = fixture_resolver(doc).await;
let error = super::resolve_connect_url(VTA_DID, &resolver)
.await
.unwrap_err()
.to_string();
assert!(error.contains(VTA_DID));
assert!(error.contains("PNM_RESOLVER_URL"), "{error}");
assert!(error.contains("--vta-url"));
}
}
#[tokio::test]
async fn connect_resolution_failure_names_did_and_explicit_fallback() {
let resolver = fixture_resolver(serde_json::json!({"id": VTA_DID})).await;
let did = "did:unsupported:bootstrap-target";
let error = super::resolve_connect_url(did, &resolver)
.await
.unwrap_err()
.to_string();
assert!(error.contains(did));
assert!(error.contains("--vta-url"));
}
#[test]
fn connect_pins_credential_identity_but_preserves_url_fallback() {
assert!(super::check_connect_vta_did(Some(VTA_DID), VTA_DID).is_ok());
assert!(super::check_connect_vta_did(None, VTA_DID).is_ok());
assert!(super::check_connect_vta_did(Some(VTA_DID), "did:web:other.example.com").is_err());
}
#[test]
fn connect_anchor_flag_matrix() {
let valid = "ab12".repeat(24);
for digest in [None, Some("digest")] {
for opt_out in [false, true] {
for pcr0 in [None, Some(valid.as_str())] {
for pcr8 in [None, Some(valid.as_str())] {
let expected = !(digest.is_some() && opt_out)
&& (digest.is_some() || opt_out || pcr0.is_some());
assert_eq!(
super::validate_connect_anchor(digest, opt_out, pcr0, pcr8).is_ok(),
expected,
"digest={digest:?}, opt_out={opt_out}, pcr0={pcr0:?}, pcr8={pcr8:?}",
);
}
}
}
}
}
#[test]
fn connect_accepts_valid_normalized_pcr0_as_sole_anchor() {
let pcr0 = "ab12".repeat(24);
for value in [
pcr0.clone(),
pcr0.to_uppercase(),
format!("0x{pcr0}"),
format!(" \t0X{}\n ", "AB12 \t".repeat(24)),
] {
super::validate_connect_anchor(None, false, Some(&value), None).unwrap();
super::validate_connect_anchor(None, false, Some(&value), Some(&value)).unwrap();
}
}
#[tokio::test]
async fn connect_rejects_malformed_pcrs_before_resolving_or_posting() {
let valid = "ab12".repeat(24);
for value in [
"".to_string(),
" ".to_string(),
"0x".to_string(),
" \t0X\n".to_string(),
"abcd".to_string(),
"a".repeat(95),
"a".repeat(97),
format!("{}g", "a".repeat(95)),
] {
for (which, pcr0, pcr8) in [
(0u8, Some(value.clone()), None),
(0, Some(value.clone()), Some(valid.clone())),
(8, Some(valid.clone()), Some(value.clone())),
(8, None, Some(value.clone())),
] {
for (digest, opt_out) in [(None, false), (Some("digest"), false), (None, true)] {
for by_did in [true, false] {
let mut config = crate::config::PnmConfig::default();
let error = super::run_connect(
by_did.then(|| "did:unsupported:bootstrap-target".into()),
(!by_did).then(|| "not-a-url".into()),
digest.map(str::to_string),
opt_out,
pcr0.clone(),
pcr8.clone(),
None,
&mut config,
)
.await
.unwrap_err();
assert!(
matches!(
error.downcast_ref::<vta_sdk::attestation::ConfigAttestationVerifyError>(),
Some(vta_sdk::attestation::ConfigAttestationVerifyError::InvalidExpectedPcr { which: actual, .. })
if *actual == which
),
"which={which}, value={value:?}, error={error}"
);
assert!(config.vtas.is_empty());
assert!(config.default_vta.is_none());
}
}
}
}
}
#[test]
fn connect_missing_anchor_explains_all_routes() {
for pcr8 in [None, Some("ab12".repeat(24))] {
let error = super::validate_connect_anchor(None, false, None, pcr8.as_deref())
.unwrap_err()
.to_string();
for route in ["--expect-pcr0", "--expect-digest", "--no-verify-digest"] {
assert!(error.contains(route), "{error}");
}
assert!(error.contains("generated server-side"), "{error}");
}
assert!(vta_cli_common::sealed_consumer::validate_digest_flags(None, false).is_err());
}
#[test]
fn parse_var_plain_string() {
let (k, v) = parse_var("URL=https://mediator.example.com").unwrap();
assert_eq!(k, "URL");
assert_eq!(v, Value::String("https://mediator.example.com".into()));
}
#[test]
fn parse_var_json_types_round_trip() {
assert_eq!(parse_var("N=42").unwrap().1, Value::Number(42.into()));
assert_eq!(parse_var("B=true").unwrap().1, Value::Bool(true));
assert!(parse_var(r#"A=[1,2]"#).unwrap().1.is_array());
}
#[test]
fn parse_var_value_may_contain_equals() {
let (_, v) = parse_var("URL=https://m.example.com?x=1").unwrap();
assert_eq!(v.as_str(), Some("https://m.example.com?x=1"));
}
#[test]
fn parse_var_missing_equals_errors() {
assert!(parse_var("LONELY").is_err());
}
#[test]
fn parse_var_empty_key_errors() {
assert!(parse_var("=value").is_err());
}
fn scratch_path(name: &str) -> std::path::PathBuf {
let mut p = std::env::temp_dir();
p.push(format!("pnm-open-out-{name}-{}.json", std::process::id()));
p
}
fn sample_bundle() -> vta_sdk::credentials::CredentialBundle {
vta_sdk::credentials::CredentialBundle {
did: "did:key:z6MkTest".into(),
private_key_multibase: "z3SecretTest".into(),
vta_did: "did:webvh:QmTest:vta.example.com:vta".into(),
vta_url: Some("https://vta.example.com".into()),
}
}
#[test]
fn written_credential_uses_wire_field_names() {
let path = scratch_path("fields");
super::write_credential_bundle(&path, &sample_bundle()).unwrap();
let v: Value = serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap();
assert_eq!(v["did"], "did:key:z6MkTest");
assert_eq!(v["privateKeyMultibase"], "z3SecretTest");
assert_eq!(v["vtaDid"], "did:webvh:QmTest:vta.example.com:vta");
assert_eq!(v["vtaUrl"], "https://vta.example.com");
std::fs::remove_file(&path).ok();
}
#[test]
fn written_credential_omits_absent_vta_url() {
let path = scratch_path("nourl");
let mut bundle = sample_bundle();
bundle.vta_url = None;
super::write_credential_bundle(&path, &bundle).unwrap();
let v: Value = serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap();
assert!(v.get("vtaUrl").is_none());
std::fs::remove_file(&path).ok();
}
#[cfg(unix)]
#[test]
fn written_credential_is_owner_only() {
use std::os::unix::fs::PermissionsExt;
let path = scratch_path("perms");
super::write_credential_bundle(&path, &sample_bundle()).unwrap();
let mode = std::fs::metadata(&path).unwrap().permissions().mode();
assert_eq!(
mode & 0o777,
0o600,
"credential file must not be group/world readable"
);
std::fs::remove_file(&path).ok();
}
#[cfg(unix)]
#[test]
fn written_credential_truncates_existing_file() {
let path = scratch_path("truncate");
std::fs::write(&path, vec![b'x'; 4096]).unwrap();
super::write_credential_bundle(&path, &sample_bundle()).unwrap();
let v: Value = serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap();
assert_eq!(v["did"], "did:key:z6MkTest");
std::fs::remove_file(&path).ok();
}
}