Attestation Engine (Rust)
The attestation engine verifies the integrity of CPS/IoT firmware using ECDSA P-256 digital signatures. Every edge device periodically signs a payload proving its firmware hasn't been tampered with.
Why Hardware Attestation?
In a cyber-physical environment, a compromised firmware can cause physical harm. Unlike IT assets where you can simply reimage, a tampered PLC controlling a water treatment plant could poison the water supply.
Attestation provides mathematical proof that the firmware running on a device matches the known-good version.
Attestation Flow
Request/Response Format
Request
#[derive(Deserialize)]
pub struct AttestationVerifyRequest {
pub device_id: String,
pub firmware_hash: String, // SHA-256 hex of firmware binary
pub boot_count: u64, // Monotonic counter (never decreases)
pub signature_hex: String, // ECDSA P-256 signature in hex
pub public_key_hex: Option<String>, // Optional: for initial enrollment
pub nonce: Option<String>, // Challenge-response nonce
}
Response
#[derive(Serialize)]
pub struct AttestationVerifyResponse {
pub valid: bool,
pub device_id: String,
pub status: String, // "valid" | "failed" | "structural_pass"
pub reason: String, // Detailed explanation
}
Verification Process
Step 1: Message Construction
// The signed message is a concatenation of identity fields
let message = format!(
"{}{}{}{}",
req.device_id,
req.firmware_hash,
req.boot_count,
req.nonce.as_deref().unwrap_or("")
);
Step 2: SHA-256 Digest
use sha2::{Sha256, Digest};
let digest = Sha256::digest(message.as_bytes());
Step 3: Key Resolution
// Priority order for public key:
// 1. Request-provided key (initial enrollment)
// 2. Static device registry (compile-time known devices)
// 3. HashiCorp Vault lookup (production)
let public_key = req.public_key_hex.as_deref()
.or_else(|| DEVICE_KEYS.get(req.device_id.as_str()).copied());
Step 4: ECDSA Verification
use p256::ecdsa::{signature::Verifier, Signature, VerifyingKey};
let sig_bytes = hex::decode(&req.signature_hex)?;
let signature = Signature::from_der(&sig_bytes)?;
let key_bytes = hex::decode(public_key)?;
let verifying_key = VerifyingKey::from_sec1_bytes(&key_bytes)?;
match verifying_key.verify(&digest, &signature) {
Ok(_) => {
// Signature valid — firmware is authentic
redis.publish_attestation(&req.device_id, "valid", "Signature verified").await;
Json(AttestationVerifyResponse {
valid: true,
status: "valid".into(),
reason: "ECDSA P-256 signature verified successfully".into(),
// ...
})
}
Err(e) => {
// Signature invalid — potential tampering!
redis.publish_attestation(&req.device_id, "failed", &e.to_string()).await;
Json(AttestationVerifyResponse {
valid: false,
status: "failed".into(),
reason: format!("Signature verification failed: {}", e),
// ...
})
}
}
Step 5: Structural Fallback
When no public key is available (development or unregistered devices), the engine performs structural validation:
// No key available — fallback to structural check
let structural_valid = !req.firmware_hash.is_empty()
&& req.firmware_hash.len() == 64
&& req.boot_count > 0;
if structural_valid {
// Accept with lower confidence
Json(AttestationVerifyResponse {
valid: true,
status: "structural_pass".into(),
reason: "No public key — structural validation only".into(),
})
}
Why ECDSA P-256?
| Algorithm | Key Size | Signature Size | Speed | Hardware Support |
|---|---|---|---|---|
| RSA-2048 | 256 bytes | 256 bytes | Slow | Limited |
| ECDSA P-256 | 32 bytes | 64 bytes | Fast | STM32 PKA, nRF CC310 |
| Ed25519 | 32 bytes | 64 bytes | Fastest | Rare on MCUs |
ECDSA P-256 is chosen because:
- Hardware acceleration — STM32's PKA and nRF52's CryptoCell CC310 both have P-256 hardware support
- Small signatures — 64 bytes fits in a single MQTT packet efficiently
- Standard compliance — NIST-approved, required by many industrial standards
- Key storage — 32-byte private keys fit in MCU OTP/UICR memory regions
Boot Count Protection
The monotonic boot count prevents replay attacks:
Attack scenario without boot_count:
1. Attacker captures valid attestation message
2. Replays it after installing malicious firmware
3. System accepts the stale (but valid) signature
With boot_count:
1. Each boot increments a hardware counter stored in OTP
2. Counter can never decrease (hardware fuse)
3. Replayed message has stale boot_count → rejected
Integration with Vault PKI
In production, device public keys are managed by HashiCorp Vault:
Vault configuration:
- PKI Engine: Issues ECDSA P-256 device certificates (1-year validity)
- Transit Engine: Stores device signing keys for verification
- Policy:
aurorasoc-devicesrole — EC P-256, client auth EKU
Redis Publishing
Attestation results are published to the aurora:cps:attestation stream:
pub async fn publish_attestation(
&self, device_id: &str, status: &str, reason: &str
) {
let mut conn = self.pool.get().await?;
redis::cmd("XADD")
.arg("aurora:cps:attestation")
.arg("MAXLEN").arg("~").arg("10000")
.arg("*")
.arg("device_id").arg(device_id)
.arg("status").arg(status)
.arg("reason").arg(reason)
.arg("timestamp").arg(chrono::Utc::now().to_rfc3339())
.query_async(&mut conn)
.await?;
}