#![cfg(unix)]
use openrtc::native::{load_or_create_endpoint_key, FileSigner};
use openrtc::offline::{
DurableOfflineTrustState, OfflineAssurance, OfflineDeviceCredential, OfflineEnrollmentRequest,
OfflineSigner, OfflineTrustBundle,
};
use std::{fs, path::PathBuf, process::Command};
const DEVICE_DIRECTORY: &str = "OPENRTC_OFFLINE_PROVISION_FIXTURE";
const ISSUER_PIN: &str = "OPENRTC_OFFLINE_PROVISION_PUBLIC_PIN";
#[test]
fn signed_files_provision_a_restart_safe_device() {
if let Some(directory) = std::env::var_os(DEVICE_DIRECTORY) {
let directory = PathBuf::from(directory);
let signer = FileSigner::load_or_create(directory.join("proof.key")).unwrap();
let endpoint = load_or_create_endpoint_key(directory.join("iroh.key")).unwrap();
let request = OfflineEnrollmentRequest::create(
&signer,
"field-a",
"device-a",
&endpoint.public().to_string(),
&uuid::Uuid::new_v4().to_string(),
vec!["sensor".into()],
OfflineAssurance::Software,
1_000,
)
.unwrap();
let bundle_file = directory.join("bundle.json");
if bundle_file.exists() {
let pin: [u8; 32] = hex::decode(std::env::var(ISSUER_PIN).unwrap())
.unwrap()
.try_into()
.unwrap();
let issuer = ed25519_dalek::VerifyingKey::from_bytes(&pin).unwrap();
let bundle: OfflineTrustBundle =
serde_json::from_slice(&fs::read(bundle_file).unwrap()).unwrap();
let mut trust = DurableOfflineTrustState::open(
directory.join("trust.json"),
"field-a",
issuer,
[],
2_000,
)
.unwrap();
if trust
.trust()
.high_water()
.unwrap()
.map(|water| water.digest)
!= Some(bundle.digest().unwrap())
{
trust.apply(bundle, 2_000).unwrap();
}
let credential = trust.trust().credential("device-a", 2_000).unwrap();
assert_eq!(credential.body.endpoint_id, endpoint.public().to_string());
assert_eq!(
credential.verify(&issuer, 2_000).unwrap(),
signer.verifying_key().unwrap()
);
}
println!(
"OPENRTC_ENROLLMENT {}",
serde_json::to_string(&request).unwrap()
);
return;
}
struct Fixture(PathBuf);
impl Drop for Fixture {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
let fixture =
Fixture(std::env::temp_dir().join(format!("openrtc-provision-{}", uuid::Uuid::new_v4())));
let device = fixture.0.join("device");
let issuer =
FileSigner::load_or_create(fixture.0.join("authority").join("issuer.key")).unwrap();
let spawn_device = || {
Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"signed_files_provision_a_restart_safe_device",
"--nocapture",
])
.env_clear()
.env(DEVICE_DIRECTORY, &device)
.env(
ISSUER_PIN,
hex::encode(issuer.verifying_key().unwrap().to_bytes()),
)
.output()
.unwrap()
};
let request_from = |output: std::process::Output| -> OfflineEnrollmentRequest {
assert!(
output.status.success(),
"device failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).unwrap();
let json = stdout
.lines()
.find_map(|line| line.split_once("OPENRTC_ENROLLMENT ").map(|(_, json)| json))
.unwrap();
serde_json::from_str(json).unwrap()
};
let first = request_from(spawn_device());
first.verify().unwrap();
let credential = OfflineDeviceCredential::issue(
&issuer,
&first,
"serial-a",
1,
vec!["sensor".into()],
OfflineAssurance::Software,
1_000,
10_000,
)
.unwrap();
let bundle =
OfflineTrustBundle::issue(&issuer, "field-a", 1, None, None, vec![credential], vec![])
.unwrap();
let bytes = serde_json::to_vec(&bundle).unwrap();
fs::write(device.join("bundle.json"), &bytes).unwrap();
for _ in 0..2 {
let restarted = request_from(spawn_device());
assert_eq!(restarted.body.endpoint_id, first.body.endpoint_id);
assert_eq!(restarted.body.proof_public_key, first.body.proof_public_key);
assert_ne!(restarted.body.enrollment_nonce, first.body.enrollment_nonce);
}
let accepted_journal = fs::read(device.join("trust.json")).unwrap();
let mut forged = bundle;
forged.body.credentials[0].body.roles = vec!["admin".into()];
fs::write(
device.join("bundle.json"),
serde_json::to_vec(&forged).unwrap(),
)
.unwrap();
assert!(
!spawn_device().status.success(),
"unsigned role escalation must fail"
);
assert_eq!(
fs::read(device.join("trust.json")).unwrap(),
accepted_journal
);
fs::write(device.join("bundle.json"), bytes).unwrap();
assert_eq!(
request_from(spawn_device()).body.proof_public_key,
first.body.proof_public_key
);
}