blob: 2d2c00fe4a9bfd7b8b46c4af69f910380c60417d [file] [view] [edit]
# Crypto Service Refactor — Implementation Plan
**Date:** February 14, 2026
**Status:** Approved
**Reference:** [crypto-service-design-review.md](crypto-service-design-review.md)
---
## Overview
This plan implements the proposed architecture from the design review:
- Algorithm marker types with compile-time dispatch
- `OneShot<A>` trait for backend abstraction
- Generic `CryptoServer<B>` that works with any backend
- Streaming sessions via `Streaming<A>` trait
---
## Design Decision: Why Not Reuse `RustCryptoController`?
The existing `platform/impls/rustcrypto/src/controller.rs` (640 lines) implements
the `openprot-hal-blocking` traits. We considered reusing it but chose **not to**
for long-term maintainability:
### Current Architecture (God Trait)
```rust
// hal/blocking/src/digest.rs
pub trait DigestInit<A> {
type Context: DigestOp;
fn init(self, algo: A) -> Result<Self::Context, Self::Error>;
}
// platform/impls/rustcrypto
impl DigestInit<Sha2_256> for RustCryptoController { ... }
impl DigestInit<Sha2_384> for RustCryptoController { ... }
impl DigestInit<Sha2_512> for RustCryptoController { ... }
impl DigestOp for DigestContext256 { ... }
impl DigestOp for DigestContext384 { ... }
impl DigestOp for DigestContext512 { ... }
// 6 impls just for digest, 6 more for MAC = 12+ total
```
**Problem:** Adding SHA3-384 requires:
1. Modify `DigestInit` trait (or add new marker type)
2. Add `DigestContext3_384` in RustCryptoController
3. Add impl for HACE backend
4. Add impl for any test mock
### Proposed Architecture (Composable Traits)
```rust
// crypto-traits/src/lib.rs
pub trait OneShot<A: Algorithm> {
fn compute(&self, input: &CryptoInput, output: &mut [u8]) -> Result<usize, CryptoError>;
}
// backend-rustcrypto/src/lib.rs
impl OneShot<Sha256> for RustCryptoBackend { ... }
impl OneShot<Sha384> for RustCryptoBackend { ... }
impl OneShot<Sha3_384> for RustCryptoBackend { ... } // ← Adding algorithm = adding impl
// backend-hace/src/lib.rs (HACE only supports SHA-2)
impl OneShot<Sha256> for HaceBackend { ... }
impl OneShot<Sha384> for HaceBackend { ... }
// No Sha3_384 impl needed if HACE doesn't support it
```
**Benefit:** Adding SHA3-384 requires:
1. Add `pub struct Sha3_384; impl Algorithm for Sha3_384 { ... }` to traits
2. Add `impl OneShot<Sha3_384> for RustCryptoBackend { ... }` to backend
3. Done — no other files touched
### Impact on Algorithm Roadmap
The SPDM spec requires 10-15 additional algorithms. Each algorithm addition:
| Approach | Files Changed | Risk |
|----------|--------------|------|
| Current God trait | 3-4 per algorithm | High (touches shared traits) |
| Composable traits | 1-2 per algorithm | Low (additive only) |
**Decision:** Take the upfront refactor cost now for clean extensibility later.
The existing `RustCryptoController` code will be referenced for implementation
details but not directly reused.
---
## Architectural Insight: Trait Layering (HAL vs Service)
During implementation review, we identified an important layering distinction:
```
┌─────────────────────────────────────────────────────────────────┐
│ Crypto Client │
│ (services/crypto/client) │
└───────────────────────────┬─────────────────────────────────────┘
│ IPC (channel_call)
┌───────────────────────────▼─────────────────────────────────────┐
│ Crypto Server │
│ (services/crypto/server) │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Service Layer Traits │ │
│ │ OneShot<A> — one-shot operations via &self │ │
│ │ Streaming<A> — session-based via session handle │ │
│ │ (services/crypto/api/src/backend.rs) │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ │ impl │
│ ▼ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ RustCryptoBackend│ │ HaceBackend │ ... (future) │
│ │ (software) │ │ (hardware accel) │ │
│ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │
└───────────┼─────────────────────┼───────────────────────────────┘
│ │
│ (may use) │ (uses)
▼ ▼
┌───────────────────────────────────────────────────────────────────┐
│ HAL Layer Traits │
│ owned::DigestInit — typestate pattern (moves self) │
│ owned::DigestOp — resource recovery via fn cancel(self) │
│ (hal/blocking/src/digest.rs) │
└───────────────────────────────────────────────────────────────────┘
│ impl
┌───────────────────────────────────────────────────────────────────┐
│ RustCryptoController │ HaceController │ ... (platform impls) │
│ (platform/impls/) │ │ │
└───────────────────────────────────────────────────────────────────┘
```
### Key Distinction
| Layer | Location | Purpose | Pattern |
|-------|----------|---------|----------|
| **Service** | `services/crypto/api/src/backend.rs` | Abstract IPC protocol | `&self` + session handle |
| **HAL** | `hal/blocking/src/digest.rs` | Abstract hardware controllers | Typestate (moves self) |
### Why Two Layers?
1. **Service Layer:** Designed for IPC semantics where state lives across multiple
requests. The server owns a `StreamingSession` and clients reference it via session ID.
2. **HAL Layer:** Designed for baremetal use where resource recovery is critical.
The typestate pattern ensures hardware controllers are properly released on
success, failure, or cancellation.
### Relationship
- The HAL layer is used **by** platform implementations (both baremetal and server backends)
- The Service layer is used **by** the crypto server to abstract over different backends
- A `HaceBackend` would wrap a `HaceController` (HAL) to implement `OneShot<A>` (Service)
---
## Architectural Insight: Decoupling Resource Recovery
The HAL traits in `hal/blocking/src/digest.rs` bundle two concerns:
1. **Operation semantics:** init/update/finalize flow
2. **Resource recovery:** typestate pattern ensuring controller is returned
```rust
// Current HAL: both concerns bundled
pub mod owned {
pub trait DigestOp {
type Output;
type Controller;
type Error;
fn update(self, data: &[u8]) -> Result<Self, Self::Error>;
fn finalize(self) -> Result<(Self::Output, Self::Controller), Self::Error>;
fn cancel(self) -> Self::Controller; // ← Resource recovery
}
}
```
### Why This Matters for Service Layer
For the crypto server:
- Sessions are **already managed** via session handles (IDs in a table)
- Resource recovery is handled at the **session table level**, not per-operation
- The typestate overhead provides no benefit for software backends
### Proposed Decoupling (Future Work)
Decouple into two orthogonal patterns:
```rust
// 1. Core operation trait (minimal, &mut self)
pub trait DigestOp {
type Output;
type Error;
fn update(&mut self, data: &[u8]) -> Result<(), Self::Error>;
fn finalize(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error>;
}
// 2. Optional wrapper for resource recovery (typestate)
pub struct Owned<T, C> {
op: T,
controller: C,
}
impl<T: DigestOp, C> Owned<T, C> {
pub fn update(self, data: &[u8]) -> Result<Self, (C, T::Error)> { ... }
pub fn finalize(self) -> Result<(T::Output, C), (C, T::Error)> { ... }
pub fn cancel(self) -> C { ... } // ← Resource recovery here
}
```
### Benefits
| Concern | Baremetal | Server |
|---------|-----------|--------|
| Operation semantics | Uses core `DigestOp` | Uses core `DigestOp` |
| Resource recovery | Wraps in `Owned<T, C>` | Not needed (session table) |
This decoupling is **not required** for the current refactor—the Service layer
traits are already designed without typestate. However, this insight informs
future HAL evolution.
---
## Threading Model Note
Pigweed userspace processes are **single-threaded event loops**. The crypto server:
- Handles requests **sequentially** within a single thread
- Cannot spawn multiple threads to handle concurrent requests
- For true parallelism, would need multiple separate server processes
This simplifies session management—no need for thread-safe session tables or locks.
The single-threaded model also means `&mut self` on `Streaming<A>` methods is safe
without additional synchronization.
---
## Phase 1: Create `crypto-traits` Crate
**Goal:** Define algorithm marker types and backend traits in a minimal, dependency-free crate.
### 1.1 Create directory structure
```
services/crypto/traits/
├── Cargo.toml
├── BUILD.bazel
└── src/
└── lib.rs
```
### 1.2 Create `Cargo.toml`
```toml
[package]
name = "crypto-traits"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0"
[dependencies]
# Intentionally minimal — no_std, no dependencies
```
### 1.3 Create `src/lib.rs`
Contents:
```rust
#![no_std]
//! Crypto traits for backend abstraction.
//!
//! This crate defines:
//! - Algorithm marker types (Sha256, HmacSha256, etc.)
//! - OneShot<A> trait for one-shot crypto operations
//! - Streaming<A> trait for session-based streaming
//! - CryptoInput enum for semantically typed inputs
// --- Algorithm trait ---
pub trait Algorithm {
const OUTPUT_SIZE: usize;
const OP_CODE: u8;
}
// --- Digest algorithms ---
pub struct Sha256;
impl Algorithm for Sha256 { const OUTPUT_SIZE: usize = 32; const OP_CODE: u8 = 0x01; }
pub struct Sha384;
impl Algorithm for Sha384 { const OUTPUT_SIZE: usize = 48; const OP_CODE: u8 = 0x02; }
pub struct Sha512;
impl Algorithm for Sha512 { const OUTPUT_SIZE: usize = 64; const OP_CODE: u8 = 0x03; }
// --- MAC algorithms ---
pub struct HmacSha256;
impl Algorithm for HmacSha256 { const OUTPUT_SIZE: usize = 32; const OP_CODE: u8 = 0x10; }
pub struct HmacSha384;
impl Algorithm for HmacSha384 { const OUTPUT_SIZE: usize = 48; const OP_CODE: u8 = 0x11; }
pub struct HmacSha512;
impl Algorithm for HmacSha512 { const OUTPUT_SIZE: usize = 64; const OP_CODE: u8 = 0x12; }
// --- AEAD algorithms ---
pub struct Aes256GcmEncrypt;
impl Algorithm for Aes256GcmEncrypt { const OUTPUT_SIZE: usize = 0; const OP_CODE: u8 = 0x20; }
pub struct Aes256GcmDecrypt;
impl Algorithm for Aes256GcmDecrypt { const OUTPUT_SIZE: usize = 0; const OP_CODE: u8 = 0x21; }
// --- Signature algorithms (gated) ---
#[cfg(feature = "ecdsa")]
pub struct EcdsaP256Sign;
#[cfg(feature = "ecdsa")]
impl Algorithm for EcdsaP256Sign { const OUTPUT_SIZE: usize = 64; const OP_CODE: u8 = 0x40; }
#[cfg(feature = "ecdsa")]
pub struct EcdsaP256Verify;
#[cfg(feature = "ecdsa")]
impl Algorithm for EcdsaP256Verify { const OUTPUT_SIZE: usize = 1; const OP_CODE: u8 = 0x41; }
#[cfg(feature = "ecdsa")]
pub struct EcdsaP384Sign;
#[cfg(feature = "ecdsa")]
impl Algorithm for EcdsaP384Sign { const OUTPUT_SIZE: usize = 96; const OP_CODE: u8 = 0x42; }
#[cfg(feature = "ecdsa")]
pub struct EcdsaP384Verify;
#[cfg(feature = "ecdsa")]
impl Algorithm for EcdsaP384Verify { const OUTPUT_SIZE: usize = 1; const OP_CODE: u8 = 0x43; }
// --- Structured input types ---
pub enum CryptoInput<'a> {
Digest { data: &'a [u8] },
Mac { key: &'a [u8], data: &'a [u8] },
Aead { key: &'a [u8], nonce: &'a [u8], data: &'a [u8] },
Sign { private_key: &'a [u8], message: &'a [u8] },
Verify { public_key: &'a [u8], message: &'a [u8], signature: &'a [u8] },
}
// --- Error type ---
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CryptoError {
InvalidOperation,
InvalidKeyLength,
InvalidNonceLength,
InvalidDataLength,
InvalidSignature,
BufferTooSmall,
AuthenticationFailed,
HardwareFailure,
}
// --- OneShot trait ---
pub trait OneShot<A: Algorithm> {
fn compute(&self, input: &CryptoInput, output: &mut [u8]) -> Result<usize, CryptoError>;
}
// --- Streaming trait ---
pub trait Streaming<A: Algorithm> {
type Session;
fn begin(&mut self) -> Result<Self::Session, CryptoError>;
fn update(&mut self, session: &mut Self::Session, data: &[u8]) -> Result<(), CryptoError>;
fn finalize(&mut self, session: Self::Session, output: &mut [u8]) -> Result<usize, CryptoError>;
fn cancel(&mut self, session: Self::Session);
}
```
### 1.4 Create `BUILD.bazel`
```python
load("@rules_rust//rust:defs.bzl", "rust_library")
rust_library(
name = "crypto-traits",
srcs = ["src/lib.rs"],
crate_features = select({
"//target:ecdsa": ["ecdsa"],
"//conditions:default": [],
}),
visibility = ["//visibility:public"],
)
```
### 1.5 Verification
```bash
bazel build //services/crypto/traits:crypto-traits
```
---
## Phase 2: Create `backend-rustcrypto` Crate
**Goal:** Implement `OneShot<A>` for all algorithms using RustCrypto.
### 2.1 Create directory structure
```
services/crypto/backend-rustcrypto/
├── Cargo.toml
├── BUILD.bazel
└── src/
└── lib.rs
```
### 2.2 Create `Cargo.toml`
```toml
[package]
name = "crypto-backend-rustcrypto"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0"
[features]
default = []
ecdsa = ["crypto-traits/ecdsa", "p256", "p384"]
[dependencies]
crypto-traits = { path = "../traits" }
# RustCrypto
sha2 = { version = "0.10", default-features = false }
hmac = { version = "0.12", default-features = false }
aes-gcm = { version = "0.10", default-features = false, features = ["aes"] }
# ECDSA (optional)
p256 = { version = "0.13", default-features = false, features = ["ecdsa"], optional = true }
p384 = { version = "0.13", default-features = false, features = ["ecdsa"], optional = true }
```
### 2.3 Create `src/lib.rs`
```rust
#![no_std]
use crypto_traits::{
Algorithm, CryptoError, CryptoInput, OneShot,
Sha256, Sha384, Sha512,
HmacSha256, HmacSha384, HmacSha512,
Aes256GcmEncrypt, Aes256GcmDecrypt,
};
#[cfg(feature = "ecdsa")]
use crypto_traits::{EcdsaP256Sign, EcdsaP256Verify, EcdsaP384Sign, EcdsaP384Verify};
use sha2::Digest as Sha2Digest;
use hmac::{Hmac, Mac};
use aes_gcm::{Aes256Gcm, KeyInit, Nonce as GcmNonce, aead::AeadInPlace};
/// RustCrypto-based backend. Stateless — can be freely copied.
#[derive(Clone, Copy, Default)]
pub struct RustCryptoBackend;
impl RustCryptoBackend {
pub const fn new() -> Self {
Self
}
}
// --- Generic digest helper ---
fn do_digest<D: Sha2Digest>(data: &[u8], output: &mut [u8]) -> Result<usize, CryptoError> {
let mut hasher = D::new();
hasher.update(data);
let result = hasher.finalize();
let size = result.len();
if output.len() < size {
return Err(CryptoError::BufferTooSmall);
}
output[..size].copy_from_slice(&result);
Ok(size)
}
impl OneShot<Sha256> for RustCryptoBackend {
fn compute(&self, input: &CryptoInput, output: &mut [u8]) -> Result<usize, CryptoError> {
let CryptoInput::Digest { data } = input else { return Err(CryptoError::InvalidOperation) };
do_digest::<sha2::Sha256>(data, output)
}
}
impl OneShot<Sha384> for RustCryptoBackend {
fn compute(&self, input: &CryptoInput, output: &mut [u8]) -> Result<usize, CryptoError> {
let CryptoInput::Digest { data } = input else { return Err(CryptoError::InvalidOperation) };
do_digest::<sha2::Sha384>(data, output)
}
}
impl OneShot<Sha512> for RustCryptoBackend {
fn compute(&self, input: &CryptoInput, output: &mut [u8]) -> Result<usize, CryptoError> {
let CryptoInput::Digest { data } = input else { return Err(CryptoError::InvalidOperation) };
do_digest::<sha2::Sha512>(data, output)
}
}
// --- Generic HMAC helper ---
fn do_hmac<D>(key: &[u8], data: &[u8], output: &mut [u8]) -> Result<usize, CryptoError>
where
D: sha2::Digest + hmac::digest::core_api::BlockSizeUser + Clone,
Hmac<D>: Mac,
{
let mut mac = <Hmac<D> as Mac>::new_from_slice(key)
.map_err(|_| CryptoError::InvalidKeyLength)?;
mac.update(data);
let result = mac.finalize().into_bytes();
let size = result.len();
if output.len() < size {
return Err(CryptoError::BufferTooSmall);
}
output[..size].copy_from_slice(&result);
Ok(size)
}
impl OneShot<HmacSha256> for RustCryptoBackend {
fn compute(&self, input: &CryptoInput, output: &mut [u8]) -> Result<usize, CryptoError> {
let CryptoInput::Mac { key, data } = input else { return Err(CryptoError::InvalidOperation) };
do_hmac::<sha2::Sha256>(key, data, output)
}
}
impl OneShot<HmacSha384> for RustCryptoBackend {
fn compute(&self, input: &CryptoInput, output: &mut [u8]) -> Result<usize, CryptoError> {
let CryptoInput::Mac { key, data } = input else { return Err(CryptoError::InvalidOperation) };
do_hmac::<sha2::Sha384>(key, data, output)
}
}
impl OneShot<HmacSha512> for RustCryptoBackend {
fn compute(&self, input: &CryptoInput, output: &mut [u8]) -> Result<usize, CryptoError> {
let CryptoInput::Mac { key, data } = input else { return Err(CryptoError::InvalidOperation) };
do_hmac::<sha2::Sha512>(key, data, output)
}
}
// --- AES-GCM ---
const AES_KEY_SIZE: usize = 32;
const GCM_NONCE_SIZE: usize = 12;
const GCM_TAG_SIZE: usize = 16;
impl OneShot<Aes256GcmEncrypt> for RustCryptoBackend {
fn compute(&self, input: &CryptoInput, output: &mut [u8]) -> Result<usize, CryptoError> {
let CryptoInput::Aead { key, nonce, data } = input else {
return Err(CryptoError::InvalidOperation)
};
if key.len() != AES_KEY_SIZE {
return Err(CryptoError::InvalidKeyLength);
}
if nonce.len() != GCM_NONCE_SIZE {
return Err(CryptoError::InvalidNonceLength);
}
let output_len = data.len() + GCM_TAG_SIZE;
if output.len() < output_len {
return Err(CryptoError::BufferTooSmall);
}
let key_array: [u8; 32] = key.try_into().map_err(|_| CryptoError::InvalidKeyLength)?;
let cipher = Aes256Gcm::new(&key_array.into());
let gcm_nonce = GcmNonce::from_slice(nonce);
output[..data.len()].copy_from_slice(data);
let tag = cipher.encrypt_in_place_detached(gcm_nonce, &[], &mut output[..data.len()])
.map_err(|_| CryptoError::HardwareFailure)?;
output[data.len()..output_len].copy_from_slice(&tag);
Ok(output_len)
}
}
impl OneShot<Aes256GcmDecrypt> for RustCryptoBackend {
fn compute(&self, input: &CryptoInput, output: &mut [u8]) -> Result<usize, CryptoError> {
let CryptoInput::Aead { key, nonce, data } = input else {
return Err(CryptoError::InvalidOperation)
};
if key.len() != AES_KEY_SIZE {
return Err(CryptoError::InvalidKeyLength);
}
if nonce.len() != GCM_NONCE_SIZE {
return Err(CryptoError::InvalidNonceLength);
}
if data.len() < GCM_TAG_SIZE {
return Err(CryptoError::InvalidDataLength);
}
let ciphertext_len = data.len() - GCM_TAG_SIZE;
if output.len() < ciphertext_len {
return Err(CryptoError::BufferTooSmall);
}
let key_array: [u8; 32] = key.try_into().map_err(|_| CryptoError::InvalidKeyLength)?;
let cipher = Aes256Gcm::new(&key_array.into());
let gcm_nonce = GcmNonce::from_slice(nonce);
let ciphertext = &data[..ciphertext_len];
let tag = &data[ciphertext_len..];
output[..ciphertext_len].copy_from_slice(ciphertext);
cipher.decrypt_in_place_detached(
gcm_nonce,
&[],
&mut output[..ciphertext_len],
tag.into(),
).map_err(|_| CryptoError::AuthenticationFailed)?;
Ok(ciphertext_len)
}
}
// --- ECDSA (feature-gated) ---
#[cfg(feature = "ecdsa")]
mod ecdsa_impl {
use super::*;
use p256::ecdsa::{SigningKey as P256SigningKey, VerifyingKey as P256VerifyingKey, Signature as P256Signature, signature::{Signer, Verifier}};
use p384::ecdsa::{SigningKey as P384SigningKey, VerifyingKey as P384VerifyingKey, Signature as P384Signature};
impl OneShot<EcdsaP256Sign> for RustCryptoBackend {
fn compute(&self, input: &CryptoInput, output: &mut [u8]) -> Result<usize, CryptoError> {
let CryptoInput::Sign { private_key, message } = input else {
return Err(CryptoError::InvalidOperation);
};
if private_key.len() != 32 {
return Err(CryptoError::InvalidKeyLength);
}
if output.len() < 64 {
return Err(CryptoError::BufferTooSmall);
}
let key = P256SigningKey::from_slice(private_key)
.map_err(|_| CryptoError::InvalidKeyLength)?;
let sig: P256Signature = key.sign(message);
output[..64].copy_from_slice(&sig.to_bytes());
Ok(64)
}
}
impl OneShot<EcdsaP256Verify> for RustCryptoBackend {
fn compute(&self, input: &CryptoInput, output: &mut [u8]) -> Result<usize, CryptoError> {
let CryptoInput::Verify { public_key, message, signature } = input else {
return Err(CryptoError::InvalidOperation);
};
if signature.len() != 64 {
return Err(CryptoError::InvalidSignature);
}
if output.is_empty() {
return Err(CryptoError::BufferTooSmall);
}
let key = P256VerifyingKey::from_sec1_bytes(public_key)
.map_err(|_| CryptoError::InvalidKeyLength)?;
let sig = P256Signature::from_slice(signature)
.map_err(|_| CryptoError::InvalidSignature)?;
match key.verify(message, &sig) {
Ok(()) => {
output[0] = 1; // verified
Ok(1)
}
Err(_) => {
output[0] = 0; // failed
Ok(1)
}
}
}
}
impl OneShot<EcdsaP384Sign> for RustCryptoBackend {
fn compute(&self, input: &CryptoInput, output: &mut [u8]) -> Result<usize, CryptoError> {
let CryptoInput::Sign { private_key, message } = input else {
return Err(CryptoError::InvalidOperation);
};
if private_key.len() != 48 {
return Err(CryptoError::InvalidKeyLength);
}
if output.len() < 96 {
return Err(CryptoError::BufferTooSmall);
}
let key = P384SigningKey::from_slice(private_key)
.map_err(|_| CryptoError::InvalidKeyLength)?;
let sig: P384Signature = Signer::sign(&key, message);
output[..96].copy_from_slice(&sig.to_bytes());
Ok(96)
}
}
impl OneShot<EcdsaP384Verify> for RustCryptoBackend {
fn compute(&self, input: &CryptoInput, output: &mut [u8]) -> Result<usize, CryptoError> {
let CryptoInput::Verify { public_key, message, signature } = input else {
return Err(CryptoError::InvalidOperation);
};
if signature.len() != 96 {
return Err(CryptoError::InvalidSignature);
}
if output.is_empty() {
return Err(CryptoError::BufferTooSmall);
}
let key = P384VerifyingKey::from_sec1_bytes(public_key)
.map_err(|_| CryptoError::InvalidKeyLength)?;
let sig = P384Signature::from_slice(signature)
.map_err(|_| CryptoError::InvalidSignature)?;
match Verifier::verify(&key, message, &sig) {
Ok(()) => {
output[0] = 1;
Ok(1)
}
Err(_) => {
output[0] = 0;
Ok(1)
}
}
}
}
}
```
### 2.4 Verification
```bash
bazel build //services/crypto/backend-rustcrypto:crypto-backend-rustcrypto
```
---
## Phase 3: Rewrite Server with `CryptoServer<B>`
**Goal:** Replace the 496-line server with a ~150-line generic implementation.
### 3.1 Modify `services/crypto/server/Cargo.toml`
Replace direct RustCrypto dependencies with trait crate:
```toml
[dependencies]
crypto-api = { path = "../api" }
crypto-traits = { path = "../traits" }
crypto-backend-rustcrypto = { path = "../backend-rustcrypto" }
zerocopy = { version = "0.8", features = ["derive"] }
# Remove: sha2, hmac, aes-gcm, p256, p384
```
### 3.2 Rewrite `services/crypto/server/src/main.rs`
New structure:
```rust
#![no_main]
#![no_std]
use crypto_api::{CryptoError as WireError, CryptoOp, CryptoRequestHeader, CryptoResponseHeader};
use crypto_traits::{
Algorithm, CryptoError, CryptoInput, OneShot, Streaming,
Sha256, Sha384, Sha512,
HmacSha256, HmacSha384, HmacSha512,
Aes256GcmEncrypt, Aes256GcmDecrypt,
};
#[cfg(feature = "ecdsa")]
use crypto_traits::{EcdsaP256Sign, EcdsaP256Verify, EcdsaP384Sign, EcdsaP384Verify};
use crypto_backend_rustcrypto::RustCryptoBackend;
use pw_status::Result;
use userspace::entry;
use userspace::syscall::{self, Signals};
use userspace::time::Instant;
use app_crypto_server::handle;
const MAX_REQUEST_SIZE: usize = 1024;
const MAX_RESPONSE_SIZE: usize = 1024;
// Type alias for backend — change this one line to swap backends
type Backend = RustCryptoBackend;
/// Generic crypto server.
struct CryptoServer<B> {
backend: B,
// Streaming session state (single session at a time)
session: StreamingSession,
}
enum StreamingSession {
None,
Sha256(Sha256State),
Sha384(Sha384State),
Sha512(Sha512State),
}
// Session state types (backend-specific, but hidden behind Streaming<A> trait)
struct Sha256State { /* backend session handle */ }
struct Sha384State { /* backend session handle */ }
struct Sha512State { /* backend session handle */ }
impl<B> CryptoServer<B>
where
B: OneShot<Sha256> + OneShot<Sha384> + OneShot<Sha512>
+ OneShot<HmacSha256> + OneShot<HmacSha384> + OneShot<HmacSha512>
+ OneShot<Aes256GcmEncrypt> + OneShot<Aes256GcmDecrypt>
#[cfg(feature = "ecdsa")]
+ OneShot<EcdsaP256Sign> + OneShot<EcdsaP256Verify>
+ OneShot<EcdsaP384Sign> + OneShot<EcdsaP384Verify>
{
fn new(backend: B) -> Self {
Self {
backend,
session: StreamingSession::None,
}
}
fn dispatch(&self, request: &[u8], response: &mut [u8]) -> usize {
// Parse header
if request.len() < CryptoRequestHeader::SIZE {
return encode_error(response, WireError::InvalidDataLength);
}
let header_bytes = &request[..CryptoRequestHeader::SIZE];
let Some(header) = zerocopy::Ref::<_, CryptoRequestHeader>::from_bytes(header_bytes).ok() else {
return encode_error(response, WireError::InvalidDataLength);
};
let header: &CryptoRequestHeader = &*header;
let op = match header.operation() {
Ok(op) => op,
Err(e) => return encode_error(response, e),
};
// Extract fields
let payload = &request[CryptoRequestHeader::SIZE..];
let key_len = header.key_length();
let nonce_len = header.nonce_length();
let data_len = header.data_length();
if payload.len() < key_len + nonce_len + data_len {
return encode_error(response, WireError::InvalidDataLength);
}
let key = &payload[..key_len];
let nonce = &payload[key_len..key_len + nonce_len];
let data = &payload[key_len + nonce_len..key_len + nonce_len + data_len];
// Build semantic input
let input = Self::build_input(op, key, nonce, data);
// Dispatch by op code
match op {
CryptoOp::Sha256Hash => self.run::<Sha256>(&input, response),
CryptoOp::Sha384Hash => self.run::<Sha384>(&input, response),
CryptoOp::Sha512Hash => self.run::<Sha512>(&input, response),
CryptoOp::HmacSha256 => self.run::<HmacSha256>(&input, response),
CryptoOp::HmacSha384 => self.run::<HmacSha384>(&input, response),
CryptoOp::HmacSha512 => self.run::<HmacSha512>(&input, response),
CryptoOp::Aes256GcmEncrypt => self.run::<Aes256GcmEncrypt>(&input, response),
CryptoOp::Aes256GcmDecrypt => self.run::<Aes256GcmDecrypt>(&input, response),
#[cfg(feature = "ecdsa")]
CryptoOp::EcdsaP256Sign => self.run::<EcdsaP256Sign>(&input, response),
#[cfg(feature = "ecdsa")]
CryptoOp::EcdsaP256Verify => self.run::<EcdsaP256Verify>(&input, response),
#[cfg(feature = "ecdsa")]
CryptoOp::EcdsaP384Sign => self.run::<EcdsaP384Sign>(&input, response),
#[cfg(feature = "ecdsa")]
CryptoOp::EcdsaP384Verify => self.run::<EcdsaP384Verify>(&input, response),
// Streaming ops (Phase 4)
CryptoOp::Sha256Begin | CryptoOp::Sha256Update | CryptoOp::Sha256Finish |
CryptoOp::Sha384Begin | CryptoOp::Sha384Update | CryptoOp::Sha384Finish |
CryptoOp::Sha512Begin | CryptoOp::Sha512Update | CryptoOp::Sha512Finish => {
encode_error(response, WireError::InvalidOperation) // TODO Phase 4
}
#[cfg(not(feature = "ecdsa"))]
CryptoOp::EcdsaP256Sign | CryptoOp::EcdsaP256Verify |
CryptoOp::EcdsaP384Sign | CryptoOp::EcdsaP384Verify => {
encode_error(response, WireError::InvalidOperation)
}
}
}
fn build_input<'a>(op: CryptoOp, key: &'a [u8], nonce: &'a [u8], data: &'a [u8]) -> CryptoInput<'a> {
match op {
CryptoOp::Sha256Hash | CryptoOp::Sha384Hash | CryptoOp::Sha512Hash =>
CryptoInput::Digest { data },
CryptoOp::HmacSha256 | CryptoOp::HmacSha384 | CryptoOp::HmacSha512 =>
CryptoInput::Mac { key, data },
CryptoOp::Aes256GcmEncrypt | CryptoOp::Aes256GcmDecrypt =>
CryptoInput::Aead { key, nonce, data },
CryptoOp::EcdsaP256Sign | CryptoOp::EcdsaP384Sign =>
CryptoInput::Sign { private_key: key, message: data },
CryptoOp::EcdsaP256Verify | CryptoOp::EcdsaP384Verify =>
CryptoInput::Verify { public_key: key, message: data, signature: nonce },
_ => CryptoInput::Digest { data: &[] }, // streaming handled separately
}
}
fn run<A: Algorithm>(&self, input: &CryptoInput, response: &mut [u8]) -> usize
where
B: OneShot<A>,
{
let result_start = CryptoResponseHeader::SIZE;
match self.backend.compute(input, &mut response[result_start..]) {
Ok(len) => encode_success(response, len),
Err(e) => encode_error(response, map_error(e)),
}
}
}
fn encode_error(response: &mut [u8], err: WireError) -> usize {
let header = CryptoResponseHeader::error(err);
let header_bytes = zerocopy::IntoBytes::as_bytes(&header);
response[..CryptoResponseHeader::SIZE].copy_from_slice(header_bytes);
CryptoResponseHeader::SIZE
}
fn encode_success(response: &mut [u8], result_len: usize) -> usize {
let header = CryptoResponseHeader::success(result_len as u16);
let header_bytes = zerocopy::IntoBytes::as_bytes(&header);
response[..CryptoResponseHeader::SIZE].copy_from_slice(header_bytes);
CryptoResponseHeader::SIZE + result_len
}
fn map_error(e: CryptoError) -> WireError {
match e {
CryptoError::InvalidOperation => WireError::InvalidOperation,
CryptoError::InvalidKeyLength => WireError::InvalidKeyLength,
CryptoError::InvalidNonceLength => WireError::InvalidNonceLength,
CryptoError::InvalidDataLength => WireError::InvalidDataLength,
CryptoError::InvalidSignature => WireError::InvalidSignature,
CryptoError::BufferTooSmall => WireError::BufferTooSmall,
CryptoError::AuthenticationFailed => WireError::AuthenticationFailed,
CryptoError::HardwareFailure => WireError::HardwareFailure,
}
}
#[entry]
fn main() -> ! {
match crypto_server_loop() {
Ok(()) => unreachable!(),
Err(e) => {
pw_log::error!("Crypto server error: {:?}", e);
loop { cortex_m::asm::wfi(); }
}
}
}
fn crypto_server_loop() -> Result<()> {
pw_log::info!("Crypto server starting");
let server = CryptoServer::new(RustCryptoBackend::new());
let mut request_buf = [0u8; MAX_REQUEST_SIZE];
let mut response_buf = [0u8; MAX_RESPONSE_SIZE];
loop {
syscall::object_wait(handle::CRYPTO, Signals::READABLE, Instant::MAX)?;
let len = syscall::channel_read(handle::CRYPTO, 0, &mut request_buf)?;
let response_len = server.dispatch(&request_buf[..len], &mut response_buf);
syscall::channel_respond(handle::CRYPTO, &response_buf[..response_len])?;
}
}
```
### 3.3 Verification
```bash
bazel test --config=virt_ast1060_evb //target/ast1060/crypto:crypto_test --test_output=all
```
All existing tests must pass unchanged.
---
## Phase 4: Add Streaming Support via `Streaming<A>` Trait
**Goal:** Implement streaming hash using the trait-based design.
### 4.1 Add to `crypto-traits/src/lib.rs`
Already defined in Phase 1. Add session state types.
### 4.2 Add to `backend-rustcrypto/src/lib.rs`
```rust
use sha2::{Sha256 as Sha2_256, Sha384 as Sha2_384, Sha512 as Sha2_512};
pub struct Sha256Session(Sha2_256);
pub struct Sha384Session(Sha2_384);
pub struct Sha512Session(Sha2_512);
impl Streaming<Sha256> for RustCryptoBackend {
type Session = Sha256Session;
fn begin(&mut self) -> Result<Self::Session, CryptoError> {
Ok(Sha256Session(Sha2_256::new()))
}
fn update(&mut self, session: &mut Self::Session, data: &[u8]) -> Result<(), CryptoError> {
session.0.update(data);
Ok(())
}
fn finalize(&mut self, session: Self::Session, output: &mut [u8]) -> Result<usize, CryptoError> {
if output.len() < 32 {
return Err(CryptoError::BufferTooSmall);
}
let result = session.0.finalize();
output[..32].copy_from_slice(&result);
Ok(32)
}
fn cancel(&mut self, _session: Self::Session) {
// Session dropped, nothing to clean up for software impl
}
}
// Similar for Sha384, Sha512
```
### 4.3 Update server dispatch
Add streaming op handling in `CryptoServer::dispatch()`.
### 4.4 Verification
```bash
bazel test --config=virt_ast1060_evb //target/ast1060/crypto:crypto_test --test_output=all
```
Streaming test (`test_sha256_streaming`) must pass.
---
## Phase 5: HACE Backend (Future)
**Goal:** Add hardware-accelerated backend for AST1060.
### 5.1 Create `backend-hace` crate
```
services/crypto/backend-hace/
├── Cargo.toml
├── BUILD.bazel
└── src/
└── lib.rs
```
### 5.2 Implement `OneShot<A>` and `Streaming<A>`
Wrap the `HaceController` from `aspeed-rust` to implement the traits.
### 5.3 Feature-gate in server
```rust
#[cfg(feature = "hace")]
type Backend = HaceBackend;
#[cfg(not(feature = "hace"))]
type Backend = RustCryptoBackend;
```
---
## Task Summary
| Phase | Task | New Files | Modified | LOC | Effort |
|-------|------|-----------|----------|-----|--------|
| 1 | Create `crypto-traits` crate | 3 | 0 | ~150 | 0.5 day |
| 2 | Create `backend-rustcrypto` crate | 3 | 0 | ~300 | 1 day |
| 3 | Rewrite server with `CryptoServer<B>` | 0 | 2 | ~150 (was 496) | 1 day |
| 4 | Add streaming via `Streaming<A>` | 0 | 3 | ~100 | 0.5 day |
| 5 | HACE backend (future) | 3 | 1 | ~300 | 3 days |
| **Total** | | **9** | **6** | **~1000** | **~6 days** |
---
## What Happens to Existing Code
| Component | Action |
|-----------|--------|
| `hal/blocking/src/digest.rs` | **Keep** — still used by non-crypto-server code |
| `hal/blocking/src/mac.rs` | **Keep** — still used by non-crypto-server code |
| `platform/impls/rustcrypto/src/controller.rs` | **Reference only** — mine for implementation patterns |
| `services/crypto/server/src/main.rs` | **Rewrite** — becomes generic `CryptoServer<B>` |
| `services/crypto/api/` | **Keep** — wire protocol unchanged |
| `services/crypto/client/` | **Keep** — client API unchanged |
| `services/crypto/tests/` | **Keep** — tests pass without modification |
---
## Code Organization After Refactor
```
services/crypto/
├── traits/ # NEW: Algorithm markers + OneShot/Streaming traits
│ └── src/lib.rs # ~150 lines, no dependencies
├── backend-rustcrypto/ # NEW: OneShot<A> impls using RustCrypto
│ └── src/lib.rs # ~300 lines
├── backend-hace/ # FUTURE: OneShot<A> impls using HACE
│ └── src/lib.rs # ~300 lines
├── api/ # UNCHANGED: Wire protocol
│ └── src/protocol.rs
├── server/ # REWRITTEN: Generic CryptoServer<B>
│ └── src/main.rs # ~150 lines (was 496)
├── client/ # UNCHANGED: Client library
│ └── src/lib.rs
└── tests/ # UNCHANGED: Integration tests
└── src/main.rs
```
---
## Verification Checkpoints
After each phase:
1. **Build:** `bazel build //services/crypto/...`
2. **Lint:** `bazel build //services/crypto/... --aspects=@aspect_rules_lint//lint:lint.bzl%clippy_lints`
3. **Test:** `bazel test --config=virt_ast1060_evb //target/ast1060/crypto:crypto_test --test_output=all`
4. **Commit:** One commit per phase with descriptive message
---
## Rollback Plan
Each phase is independently deployable:
- Phase 1-2 are additive (no existing code changes)
- Phase 3 is the critical rewrite — keep old server as `main.rs.bak` until tests pass
- Phase 4-5 are additive features
---
## Decision Record
**Date:** February 13, 2026
**Decision:** Implement new composable `OneShot<A>` / `Streaming<A>` traits rather
than reusing existing `openprot-hal-blocking` + `RustCryptoController`.
**Rationale:**
1. Current architecture uses "God trait" pattern requiring all backends to implement all algorithms
2. Adding new algorithms touches shared trait definitions, violating Open-Closed Principle
3. SPDM spec requires 10-15 additional algorithms; current approach would be 3-4 files per algorithm
4. Composable traits allow adding algorithms in 1-2 files with no modifications to existing code
5. HAL traits bundle resource recovery (typestate) with operation semantics—unnecessary for server
6. Service layer traits use session handles, better fit for IPC-based state management
**Tradeoff:** ~6 days upfront work vs. ongoing maintenance cost for 2+ years of algorithm additions.
**Accepted by:** User (February 13, 2026)
**Updated:** February 14, 2026 — Added architectural insights on trait layering and resource recovery decoupling
---
*Ready to execute.*