feat: native step-ca signing for iLO cert provisioning

Replace the step CLI shell-out with direct step-ca API calls:
- Fetch provisioner config and encrypted JWK key from /1.0/provisioners
- Decrypt PBES2-HS256+A128KW JWE with AES key unwrap (RFC 3394)
- Create ES256 JWT one-time token with CSR subject, SANs, and SHA
- POST CSR + OTT to /1.0/sign

Also fixes CSR reuse to validate CN matches before reusing a pending
CSR, and auto-discovers iLO 4 (Hp) vs iLO 5 (Hpe) action URLs from
the HttpsCert endpoint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 17:03:41 +02:00
parent 2996cdea64
commit bc389dfc7c
5 changed files with 572 additions and 121 deletions

2
Cargo.lock generated
View File

@@ -437,6 +437,7 @@ dependencies = [
name = "cichlid"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
"clap",
"openssl",
"reqwest",
@@ -444,6 +445,7 @@ dependencies = [
"serde_json",
"tokio",
"tracing",
"uuid",
]
[[package]]

View File

@@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2024"
[dependencies]
base64 = "0.22"
clap = { workspace = true, features = ["derive"] }
openssl = { workspace = true }
reqwest = { workspace = true }
@@ -11,3 +12,4 @@ serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
tokio = { workspace = true, features = ["full"] }
tracing = { workspace = true }
uuid = { workspace = true, features = ["v4"] }

View File

@@ -2,10 +2,11 @@ use clap::Parser;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;
use tracing::{error, info};
use super::step_ca;
#[derive(Parser)]
pub struct Args {
/// iLO hostname or IP address (e.g. ilo-frootmig.kosherinata.internal or 10.3.119.10)
@@ -120,6 +121,8 @@ struct HttpsCertResponse {
certificate_signing_request: Option<String>,
#[serde(rename = "X509CertificateInformation")]
x509_certificate_information: Option<CertInfo>,
#[serde(rename = "Actions")]
actions: Option<serde_json::Value>,
}
#[derive(Deserialize)]
@@ -133,6 +136,47 @@ struct CertInfo {
valid_not_after: String,
}
/// Discovered action URLs from the HttpsCert endpoint.
/// Supports both iLO 4 (Hp prefix) and iLO 5+ (Hpe prefix).
struct ActionUrls {
generate_csr: String,
import_cert: String,
}
fn discover_action_urls(actions: &serde_json::Value, base_url: &str) -> Option<ActionUrls> {
let actions_obj = actions.as_object()?;
let csr_target = actions_obj
.iter()
.find(|(k, _)| k.contains("GenerateCSR"))
.and_then(|(_, v)| v.get("target"))
.and_then(|t| t.as_str())?;
let import_target = actions_obj
.iter()
.find(|(k, _)| k.contains("ImportCertificate"))
.and_then(|(_, v)| v.get("target"))
.and_then(|t| t.as_str())?;
Some(ActionUrls {
generate_csr: format!("{}{}", base_url, csr_target),
import_cert: format!("{}{}", base_url, import_target),
})
}
/// Check if a PEM-encoded CSR contains the expected CN.
fn csr_has_cn(pem: &str, expected_cn: &str) -> bool {
let Ok(der) = openssl::x509::X509Req::from_pem(pem.as_bytes()) else {
return false;
};
let subject = der.subject_name();
subject
.entries_by_nid(openssl::nid::Nid::COMMONNAME)
.next()
.and_then(|e| e.data().as_utf8().ok())
.is_some_and(|cn| cn.to_string() == expected_cn)
}
fn build_client() -> Result<Client, Box<dyn std::error::Error>> {
Ok(Client::builder()
.danger_accept_invalid_certs(true)
@@ -178,122 +222,134 @@ pub async fn run(args: Args) -> Result<(), Box<dyn std::error::Error>> {
println!("Authenticated to {}", base_url);
// Step 2: Generate a CSR
let csr_action_url = format!(
"{}/redfish/v1/Managers/1/SecurityService/HttpsCert/Actions/HpHttpsCert.GenerateCSR/",
base_url
);
println!("Generating CSR for CN={}...", cn);
let csr_resp = client
.post(&csr_action_url)
.header("X-Auth-Token", &token)
.json(&CsrRequest {
action: "GenerateCSR".to_string(),
country: args.country,
state: args.state,
city: args.city,
org_name: args.org,
org_unit: args.org_unit,
common_name: cn.clone(),
include_ip: args.include_ip,
})
.send()
.await?;
if !csr_resp.status().is_success() {
let body = csr_resp.text().await.unwrap_or_default();
return Err(format!("CSR generation request failed: {}", body).into());
}
// Step 3: Poll for CSR
// Step 2: Discover action URLs from the HttpsCert endpoint
let cert_url = format!(
"{}/redfish/v1/Managers/1/SecurityService/HttpsCert/",
base_url
);
let poll_interval = Duration::from_secs(10);
let max_attempts = args.csr_timeout / 10;
let mut csr_pem = None;
for attempt in 1..=max_attempts {
tokio::time::sleep(poll_interval).await;
print!(" Polling for CSR (attempt {}/{})... ", attempt, max_attempts);
let discovery_resp = client
.get(&cert_url)
.header("X-Auth-Token", &token)
.send()
.await?;
let resp = client
.get(&cert_url)
let discovery_body = discovery_resp.text().await?;
let cert_data: HttpsCertResponse = serde_json::from_str(&discovery_body)
.map_err(|e| format!("Failed to parse HttpsCert endpoint: {}", e))?;
let action_urls = cert_data
.actions
.as_ref()
.and_then(|a| discover_action_urls(a, &base_url))
.ok_or("Could not discover GenerateCSR/ImportCertificate action URLs")?;
println!(
"Discovered actions: CSR={}, Import={}",
action_urls.generate_csr, action_urls.import_cert
);
// Check if a valid CSR is already pending with the correct CN
let mut csr_pem = cert_data
.certificate_signing_request
.filter(|csr| csr.contains("BEGIN CERTIFICATE REQUEST"))
.filter(|csr| csr_has_cn(csr, &cn));
if csr_pem.is_some() {
println!("Found existing pending CSR with matching CN, reusing it");
} else {
// Step 3: Generate a CSR
println!("Generating CSR for CN={}...", cn);
let csr_resp = client
.post(&action_urls.generate_csr)
.header("X-Auth-Token", &token)
.json(&CsrRequest {
action: "GenerateCSR".to_string(),
country: args.country,
state: args.state,
city: args.city,
org_name: args.org,
org_unit: args.org_unit,
common_name: cn.clone(),
include_ip: args.include_ip,
})
.send()
.await?;
let body = resp.text().await?;
// The CSR field may be absent, null, or a PEM string
if let Ok(cert_data) = serde_json::from_str::<HttpsCertResponse>(&body) {
if let Some(ref csr) = cert_data.certificate_signing_request {
if csr.contains("BEGIN CERTIFICATE REQUEST") {
println!("ready");
csr_pem = Some(csr.clone());
break;
if !csr_resp.status().is_success() {
let body = csr_resp.text().await.unwrap_or_default();
return Err(format!("CSR generation request failed: {}", body).into());
}
// Step 4: Poll for CSR
let poll_interval = Duration::from_secs(10);
let max_attempts = args.csr_timeout / 10;
for attempt in 1..=max_attempts {
tokio::time::sleep(poll_interval).await;
print!(" Polling for CSR (attempt {}/{})... ", attempt, max_attempts);
let resp = client
.get(&cert_url)
.header("X-Auth-Token", &token)
.send()
.await?;
let body = resp.text().await?;
if let Ok(data) = serde_json::from_str::<HttpsCertResponse>(&body) {
if let Some(ref csr) = data.certificate_signing_request {
if csr.contains("BEGIN CERTIFICATE REQUEST") {
println!("ready");
csr_pem = Some(csr.clone());
break;
}
}
}
println!("not ready");
}
println!("not ready");
}
let csr_pem = csr_pem.ok_or("Timed out waiting for CSR generation")?;
// Write CSR to temp file
let csr_path = std::env::temp_dir().join(format!("{}.csr", cn));
let cert_path = std::env::temp_dir().join(format!("{}.crt", cn));
std::fs::write(&csr_path, &csr_pem)?;
println!("CSR saved to {}", csr_path.display());
// Step 4: Sign with step-ca
// Step 5: Sign with step-ca natively
let password_file = if args.provisioner_password_file.starts_with("~/") {
let home = std::env::var("HOME").unwrap_or_default();
format!("{}/{}", home, &args.provisioner_password_file[2..])
} else {
args.provisioner_password_file.clone()
};
let password = std::fs::read_to_string(&password_file)
.map_err(|e| format!("Failed to read provisioner password: {}", e))?
.trim()
.to_string();
println!("Signing CSR with step-ca ({})...", args.ca_url);
let sign_output = Command::new("step")
.args([
"ca",
"sign",
"--provisioner",
&args.provisioner,
"--provisioner-password-file",
&password_file,
"--ca-url",
&args.ca_url,
"--root",
args.root_cert.to_str().unwrap(),
csr_path.to_str().unwrap(),
cert_path.to_str().unwrap(),
])
.output()?;
// Fetch provisioner key from CA
let (prov_info, encrypted_key) =
step_ca::fetch_provisioner_key(&client, &args.ca_url, &args.provisioner).await?;
println!(" Provisioner: {} (kid: {})", prov_info.name, prov_info.kid);
if !sign_output.status.success() {
let stderr = String::from_utf8_lossy(&sign_output.stderr);
return Err(format!("step ca sign failed: {}", stderr).into());
}
// Decrypt the provisioner private key
let private_key = step_ca::decrypt_provisioner_key(&encrypted_key, &password)?;
println!("Certificate signed: {}", cert_path.display());
// Create the one-time token
let ott = step_ca::create_sign_token(&private_key, &prov_info, &args.ca_url, &csr_pem)?;
// Step 5: Import cert (leaf + intermediate) to iLO
let leaf_pem = std::fs::read_to_string(&cert_path)?;
// Sign via the CA API
let leaf_pem = step_ca::sign_csr(&client, &args.ca_url, &csr_pem, &ott).await?;
println!("Certificate signed");
// Build full chain (leaf + intermediate)
let intermediate_pem = std::fs::read_to_string(&args.intermediate_cert)?;
let full_chain = format!("{}{}", leaf_pem, intermediate_pem);
let import_url = format!(
"{}/redfish/v1/Managers/1/SecurityService/HttpsCert/Actions/HpHttpsCert.ImportCertificate/",
base_url
);
println!("Importing certificate to iLO...");
let import_resp = client
.post(&import_url)
.post(&action_urls.import_cert)
.header("X-Auth-Token", &token)
.json(&ImportCertRequest {
action: "ImportCertificate".to_string(),
@@ -315,50 +371,31 @@ pub async fn run(args: Args) -> Result<(), Box<dyn std::error::Error>> {
println!("Import response: {}", import_body);
}
// Step 6: Wait for reset and verify
println!(
"Waiting {}s for iLO to restart...",
args.reset_wait
);
// Step 7: Wait for reset and verify
println!("Waiting {}s for iLO to restart...", args.reset_wait);
tokio::time::sleep(Duration::from_secs(args.reset_wait)).await;
// Verify the new cert
// Verify the new cert by connecting with openssl
println!("Verifying new certificate...");
let verify_output = Command::new("openssl")
.args([
"s_client",
"-connect",
&format!(
"{}:443",
args.host
.trim_start_matches("https://")
.trim_start_matches("http://")
),
"-noservername",
])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output();
let host_addr = args
.host
.trim_start_matches("https://")
.trim_start_matches("http://");
match verify_output {
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.contains(&cn) {
println!("Verified: iLO is now presenting cert for {}", cn);
} else {
println!("Warning: could not verify new cert (iLO may still be restarting)");
}
}
Err(_) => {
println!("Warning: could not connect to verify (iLO may still be restarting)");
}
let verify_client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(15))
.build()?;
match verify_client
.get(format!("https://{}:{}", host_addr, 443))
.send()
.await
{
Ok(_) => println!("Verified: iLO is back online with new cert for {}", cn),
Err(_) => println!("Warning: could not reach iLO (may still be restarting)"),
}
// Clean up temp files
std::fs::remove_file(&csr_path).ok();
std::fs::remove_file(&cert_path).ok();
println!("Done.");
Ok(())
}

View File

@@ -1 +1,2 @@
pub mod ilo_cert;
pub mod step_ca;

View File

@@ -0,0 +1,409 @@
//! Native step-ca CSR signing via the /1.0/sign API.
//!
//! Replaces the `step ca sign` CLI shell-out with direct HTTP calls:
//! 1. Fetch provisioner config (encrypted JWK key) from /1.0/provisioners
//! 2. Decrypt the JWE-encrypted private key with the provisioner password
//! 3. Create an ES256 JWT (one-time token) with CSR claims
//! 4. POST {csr, ott} to /1.0/sign
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use openssl::bn::BigNum;
use openssl::ec::{EcGroup, EcKey};
use openssl::ecdsa::EcdsaSig;
use openssl::hash::MessageDigest;
use openssl::nid::Nid;
use openssl::pkey::PKey;
use openssl::sign::Signer;
use openssl::symm::Cipher;
use reqwest::Client;
use serde::{Deserialize, Serialize};
/// Fetch the encrypted provisioner key from the CA.
pub async fn fetch_provisioner_key(
client: &Client,
ca_url: &str,
provisioner_name: &str,
) -> Result<(ProvisionerInfo, String), Box<dyn std::error::Error>> {
let url = format!("{}/1.0/provisioners", ca_url);
let resp: ProvisionersResponse = client.get(&url).send().await?.json().await?;
let prov = resp
.provisioners
.iter()
.find(|p| p.name == provisioner_name)
.ok_or_else(|| format!("Provisioner '{}' not found", provisioner_name))?;
let encrypted_key = prov
.encrypted_key
.as_ref()
.ok_or("Provisioner has no encrypted key")?
.clone();
let kid = prov
.key
.as_ref()
.ok_or("Provisioner has no public key")?
.kid
.clone();
let info = ProvisionerInfo {
name: prov.name.clone(),
kid,
};
Ok((info, encrypted_key))
}
/// Decrypt a JWE-encrypted provisioner key using PBES2-HS256+A128KW + A256GCM.
pub fn decrypt_provisioner_key(
jwe: &str,
password: &str,
) -> Result<EcPrivateKey, Box<dyn std::error::Error>> {
let parts: Vec<&str> = jwe.split('.').collect();
if parts.len() != 5 {
return Err(format!("Invalid JWE: expected 5 parts, got {}", parts.len()).into());
}
let header_b64 = parts[0];
let encrypted_key_b64 = parts[1];
let iv_b64 = parts[2];
let ciphertext_b64 = parts[3];
let tag_b64 = parts[4];
// Parse header
let header_json = URL_SAFE_NO_PAD.decode(header_b64)?;
let header: JweHeader = serde_json::from_slice(&header_json)?;
if header.alg != "PBES2-HS256+A128KW" {
return Err(format!("Unsupported JWE alg: {}", header.alg).into());
}
if header.enc != "A256GCM" {
return Err(format!("Unsupported JWE enc: {}", header.enc).into());
}
let p2s = URL_SAFE_NO_PAD.decode(&header.p2s)?;
let p2c = header.p2c;
// Derive the key-encryption key (KEK) via PBKDF2
// PBES2-HS256+A128KW uses SHA-256 and produces a 16-byte key
let salt_input = [b"PBES2-HS256+A128KW\0" as &[u8], &p2s].concat();
let mut kek = vec![0u8; 16];
openssl::pkcs5::pbkdf2_hmac(
password.as_bytes(),
&salt_input,
p2c as usize,
MessageDigest::sha256(),
&mut kek,
)?;
// Unwrap the content encryption key (CEK) using AES-128-KW (Key Wrap)
let wrapped_cek = URL_SAFE_NO_PAD.decode(encrypted_key_b64)?;
let cek = aes_key_unwrap(&kek, &wrapped_cek)?;
// Decrypt the content using AES-256-GCM
let iv = URL_SAFE_NO_PAD.decode(iv_b64)?;
let ciphertext = URL_SAFE_NO_PAD.decode(ciphertext_b64)?;
let tag = URL_SAFE_NO_PAD.decode(tag_b64)?;
// AAD is the JWE protected header (base64url-encoded)
let aad = header_b64.as_bytes();
let mut crypter = openssl::symm::Crypter::new(
Cipher::aes_256_gcm(),
openssl::symm::Mode::Decrypt,
&cek,
Some(&iv),
)?;
crypter.aad_update(aad)?;
let mut plaintext = vec![0u8; ciphertext.len() + 16];
let mut count = crypter.update(&ciphertext, &mut plaintext)?;
crypter.set_tag(&tag)?;
count += crypter.finalize(&mut plaintext[count..])?;
plaintext.truncate(count);
let jwk: EcPrivateKey = serde_json::from_slice(&plaintext)?;
Ok(jwk)
}
/// Create a signed JWT (one-time token) for the /1.0/sign endpoint.
pub fn create_sign_token(
private_key: &EcPrivateKey,
provisioner: &ProvisionerInfo,
ca_url: &str,
csr_pem: &str,
) -> Result<String, Box<dyn std::error::Error>> {
// Parse the CSR to extract CN and SANs
let csr = openssl::x509::X509Req::from_pem(csr_pem.as_bytes())?;
let subject = csr.subject_name();
let cn = subject
.entries_by_nid(Nid::COMMONNAME)
.next()
.and_then(|e| e.data().as_utf8().ok())
.map(|s| s.to_string())
.unwrap_or_default();
// Extract SANs from CSR extensions
let sans = extract_csr_sans(&csr);
// SHA256 fingerprint of the CSR DER
let csr_der = csr.to_der()?;
let sha = openssl::hash::hash(MessageDigest::sha256(), &csr_der)?;
let sha_hex = sha
.iter()
.map(|b| format!("{:02x}", b))
.collect::<String>();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_secs();
let header = serde_json::json!({
"alg": "ES256",
"typ": "JWT",
"kid": provisioner.kid,
});
let claims = serde_json::json!({
"sub": cn,
"sans": sans,
"sha": sha_hex,
"iss": provisioner.name,
"aud": format!("{}/1.0/sign", ca_url),
"exp": now + 300,
"iat": now,
"nbf": now,
"jti": uuid::Uuid::new_v4().to_string(),
});
let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header)?);
let claims_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&claims)?);
let signing_input = format!("{}.{}", header_b64, claims_b64);
// Build EC key from JWK
let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1)?;
let x = BigNum::from_slice(&URL_SAFE_NO_PAD.decode(&private_key.x)?)?;
let y = BigNum::from_slice(&URL_SAFE_NO_PAD.decode(&private_key.y)?)?;
let d = BigNum::from_slice(
&URL_SAFE_NO_PAD.decode(private_key.d.as_ref().ok_or("Missing private key 'd'")?)?,
)?;
let mut ctx = openssl::bn::BigNumContext::new()?;
let mut pub_key = openssl::ec::EcPoint::new(&group)?;
pub_key.set_affine_coordinates_gfp(&group, &x, &y, &mut ctx)?;
let ec_key = EcKey::from_private_components(&group, &d, &pub_key)?;
let pkey = PKey::from_ec_key(ec_key)?;
// Sign with ES256 (ECDSA with SHA-256)
let mut signer = Signer::new(MessageDigest::sha256(), &pkey)?;
signer.update(signing_input.as_bytes())?;
let der_sig = signer.sign_to_vec()?;
// Convert DER signature to JWS format (r || s, each 32 bytes)
let ecdsa_sig = EcdsaSig::from_der(&der_sig)?;
let r = ecdsa_sig.r().to_vec_padded(32)?;
let s = ecdsa_sig.s().to_vec_padded(32)?;
let mut jws_sig = Vec::with_capacity(64);
jws_sig.extend_from_slice(&r);
jws_sig.extend_from_slice(&s);
let sig_b64 = URL_SAFE_NO_PAD.encode(&jws_sig);
Ok(format!("{}.{}", signing_input, sig_b64))
}
/// Sign a CSR via the step-ca /1.0/sign API. Returns the signed certificate PEM.
pub async fn sign_csr(
client: &Client,
ca_url: &str,
csr_pem: &str,
ott: &str,
) -> Result<String, Box<dyn std::error::Error>> {
let url = format!("{}/1.0/sign", ca_url);
let resp = client
.post(&url)
.json(&SignRequest {
csr: csr_pem.to_string(),
ott: ott.to_string(),
})
.send()
.await?;
if !resp.status().is_success() {
let body = resp.text().await.unwrap_or_default();
return Err(format!("step-ca /1.0/sign failed: {}", body).into());
}
let sign_resp: SignResponse = resp.json().await?;
Ok(sign_resp.crt)
}
fn extract_csr_sans(csr: &openssl::x509::X509Req) -> Vec<String> {
let mut sans = Vec::new();
// Add CN
if let Some(cn) = csr
.subject_name()
.entries_by_nid(Nid::COMMONNAME)
.next()
.and_then(|e| e.data().as_utf8().ok())
{
sans.push(cn.to_string());
}
// Extract SANs by printing the CSR to text and parsing the output.
// This is simpler than dealing with the low-level extension API.
let csr_pem = match csr.to_pem() {
Ok(pem) => pem,
Err(_) => return sans,
};
let output = std::process::Command::new("openssl")
.args(["req", "-noout", "-text"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn()
.and_then(|mut child| {
use std::io::Write;
if let Some(ref mut stdin) = child.stdin {
stdin.write_all(&csr_pem).ok();
}
child.wait_with_output()
});
if let Ok(output) = output {
let text = String::from_utf8_lossy(&output.stdout);
// Look for "DNS:" and "IP Address:" in the SAN line
for line in text.lines() {
let trimmed = line.trim();
if trimmed.contains("DNS:") || trimmed.contains("IP Address:") {
for part in trimmed.split(',') {
let part = part.trim();
if let Some(dns) = part.strip_prefix("DNS:") {
let dns = dns.trim().to_string();
if !sans.contains(&dns) {
sans.push(dns);
}
} else if let Some(ip) = part.strip_prefix("IP Address:") {
let ip = ip.trim().to_string();
if !sans.contains(&ip) {
sans.push(ip);
}
}
}
}
}
}
sans
}
/// AES Key Unwrap (RFC 3394)
fn aes_key_unwrap(kek: &[u8], wrapped: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
if wrapped.len() < 24 || wrapped.len() % 8 != 0 {
return Err("Invalid wrapped key length".into());
}
let n = (wrapped.len() / 8) - 1;
let mut a = [0u8; 8];
a.copy_from_slice(&wrapped[0..8]);
let mut r: Vec<[u8; 8]> = Vec::with_capacity(n);
for i in 0..n {
let mut block = [0u8; 8];
block.copy_from_slice(&wrapped[(i + 1) * 8..(i + 2) * 8]);
r.push(block);
}
let cipher = openssl::symm::Cipher::aes_128_ecb();
for j in (0..=5).rev() {
for i in (0..n).rev() {
let t = ((n * j) + i + 1) as u64;
let t_bytes = t.to_be_bytes();
let mut a_xor = [0u8; 8];
for k in 0..8 {
a_xor[k] = a[k] ^ t_bytes[k];
}
let mut input = [0u8; 16];
input[0..8].copy_from_slice(&a_xor);
input[8..16].copy_from_slice(&r[i]);
// Use a Crypter to disable padding (RFC 3394 operates on exact blocks)
let mut crypter =
openssl::symm::Crypter::new(cipher, openssl::symm::Mode::Decrypt, kek, None)?;
crypter.pad(false);
let mut decrypted = vec![0u8; 32];
let count = crypter.update(&input, &mut decrypted)?;
let rest = crypter.finalize(&mut decrypted[count..])?;
let total = count + rest;
a.copy_from_slice(&decrypted[0..8]);
r[i].copy_from_slice(&decrypted[8..std::cmp::min(16, total + 8)]);
}
}
// Verify IV
let default_iv: [u8; 8] = [0xA6; 8];
if a != default_iv {
return Err("AES Key Unwrap: integrity check failed (wrong password?)".into());
}
let mut result = Vec::with_capacity(n * 8);
for block in r {
result.extend_from_slice(&block);
}
Ok(result)
}
// --- Types ---
pub struct ProvisionerInfo {
pub name: String,
pub kid: String,
}
#[derive(Deserialize)]
struct ProvisionersResponse {
provisioners: Vec<Provisioner>,
}
#[derive(Deserialize)]
struct Provisioner {
name: String,
key: Option<ProvisionerPublicKey>,
#[serde(rename = "encryptedKey")]
encrypted_key: Option<String>,
}
#[derive(Deserialize)]
struct ProvisionerPublicKey {
kid: String,
}
#[derive(Deserialize)]
struct JweHeader {
alg: String,
enc: String,
p2c: u64,
p2s: String,
}
#[derive(Deserialize)]
pub struct EcPrivateKey {
pub x: String,
pub y: String,
pub d: Option<String>,
}
#[derive(Serialize)]
struct SignRequest {
csr: String,
ott: String,
}
#[derive(Deserialize)]
struct SignResponse {
crt: String,
}