انتقل إلى المحتوى الرئيسي

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?

AlgorithmKey SizeSignature SizeSpeedHardware Support
RSA-2048256 bytes256 bytesSlowLimited
ECDSA P-25632 bytes64 bytesFastSTM32 PKA, nRF CC310
Ed2551932 bytes64 bytesFastestRare on MCUs

ECDSA P-256 is chosen because:

  1. Hardware acceleration — STM32's PKA and nRF52's CryptoCell CC310 both have P-256 hardware support
  2. Small signatures — 64 bytes fits in a single MQTT packet efficiently
  3. Standard compliance — NIST-approved, required by many industrial standards
  4. 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-devices role — 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?;
}