Crypto Service Design Review

Date: February 14, 2026
Authors: Steven
Status: Draft
Scope: Pigweed Crypto Service vs. Hubris Digest Server — architectural comparison and redesign proposal


Table of Contents

  1. Executive Summary
  2. Systems Under Review
  3. Hubris Digest Server Analysis
  4. OpenPRoT HAL Trait Architecture
  5. Pigweed Crypto Service Analysis
  6. Comparative Analysis
  7. Design Deficiencies — Hubris
  8. Design Deficiencies — Pigweed
  9. Proposed Architecture
  10. Migration Plan
  11. Appendix: Source Inventory
  12. Specification Gap Analysis

1. Executive Summary

This review compares two embedded crypto service implementations:

  • Hubris Digest Server (hubris/drv/digest-server/) — a 1,354-line server built on the Oxide Computer Hubris RTOS, using the OpenPRoT HAL trait hierarchy and Idol IDL for IPC. Supports hardware (ASPEED HACE) and software (RustCrypto) backends via compile-time feature selection. Provides session-based and one-shot digest/HMAC operations.

  • Pigweed Crypto Service (services/crypto/) — a 398-line server running on the Pigweed kernel, using RustCrypto crates directly with a manual wire protocol over channel IPC. Supports SHA-2, HMAC, AES-GCM, AES-CTR, and ECDSA P-256/P-384. One-shot only.

Key Findings:

FindingHubrisPigweed
Backend abstractionYes (trait-based)No (hardcoded RustCrypto)
Session/streaming APIYesNo
Code duplication severityCritical (6×6 boilerplate)Moderate (14 similar functions)
Algorithm extensibilityPoor (God trait changes required)Poor (touch 3+ files)
IPC mechanismGenerated (Idol)Manual wire format
Feature coverageDigest + HMAC onlyDigest + HMAC + AEAD + ECDSA
TestabilityHost-testable (RustCrypto backend)QEMU-only

Recommendation: Neither design is satisfactory for a production crypto service. We propose a unified architecture using algorithm marker types and the OneShot<A>/Streaming<A> trait pattern. This reduces server logic to ~120 lines, makes adding new algorithms a single-file change, and enables backend swappability with zero server modifications.

Architectural Insight — Trait Layering: The HAL layer (hal/blocking/) and service layer (services/crypto/api/) serve distinct purposes and should remain separate:

  • HAL traits abstract hardware controllers — used by platform impls regardless of OS
  • Service traits abstract IPC protocol — used by servers for backend dispatch

The HAL's owned::DigestOp pattern (typestate with resource recovery) should be decoupled from the core operation semantics. Resource recovery is valuable for baremetal code but adds friction when wrapping HAL controllers behind service traits. The proposed design uses a simple &mut self core trait with an optional Owned<T> wrapper for baremetal safety, eliminating the need for adapter layers.


2. Systems Under Review

2.1 Hubris Digest Server

ComponentFileLines
Serverhubris/drv/digest-server/src/main.rs1,354
IDL definitionhubris/idl/openprot-digest.idol360
Client APIhubris/drv/openprot-digest-api/src/lib.rs222
HAL traits (digest)bazel-stuff/hal/blocking/src/digest.rs~900
HAL traits (mac)bazel-stuff/hal/blocking/src/mac.rs~680
Service backend traitbazel-stuff/platform/traits/hubris/src/lib.rs~400
RustCrypto backendbazel-stuff/platform/impls/rustcrypto/src/controller.rs1,033
Test clienthubris/task/hmac-client/src/main.rs208

IPC Model: Idol IDL-generated stubs with compile-time type checking. Uses leases for zero-copy data transfer ([u8] read leases, [u32; N] write leases). Server-side is InOrder (serialized request processing).

Backend Model: Compile-time feature selection (aspeed-hace, rustcrypto, mock). A DefaultDigestDevice type alias selects the concrete backend. The server is generic: ServerImpl<D: HubrisDigestDevice>.

2.2 Pigweed Crypto Service

ComponentFileLines
Serverservices/crypto/server/src/main.rs398
Wire protocolservices/crypto/api/src/protocol.rs265
Client libraryservices/crypto/client/src/lib.rs509
Integration testsservices/crypto/tests/src/main.rs~200

IPC Model: Manual wire protocol over Pigweed kernel channels. Request header (8 bytes): op:u8 | flags:u8 | key_len:u16 | nonce_len:u16 | data_len:u16. Response header (4 bytes): status:u8 | reserved:u8 | result_len:u16. Uses zerocopy::FromBytes for zero-copy header parsing.

Backend Model: Direct RustCrypto crate imports. No trait abstraction. No backend swappability.


3. Hubris Digest Server Analysis

3.1 Architecture

┌─────────────────┐    Idol IPC    ┌───────────────────┐   HAL Traits    ┌──────────────────┐
│   Client Task   │ ────────────── │    ServerImpl<D>   │ ────────────── │   Backend        │
│  task_slot!()   │  (generated)   │                    │                │ • HaceController │
│  Digest::from() │                │ • Controllers<D>   │                │ • RustCryptoCtrl │
│                 │                │ • DigestSession<D> │                │ • MockDigestCtrl │
│                 │                │ • SessionContext<D>│                └──────────────────┘
└─────────────────┘                └───────────────────┘

3.2 Session Lifecycle

The Hubris server implements a full session lifecycle for streaming large data:

Client                         Server                        Backend
  │                              │                              │
  │ init_sha256()                │                              │
  ├─────────────────────────────►│ take hardware controller     │
  │         session_id=1         │ init_digest_session_sha256() │
  │◄─────────────────────────────├────────────────────────────►│
  │                              │     CryptoSession created    │
  │ update(id=1, chunk1)         │                              │
  ├─────────────────────────────►│ session.update(chunk1)       │
  │                              ├────────────────────────────►│
  │                              │                              │
  │ update(id=1, chunk2)         │                              │
  ├─────────────────────────────►│ session.update(chunk2)       │
  │                              ├────────────────────────────►│
  │                              │                              │
  │ finalize_sha256(id=1)        │                              │
  ├─────────────────────────────►│ session.finalize()           │
  │        Digest<8>             │──►(output, controller)       │
  │◄─────────────────────────────│   hardware returned          │
  │                              │                              │

3.3 One-Shot Path

For small data that fits in a single lease buffer (≤1024 bytes):

// Client side — single IPC call
digest.digest_oneshot_sha256(data.len() as u32, data, &mut result)?;

// Server side — create, feed, finalize in one method
fn compute_sha256_oneshot(&mut self, ...) {
    let controller = self.controllers.hardware.take()?;
    let ctx = controller.init_digest_sha256()?;
    let ctx = ctx.update(data)?;
    let (digest, controller) = ctx.finalize()?;
    self.controllers.hardware = Some(controller);
    // write digest to lease
}

3.4 Device Recovery Invariant

The most important architectural property: the hardware controller is always recovered, even on error paths. This is ensured by the move-semantics of DigestOp:

pub trait DigestOp: Sized {
    type Controller;
    fn update(self, data: &[u8]) -> Result<Self, Self::Error>;          // consumes, returns self
    fn finalize(self) -> Result<(Self::Output, Self::Controller), ...>; // returns controller
    fn cancel(self) -> Self::Controller;                                 // returns controller
}

Every terminal state — success (finalize) or error (cancel) — yields the controller back. The Rust type system statically prevents forgetting to return it.

3.5 Data Types

Digest output uses const-generic word arrays:

#[repr(C)]
pub struct Digest<const N: usize> {
    pub value: [u32; N],
}
AlgorithmTypeBytes
SHA-256Digest<8>32
SHA-384Digest<12>48
SHA-512Digest<16>64

HMAC output uses raw byte arrays: [u8; 32], [u8; 48], [u8; 64]. This type mismatch is a design flaw (see §7.4).


4. OpenPRoT HAL Trait Architecture

The OpenPRoT HAL defines a layered trait hierarchy for cryptographic operations:

4.1 Layer 1: Base Traits (hal/blocking/)

Owned (move-semantic) API:

// digest.rs — move-based init
pub trait DigestInit<T: DigestAlgorithm>: ErrorType + Sized {
    type Context: DigestOp<Output = Self::Output, Controller = Self>;
    type Output: IntoBytes;
    fn init(self, init_params: T) -> Result<Self::Context, Self::Error>;
}

// digest.rs — move-based operations
pub trait DigestOp: ErrorType + Sized {
    type Output: IntoBytes;
    type Controller;
    fn update(self, data: &[u8]) -> Result<Self, Self::Error>;
    fn finalize(self) -> Result<(Self::Output, Self::Controller), Self::Error>;
    fn cancel(self) -> Self::Controller;
}

// mac.rs — move-based init
pub trait MacInit<A: MacAlgorithm>: ErrorType + Sized {
    type Key: KeyHandle;
    type Context: MacOp<Output = Self::Output, Controller = Self>;
    type Output: IntoBytes;
    fn init(self, algorithm: A, key: Self::Key) -> Result<Self::Context, Self::Error>;
}

// mac.rs — move-based operations
pub trait MacOp: ErrorType + Sized {
    type Output: IntoBytes;
    type Controller;
    fn update(self, data: &[u8]) -> Result<Self, Self::Error>;
    fn finalize(self) -> Result<(Self::Output, Self::Controller), Self::Error>;
    fn cancel(self) -> Self::Controller;
}

Scoped (borrow-semantic) API — alternative API using &mut self:

pub trait DigestOp: ErrorType {  // Note: no Sized bound
    type Output: IntoBytes;
    fn update(&mut self, input: &[u8]) -> Result<(), Self::Error>;
    fn finalize(self) -> Result<Self::Output, Self::Error>;
}

The Hubris platform exclusively uses the owned API for session lifecycle management.

4.2 Layer 2: Crypto Service Trait (platform/traits/hubris/)

Important finding: Despite the crate name openprot-platform-traits-hubris and doc comments claiming “Hubris IDL compatibility” and “Hubris task model,” this trait has zero Hubris dependencies. Its only dependency is openprot-hal-blocking. There are no imports of userlib, idol_runtime, task_slot!, or any OS primitive. The trait is fully OS-agnostic and would work identically on Pigweed, Zephyr, or bare-metal. The name is misleading — it should be openprot-platform-traits-crypto.

Furthermore, this is not a platform trait at all. A platform trait abstracts hardware capabilities (GPIO, timers, DMA). HubrisDigestDevice bundles 7 algorithm- specific associated types and 12 init methods that mirror the digest server's IDL operations 1:1. It is a crypto service backend trait — the contract between the digest server and its pluggable backend. It belongs alongside the server, not in platform/traits/.

pub trait HubrisDigestDevice {
    type DigestContext256: DigestOp<Controller = Self, Output = Digest<8>>;
    type DigestContext384: DigestOp<Controller = Self, Output = Digest<12>>;
    type DigestContext512: DigestOp<Controller = Self, Output = Digest<16>>;
    type HmacKey: for<'a> TryFrom<&'a [u8]>;
    type HmacContext256: MacOp<Controller = Self, Output = [u8; 32]>;
    type HmacContext384: MacOp<Controller = Self, Output = [u8; 48]>;
    type HmacContext512: MacOp<Controller = Self, Output = [u8; 64]>;

    const MAX_KEY_SIZE: usize = 128;

    // 6 direct init methods (consume self, create context)
    fn init_digest_sha256(self) -> Result<Self::DigestContext256, HubrisCryptoError>;
    fn init_digest_sha384(self) -> Result<Self::DigestContext384, HubrisCryptoError>;
    fn init_digest_sha512(self) -> Result<Self::DigestContext512, HubrisCryptoError>;
    fn init_hmac_sha256(self, key: Self::HmacKey) -> Result<Self::HmacContext256, HubrisCryptoError>;
    fn init_hmac_sha384(self, key: Self::HmacKey) -> Result<Self::HmacContext384, HubrisCryptoError>;
    fn init_hmac_sha512(self, key: Self::HmacKey) -> Result<Self::HmacContext512, HubrisCryptoError>;

    // 6 session init methods (consume self, return CryptoSession with device recovery)
    fn init_digest_session_sha256(self) -> Result<CryptoSession<Self::DigestContext256, Self>, HubrisCryptoError> where Self: Sized;
    // ... (sha384, sha512, hmac×3 variants)
}

4.3 Layer 3: RAII Session Wrapper

pub struct CryptoSession<Context, Device> {
    context: Option<Context>,
    device: Option<Device>,
}

impl<Context, Device> CryptoSession<Context, Device> {
    pub fn new(context: Context, device: Device) -> Self;

    // Digest path (Context: DigestOp)
    pub fn update(mut self, data: &[u8]) -> Result<Self, HubrisCryptoError>;
    pub fn finalize(mut self) -> Result<(Context::Output, Device), HubrisCryptoError>;

    // MAC path (Context: MacOp)
    pub fn update_mac(mut self, data: &[u8]) -> Result<Self, HubrisCryptoError>;
    pub fn finalize_mac(mut self) -> Result<(Context::Output, Device), HubrisCryptoError>;
}

Critical observation: CryptoSession has no Drop implementation. If a session is dropped without calling finalize(), the device (Option<Device>) is silently discarded. The RAII recovery guarantee documented in the design exists only conceptually, not in code.

4.4 Layer 4: Backend Implementations

RustCrypto backend (platform/impls/rustcrypto/):

pub struct RustCryptoController {} // Non-cloneable, empty struct

pub struct DigestContext256(sha2::Sha256);   // Newtype wrappers
pub struct DigestContext384(sha2::Sha384);
pub struct DigestContext512(sha2::Sha512);
pub struct MacContext256(Hmac<Sha256>);
pub struct MacContext384(Hmac<Sha384>);
pub struct MacContext512(Hmac<Sha512>);

// Key type: stack-allocated [u8; 128] with Zeroize-on-Drop
pub struct SecureOwnedKey { ... }

The RustCrypto controller is an empty struct — cancel() and the device-recovery path simply call RustCryptoController::new() since software backends have no hardware state to recover. This means the entire owned-API device-recovery machinery is pure overhead for software backends.


5. Pigweed Crypto Service Analysis

5.1 Architecture

┌──────────────┐   channel_transact   ┌──────────────────┐   Direct calls   ┌──────────────┐
│  Client App  │ ───────────────────── │  Crypto Server   │ ──────────────── │  RustCrypto  │
│              │   manual wire fmt     │  (flat dispatch) │                  │  Crates      │
│  crypto_     │   CryptoRequest/     │                  │    sha2          │              │
│  client::*() │   ResponseHeader     │  dispatch_       │    hmac          │              │
│              │                      │    crypto_op()   │    aes-gcm       │              │
│              │                      │    → do_sha256() │    p256, p384    │              │
│              │                      │    → do_hmac*()  │                  │              │
│              │                      │    → do_aes*()   │                  │              │
│              │                      │    → do_ecdsa*() │                  │              │
└──────────────┘                      └──────────────────┘                  └──────────────┘

5.2 Wire Protocol

Request (8-byte header + variable payload):

 0       1       2       3       4       5       6       7
+-------+-------+-------+-------+-------+-------+-------+-------+
|  op   | flags |  key_len (LE) |nonce_len (LE) | data_len (LE) |
+-------+-------+-------+-------+-------+-------+-------+-------+
| key (key_len bytes) | nonce (nonce_len bytes) | data (data_len)|
+---------------------+------------------------+-----------------+

Response (4-byte header + variable payload):

 0       1       2       3
+-------+-------+-------+-------+
|status |reservd|result_len (LE)|
+-------+-------+-------+-------+
| result (result_len bytes)     |
+-------------------------------+

The protocol is simple, efficient, and zerocopy-compatible. It uses fixed-size headers that can be parsed with zero allocation.

5.3 Server Dispatch

fn dispatch_crypto_op(request: &[u8], response: &mut [u8]) -> usize {
    let header = parse_header(request)?;
    let (key, nonce, data) = extract_payload(request, &header);

    match op {
        CryptoOp::Sha256Hash      => do_sha256(data, response),
        CryptoOp::Sha384Hash      => do_sha384(data, response),
        CryptoOp::Sha512Hash      => do_sha512(data, response),
        CryptoOp::HmacSha256      => do_hmac_sha256(key, data, response),
        CryptoOp::HmacSha384      => do_hmac_sha384(key, data, response),
        CryptoOp::HmacSha512      => do_hmac_sha512(key, data, response),
        CryptoOp::Aes256GcmEncrypt => do_aes_gcm_encrypt(key, nonce, data, response),
        CryptoOp::Aes256GcmDecrypt => do_aes_gcm_decrypt(key, nonce, data, response),
        CryptoOp::Aes256CtrEncrypt => do_aes_ctr(key, nonce, data, response),
        CryptoOp::Aes256CtrDecrypt => do_aes_ctr(key, nonce, data, response),
        CryptoOp::EcdsaP256Sign    => do_ecdsa_p256_sign(key, data, response),
        CryptoOp::EcdsaP256Verify  => do_ecdsa_p256_verify(key, data, nonce, response),
        CryptoOp::EcdsaP384Sign    => do_ecdsa_p384_sign(key, data, response),
        CryptoOp::EcdsaP384Verify  => do_ecdsa_p384_verify(key, data, nonce, response),
    }
}

5.4 Supported Operations

CategoryOperationsKey SizesOutput
DigestSHA-256, SHA-384, SHA-512N/A32, 48, 64 bytes
MACHMAC-SHA-256/384/512Variable32, 48, 64 bytes
AEADAES-256-GCM encrypt/decrypt32 bytesCT+tag / PT
Stream cipherAES-256-CTR32 bytes, IV 16 bytesSame length
SignaturesECDSA P-256/P-384 sign/verify32/48 bytes64/96 bytes

5.5 Client Library

The client library (crypto_client) provides typed wrappers:

pub fn sha256(handle: u32, data: &[u8], output: &mut [u8; 32]) -> Result<(), ClientError>;
pub fn hmac_sha256(handle: u32, key: &[u8], data: &[u8], output: &mut [u8; 32]) -> Result<(), ClientError>;
pub fn aes_gcm_encrypt(handle: u32, key: &[u8;32], nonce: &[u8;12], ...) -> Result<usize, ClientError>;
pub fn ecdsa_p256_sign(handle: u32, private_key: &[u8;32], msg: &[u8], sig: &mut [u8;64]) -> ...;
pub fn ecdsa_p256_verify(handle: u32, pubkey: &[u8], msg: &[u8], sig: &[u8;64]) -> Result<bool, ...>;

Each function builds the request header, serializes key || nonce || data, calls channel_transact, and parses the response. The functions are generic over output size using parse_response<const N: usize>().


6. Comparative Analysis

6.1 Feature Matrix

CapabilityHubris Digest ServerPigweed Crypto Service
Hash (SHA-2 family)✅ SHA-256/384/512✅ SHA-256/384/512
HMAC✅ SHA-256/384/512✅ SHA-256/384/512
AEAD✅ AES-256-GCM
Stream cipher✅ AES-256-CTR
Digital signatures✅ ECDSA P-256/P-384
Session/streaming✅ init→update×N→finalize❌ One-shot only
Backend abstraction✅ Trait-based❌ Hardcoded
Hardware backend✅ ASPEED HACE
Zero-copy IPC✅ Leases❌ Copy-based
IDL code generation✅ Idol❌ Manual
Host testability✅ RustCrypto backend (no HW needed)❌ QEMU required

6.2 Code Metrics

MetricHubrisPigweed
Server lines1,354398
Unique logic lines (est.)~200~200
Boilerplate ratio85%50%
Lines to add SHA3~200 (6 methods)~30 (2 functions)
Lines to add BLAKE3~200 (6 methods)~15 (1 function)
Lines to add new backend~200 (new impl)Server rewrite

6.3 IPC Comparison

AspectHubris (Idol)Pigweed (Manual)
Type safetyCompile-time (generated stubs)Runtime (manual parsing)
Lease / zero-copyYes (read/write leases)No (copy to/from channel)
Task discoverytask_slot! macro, compile-timeHandle table (system.json5)
Error propagationResult<T, DigestError> generatedManual header status byte
Versioning / compatIDL revisionAd-hoc op-code numbering
Data size limitsLease max_len enforcementManual MAX_PAYLOAD_SIZE check
Overhead~0 (generated inline code)~0 (8-byte header parse)

7. Design Deficiencies — Hubris

7.1 Catastrophic Code Duplication

The server has 18 near-identical method bodies organized in a 6×3 matrix:

OperationSHA-256SHA-384SHA-512
Session initinit_sha256_internalinit_sha384_internalinit_sha512_internal
HMAC initinit_hmac_sha256_internalinit_hmac_sha384_internalinit_hmac_sha512_internal
Session finalizefinalize_sha256finalize_sha384finalize_sha512
HMAC finalizefinalize_hmac_sha256finalize_hmac_sha384finalize_hmac_sha512
Oneshot digestcompute_sha256_oneshotcompute_sha384_oneshotcompute_sha512_oneshot
HMAC oneshothmac_oneshot_sha256hmac_oneshot_sha384hmac_oneshot_sha512

Each row contains 3 methods that differ only in:

  • The associated type selected (DigestContext256 vs 384 vs 512)
  • The session variant (SessionContext::Sha256 vs Sha384 vs Sha512)
  • The output size constant

The update_internal method has a 6-arm match where every arm does session.update(data) — literally the same operation regardless of variant.

Impact: Adding SHA3 support requires ~200 lines of pure boilerplate across 6 new methods. A bug fix in the init sequence must be applied to 6 locations.

7.2 Misplaced Abstraction Layer

HubrisDigestDevice sits in platform/traits/ but is not a platform abstraction. The actual platform/HAL traits are DigestOp and MacOp in hal/blocking/ — those abstract what a crypto hardware block can do, independent of any server. HubrisDigestDevice is the digest server's backend contract: it says “to run this server, implement these 12 methods with these 7 context types.” This is application-level coupling masquerading as platform abstraction.

7.3 God Trait: HubrisDigestDevice

The crypto service backend trait bundles all algorithms into a single monolithic interface:

pub trait HubrisDigestDevice {
    type DigestContext256: DigestOp<...>;   // 7 associated types
    type DigestContext384: DigestOp<...>;
    type DigestContext512: DigestOp<...>;
    type HmacKey: TryFrom<&[u8]>;
    type HmacContext256: MacOp<...>;
    type HmacContext384: MacOp<...>;
    type HmacContext512: MacOp<...>;
    // 12 methods
}

This is an interface segregation violation. A backend implementing only SHA-256 must still provide all 7 associated types and 12 methods. Adding AEAD or ECDSA support would require modifying:

  1. The HubrisDigestDevice trait (new types + methods)
  2. Every backend impl (RustCryptoController, HaceController, MockDigestController)
  3. The SessionContext enum (new variants)
  4. The ServerImpl (6+ new methods per algorithm class)

7.4 Four Layers of Option to Track One Session

                    current_session: Option<                     // Layer 1: is any session active?
                        DigestSession {
                            context: SessionContext::Sha256(
                                Option<                          // Layer 2: is this variant occupied?
                                    CryptoSession {
                                        context: Option<Ctx>,    // Layer 3: internal take/put
                                        device: Option<Dev>,     // Layer 4: internal take/put
                                    }
                                >
                            )
                        }
                    >

This quadruple-nesting exists because CryptoSession uses an internal Option dance (take()/put()) to work around the borrow checker for its move-based update → consume self → return new self pattern. But since CryptoSession already consumes self in update(), the internal Option is redundant given proper ownership handling.

7.5 Digest vs. HMAC Output Type Mismatch

Digests return Digest<N> (a [u32; N] word array) while HMACs return [u8; M] (a byte array). This forces the server to include a manual byte-to-word conversion:

let mut u32_result = [0u32; 8];
for (i, chunk) in result.chunks(4).enumerate() {
    u32_result[i] = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
}

This 4-line pattern is repeated 6 times (once per HMAC algorithm × once per finalize + oneshot). If HMAC output were also Digest<N>, this conversion would be unnecessary and the HMAC finalize methods could share code paths with digest finalize.

7.6 Missing Drop on CryptoSession

The CryptoSession struct wraps Option<Device> but has no Drop implementation in the actual source code. The design documentation claims RAII recovery, but if a CryptoSession is dropped (e.g., on panic or early return), the device is silently leaked. For hardware backends, this means permanent controller lockout until system restart.

7.7 No Session Timeout

struct DigestSession<D: HubrisDigestDevice> {
    created_at: u64,  // stored but NEVER CHECKED
    ...
}

A crashed or malicious client that calls init_sha256() but never finalize() will permanently lock the server. There is no watchdog, no keepalive, no administrative cancel command in the IDL.

7.8 Software Backend Overhead

For RustCryptoController (an empty struct {}):

  • cancel(self) -> Self::Controller returns RustCryptoController::new() — a no-op constructor
  • The entire owned-API device-recovery dance adds zero value for software backends
  • The SecureOwnedKey type allocates 128 bytes on the stack regardless of actual key size

This is not a fatal flaw (the overhead is small), but it reveals a leaky abstraction: the API is designed for hardware single-controller semantics and imposes its cost model on all backends.


8. Design Deficiencies — Pigweed

8.1 Hardcoded Backend

The server directly imports crate-level types:

use sha2::{Digest, Sha256, Sha384, Sha512};
use hmac::{Hmac, Mac};
use aes_gcm::{Aes256Gcm, KeyInit, Nonce as GcmNonce};
use p256::ecdsa::{SigningKey as P256SigningKey, ...};

Switching to hardware crypto (e.g., ASPEED HACE) requires rewriting the server. There is no trait abstraction, no feature gating, no pluggability.

8.2 Flat Function Explosion

14 separate do_* functions with repeated patterns:

fn do_sha256(data: &[u8], response: &mut [u8]) -> usize {
    let mut hasher = Sha256::new();   // Only this line differs
    hasher.update(data);
    let result = hasher.finalize();
    encode_success(response, &result[..SHA256_OUTPUT_SIZE])
}

fn do_sha384(data: &[u8], response: &mut [u8]) -> usize {
    let mut hasher = Sha384::new();   // Only this line differs
    hasher.update(data);
    let result = hasher.finalize();
    encode_success(response, &result[..SHA384_OUTPUT_SIZE])
}
// ... and so on for sha512

The SHA functions are identical except for the hasher type and output size. The HMAC functions are identical except for the type alias. These should be unified through generics.

8.3 No Streaming / Session Support

The server processes each request in a single dispatch_crypto_op call. There is no concept of a session, init/update/finalize lifecycle, or streaming. This means:

  • Data larger than MAX_PAYLOAD_SIZE (512 bytes) cannot be hashed
  • The entire message must fit in the IPC buffer
  • Firmware image verification (common embedded use case) is impossible

The flags field in CryptoRequestHeader is defined but always set to 0 — it could carry session semantics.

8.4 Semantic Field Overloading

The wire protocol uses three generic fields (key, nonce, data) for all operation types. This leads to semantic mismatches:

Operationkey fieldnonce fielddata field
Hashunusedunusedmessage
HMACHMAC keyunusedmessage
AES-GCMAES keyGCM nonceplaintext
AES-CTRAES keyCTR IVplaintext
ECDSA signprivate keyunusedmessage
ECDSA verifypublic keysignature (!)message

ECDSA verify stuffs the signature into the nonce field. This is semantically wrong and confusing — a signature is not a nonce. The comment in the client says // We use: key=pubkey, nonce=signature, data=message, acknowledging the hack.

8.5 No Constant-Time HMAC Verification

The server produces HMAC tags but provides no verify operation. Clients must compare tags themselves, risking timing side-channel attacks if they use == instead of constant-time comparison. The Hubris IDL defines hmac_verify_* operations; the Pigweed service does not.

8.6 Not Host-Testable

All tests require QEMU because the server directly uses kernel syscalls (object_wait, channel_read, channel_respond). There is no way to test crypto logic independently of the IPC layer.


9. Proposed Architecture

9.1 Design Principles

  1. Algorithm = type parameter, not enum variant — let the compiler monomorphize dispatch
  2. Backend = trait bound, not import — swap implementations via generics
  3. Separate concerns — wire format ↔ dispatch ↔ crypto logic ↔ backend
  4. One-shot first, session optional — one-shot is the common case; sessions are additive
  5. Const-generic output sizes — encode output dimensions in the type system

9.2 Crate Structure

services/crypto/
├── traits/         # NEW: no_std, no dependencies
│   └── src/lib.rs  # Algorithm, OneShot<A>, Streaming<A>, CryptoInput
├── backend-rustcrypto/   # NEW: OneShot<*> impls for RustCrypto
│   └── src/lib.rs
├── api/            # KEEP: wire protocol (unchanged)
│   └── src/protocol.rs
├── server/         # REWRITE: generic CryptoServer<B>
│   └── src/main.rs
├── client/         # KEEP: client library (unchanged)
│   └── src/lib.rs
└── tests/          # KEEP: integration tests (unchanged)
    └── src/main.rs

9.3 Algorithm Marker Types

// services/crypto/traits/src/lib.rs
#![no_std]

/// Marker trait for crypto algorithms. Each algorithm is a zero-sized type.
pub trait Algorithm {
    /// The fixed output size in bytes (0 for variable-output operations like AEAD).
    const OUTPUT_SIZE: usize;
    /// The wire protocol op code.
    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; }

// --- Stream cipher algorithms ---

pub struct Aes256Ctr;
impl Algorithm for Aes256Ctr { const OUTPUT_SIZE: usize = 0; const OP_CODE: u8 = 0x30; }

// --- Signature algorithms ---

pub struct EcdsaP256Sign;
impl Algorithm for EcdsaP256Sign { const OUTPUT_SIZE: usize = 64; const OP_CODE: u8 = 0x40; }

pub struct EcdsaP256Verify;
impl Algorithm for EcdsaP256Verify { const OUTPUT_SIZE: usize = 1; const OP_CODE: u8 = 0x41; }

pub struct EcdsaP384Sign;
impl Algorithm for EcdsaP384Sign { const OUTPUT_SIZE: usize = 96; const OP_CODE: u8 = 0x42; }

pub struct EcdsaP384Verify;
impl Algorithm for EcdsaP384Verify { const OUTPUT_SIZE: usize = 1; const OP_CODE: u8 = 0x43; }

9.4 Structured Input Type

Replace the flat key || nonce || data concatenation with a semantically typed enum:

/// Structured crypto input — each variant carries exactly the fields
/// its operation class needs. No more stuffing signatures into "nonce".
pub enum CryptoInput<'a> {
    /// Hash operations (SHA-*): just the message data.
    Digest { data: &'a [u8] },

    /// MAC operations (HMAC-*): key + message data.
    Mac { key: &'a [u8], data: &'a [u8] },

    /// AEAD operations (AES-GCM): key + nonce + plaintext/ciphertext.
    /// For decrypt: data = ciphertext || tag.
    Aead { key: &'a [u8], nonce: &'a [u8], data: &'a [u8] },

    /// Stream cipher (AES-CTR): key + IV + data.
    StreamCipher { key: &'a [u8], iv: &'a [u8], data: &'a [u8] },

    /// Signing: private key + message.
    Sign { private_key: &'a [u8], message: &'a [u8] },

    /// Verification: public key + message + signature.
    Verify { public_key: &'a [u8], message: &'a [u8], signature: &'a [u8] },
}

impl<'a> CryptoInput<'a> {
    /// Construct from parsed wire format header + payload.
    /// This is the ONLY place that knows about the key/nonce/data encoding.
    pub fn from_wire(op: CryptoOp, key: &'a [u8], nonce: &'a [u8], data: &'a [u8]) -> Self {
        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::Aes256CtrEncrypt | CryptoOp::Aes256CtrDecrypt =>
                CryptoInput::StreamCipher { key, iv: 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 },
        }
    }
}

9.5 Backend Traits

/// One-shot crypto operation trait. One impl per (Backend, Algorithm) pair.
///
/// The backend is `&self` (not consumed) because software backends are stateless.
/// Hardware backends that need exclusive access should use internal `RefCell`
/// or be wrapped in an `Option<HwController>` at the server level.
pub trait OneShot<A: Algorithm> {
    type Error;

    /// Execute a one-shot crypto operation.
    ///
    /// `input`:  structured crypto input matching the algorithm class.
    /// `output`: mutable buffer for the result (at least `A::OUTPUT_SIZE` bytes,
    ///           or `data.len() + TAG_SIZE` for AEAD).
    ///
    /// Returns the number of bytes written to `output`.
    fn compute(&self, input: &CryptoInput, output: &mut [u8]) -> Result<usize, Self::Error>;
}

/// Session-based streaming trait (optional — only needed for large data).
pub trait Streaming<A: Algorithm> {
    type Session;
    type Error;

    fn begin(&mut self) -> Result<Self::Session, Self::Error>;
    fn feed(&mut self, session: &mut Self::Session, data: &[u8]) -> Result<(), Self::Error>;
    fn finish(&mut self, session: Self::Session, output: &mut [u8]) -> Result<usize, Self::Error>;
    fn cancel(&mut self, session: Self::Session);
}

9.6 RustCrypto Backend

// services/crypto/backend-rustcrypto/src/lib.rs
#![no_std]

use crypto_traits::{Algorithm, CryptoInput, OneShot, CryptoError};
use crypto_traits::{Sha256, Sha384, Sha512, HmacSha256, HmacSha384, HmacSha512};
use crypto_traits::{Aes256GcmEncrypt, Aes256GcmDecrypt, Aes256Ctr};
use crypto_traits::{EcdsaP256Sign, EcdsaP256Verify, EcdsaP384Sign, EcdsaP384Verify};

pub struct RustCryptoBackend;

// --- Generic digest helper (eliminates do_sha256/384/512 duplication) ---

fn do_digest<D: sha2::Digest>(data: &[u8], output: &mut [u8]) -> Result<usize, CryptoError> {
    let result = D::digest(data);
    let size = result.len();
    output[..size].copy_from_slice(&result);
    Ok(size)
}

impl OneShot<Sha256> for RustCryptoBackend {
    type Error = CryptoError;
    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 {
    type Error = CryptoError;
    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 {
    type Error = CryptoError;
    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 (eliminates do_hmac_sha256/384/512 duplication) ---

fn do_hmac<D: hmac::digest::core_api::CoreProxy>(
    key: &[u8], data: &[u8], output: &mut [u8]
) -> Result<usize, CryptoError>
where
    hmac::Hmac<D>: hmac::Mac,
{
    use hmac::Mac;
    let mut mac = <hmac::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();
    output[..size].copy_from_slice(&result);
    Ok(size)
}

// ... (similar compact impls for HmacSha256, AES-GCM, ECDSA)

// Adding BLAKE3 is exactly this:
//
// impl OneShot<Blake3> for RustCryptoBackend {
//     type Error = CryptoError;
//     fn compute(&self, input: &CryptoInput, output: &mut [u8]) -> Result<usize, CryptoError> {
//         let CryptoInput::Digest { data } = input else { return Err(CryptoError::InvalidOperation) };
//         let hash = blake3::hash(data);
//         output[..32].copy_from_slice(hash.as_bytes());
//         Ok(32)
//     }
// }

9.7 Generic Server

// services/crypto/server/src/main.rs
pub struct CryptoServer<B> {
    backend: B,
}

impl<B> CryptoServer<B>
where
    B: OneShot<Sha256, Error = CryptoError>
     + OneShot<Sha384, Error = CryptoError>
     + OneShot<Sha512, Error = CryptoError>
     + OneShot<HmacSha256, Error = CryptoError>
     + OneShot<HmacSha384, Error = CryptoError>
     + OneShot<HmacSha512, Error = CryptoError>
     + OneShot<Aes256GcmEncrypt, Error = CryptoError>
     + OneShot<Aes256GcmDecrypt, Error = CryptoError>
     + OneShot<Aes256Ctr, Error = CryptoError>
     + OneShot<EcdsaP256Sign, Error = CryptoError>
     + OneShot<EcdsaP256Verify, Error = CryptoError>
     + OneShot<EcdsaP384Sign, Error = CryptoError>
     + OneShot<EcdsaP384Verify, Error = CryptoError>
{
    pub fn new(backend: B) -> Self {
        Self { backend }
    }

    /// The entire dispatch logic. Compare with the current 300+ line dispatch.
    pub fn dispatch(&self, request: &[u8], response: &mut [u8]) -> usize {
        let (header, key, nonce, data) = match parse_request(request) {
            Ok(parsed) => parsed,
            Err(e) => return encode_error(response, e),
        };

        let input = CryptoInput::from_wire(header.operation().unwrap(), key, nonce, data);

        match header.operation().unwrap() {
            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),
            CryptoOp::Aes256CtrEncrypt => self.run::<Aes256Ctr>(&input, response),
            CryptoOp::Aes256CtrDecrypt => self.run::<Aes256Ctr>(&input, response),
            CryptoOp::EcdsaP256Sign    => self.run::<EcdsaP256Sign>(&input, response),
            CryptoOp::EcdsaP256Verify  => self.run::<EcdsaP256Verify>(&input, response),
            CryptoOp::EcdsaP384Sign    => self.run::<EcdsaP384Sign>(&input, response),
            CryptoOp::EcdsaP384Verify  => self.run::<EcdsaP384Verify>(&input, response),
        }
    }

    /// Generic one-shot dispatch — ONE function for all algorithms.
    fn run<A: Algorithm>(&self, input: &CryptoInput, response: &mut [u8]) -> usize
    where
        B: OneShot<A, Error = CryptoError>,
    {
        let result_start = CryptoResponseHeader::SIZE;
        match self.backend.compute(input, &mut response[result_start..]) {
            Ok(len) => {
                let header = CryptoResponseHeader::success(len as u16);
                response[..CryptoResponseHeader::SIZE]
                    .copy_from_slice(zerocopy::IntoBytes::as_bytes(&header));
                result_start + len
            }
            Err(e) => encode_error(response, e),
        }
    }
}

Total server dispatch logic: ~50 lines. The remaining ~70 lines are the event loop, request parsing, and error encoding — shared infrastructure that doesn't change when algorithms are added.

9.8 Session Extension (Future)

The wire protocol already has an unused flags byte. Define session semantics:

flags bit 0:    0 = one-shot, 1 = session operation
flags bits 1-2: 00 = init, 01 = update, 10 = finalize, 11 = cancel
flags bits 3-7: reserved

The server would add:

if header.flags & 0x01 != 0 {
    let session_op = (header.flags >> 1) & 0x03;
    match session_op {
        0 => self.session_init(op, key, response),
        1 => self.session_update(header.data_length() as u32, data, response),
        2 => self.session_finalize(op, response),
        3 => self.session_cancel(response),
        _ => encode_error(response, CryptoError::InvalidOperation),
    }
} else {
    self.dispatch_oneshot(op, &input, response)
}

This requires no wire format changes, no client library changes for one-shot users, and no server changes for existing algorithms.

9.9 Architecture Diagram

                         ┌─────────────────────────────────────────┐
                         │          crypto-traits crate            │
                         │   (no_std, zero dependencies)           │
                         │                                         │
                         │  pub trait Algorithm { OUTPUT_SIZE, .. } │
                         │  pub trait OneShot<A> { compute() }     │
                         │  pub trait Streaming<A> { begin/feed/.. }│
                         │  pub enum CryptoInput { Digest, Mac, .. }│
                         │                                         │
                         │  Sha256, Sha384, HmacSha256, ...        │
                         │  Aes256GcmEncrypt, EcdsaP256Sign, ...   │
                         └────────────┬────────────────────────────┘
                                      │
                    ┌─────────────────┴─────────────────┐
                    │                                   │
          ┌─────────▼──────┐                 ┌──────────▼─────────┐
          │  RustCrypto    │                 │  ASPEED HACE       │
          │  Backend       │                 │  Backend           │
          │  (host + target)│                │  (target only)     │
          │                │                 │                    │
          │ impl OneShot   │                 │ impl OneShot       │
          │    <Sha256>    │                 │    <Sha256>        │
          │    <HmacSha256>│                 │    <HmacSha256>    │
          │    <AesGcm..>  │                 │    ...             │
          │    <Ecdsa..>   │                 │                    │
          └───────┬────────┘                 └──────────┬─────────┘
                  │                                     │
                  └──────────────┬───────────────────────┘
                                     │
                          ┌──────────▼──────────┐
                          │  CryptoServer<B>    │
                          │                     │
                          │  dispatch()         │
                          │    → run::<Sha256>() │  ← monomorphized per algorithm
                          │    → run::<Hmac..>() │
                          │    → run::<Aes..>()  │
                          │                     │
                          │  Uses:              │
                          │  • crypto-api (wire)│
                          │  • crypto-traits    │
                          │  • kernel syscalls  │
                          └──────────┬──────────┘
                                     │
                          ┌──────────▼──────────┐
                          │  crypto-client      │
                          │  (unchanged API)    │
                          │                     │
                          │  sha256()           │
                          │  hmac_sha256()      │
                          │  aes_gcm_encrypt()  │
                          │  ecdsa_p256_sign()  │
                          └─────────────────────┘

9.10 Trait Layering: HAL vs. Service

The architecture contains two distinct trait layers that serve different purposes:

┌─────────────────────────────────────────────────────────────┐
│                     Crypto Client                           │
│  (user apps calling CryptoClient::sha256(), etc.)           │
└────────────────────────┬────────────────────────────────────┘
                         │ IPC (channel_call)
                         ▼
┌─────────────────────────────────────────────────────────────┐
│                    Crypto Server                            │
│  dispatch_crypto_op() → OneShot<A> / Streaming<A>           │
└────────────────────────┬────────────────────────────────────┘
                         │ Service-layer traits
                         ▼
┌─────────────────────────────────────────────────────────────┐
│              services/crypto/api/backend.rs                 │  ◄── SERVICE LAYER
│  OneShot<A>, Streaming<A>, CryptoInput, BackendError        │
│  (IPC-oriented: writes to &mut [u8], session handles)       │
└────────────────────────┬────────────────────────────────────┘
                         │ impl OneShot<A> for ...
                         ▼
┌──────────────────────────────┬──────────────────────────────┐
│   RustCryptoBackend          │      HaceBackend             │
│   (software impl)            │      (hardware impl)         │
└──────────────────────────────┴───────────────┬──────────────┘
                                               │ uses HAL
                                               ▼
┌─────────────────────────────────────────────────────────────┐
│                hal/blocking/src/digest.rs                   │  ◄── HAL LAYER
│  owned::DigestInit, owned::DigestOp, scoped::*              │
│  (hardware-oriented: typestate, resource recovery)          │
└─────────────────────────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│               platform/impls/hace/                          │
│  HaceController impl owned::DigestInit<Sha2_256>            │
└─────────────────────────────────────────────────────────────┘

HAL Layer (hal/blocking/) — abstracts hardware vs. software crypto implementations. Used by platform impls (HACE, RustCrypto controllers) regardless of whether they run baremetal or inside a server.

Service Layer (services/crypto/api/) — abstracts IPC protocol between server and backends. Handles session management, writes to caller-provided buffers, and presents a uniform interface to the server dispatch logic.

This separation is intentional — HAL traits handle hardware resource management while service traits handle IPC concerns.

9.11 Decoupling Resource Recovery

The HAL's owned::DigestOp currently bundles two concerns:

Concern 1: Operation semantics — what the API does

fn update(&mut self, data: &[u8]) -> Result<(), Error>;
fn finalize(&mut self, output: &mut [u8]) -> Result<usize, Error>;

Concern 2: Resource lifecycle — typestate enforcement for baremetal safety

fn finalize(self) -> Result<(Output, Controller), Error>;  // consume + recover
fn cancel(self) -> Controller;                              // recover without completing

This coupling creates friction:

  • Software backends (RustCrypto) have no resources to recover — the pattern is pure overhead
  • Servers already manage sessions via explicit handles — the typestate ceremony is redundant
  • Wrapping HAL controllers in service traits requires adapting between the two models

Proposed: Decouple into composable parts

// Core trait — used by HAL impls and servers directly
pub trait DigestOp {
    fn update(&mut self, data: &[u8]) -> Result<(), Error>;
    fn finalize(&mut self, output: &mut [u8]) -> Result<usize, Error>;
    fn reset(&mut self);  // reuse without destroying
}

// Optional typestate wrapper — for baremetal code that wants compile-time safety
pub struct Owned<T>(T);

impl<T: DigestOp> Owned<T> {
    pub fn update(mut self, data: &[u8]) -> Result<Self, Error> { ... }
    pub fn finalize(self) -> Result<(Output, T), Error> { ... }  // recovers inner
    pub fn cancel(self) -> T { ... }
}

Benefits of decoupling:

AspectCurrent (coupled)Proposed (decoupled)
HAL controllersMust impl owned semanticsImpl simple &mut self trait
Server backendsWrap HAL with adaptersUse HAL controllers directly
Baremetal apps✅ Typestate enforcementOpt-in via Owned<T> wrapper
Trait duplicationHAL + Service traitsSingle core trait
Software backendsFake resource recoveryNo overhead

The service-layer Streaming<A> trait already uses the simpler &mut self + session handle pattern. Aligning the HAL to this model would eliminate the adaptation layer between them.


10. Migration Plan

Phase 1: Extract crypto-traits crate

Effort: ~150 lines, 1 day
Risk: None (additive only, no existing code changes)
Deliverable: New crate with Algorithm, OneShot<A>, CryptoInput types

Phase 2: Implement backend-rustcrypto

Effort: ~250 lines, 1–2 days
Risk: Low (wrapper impls over existing working code)
Deliverable: impl OneShot<*> for RustCryptoBackend — all 14 algorithm impls
Validation: Unit tests against known test vectors (RFC 4231, NIST vectors)

Phase 3: Rewrite server with CryptoServer<B>

Effort: ~120 lines (down from 398), 1 day
Risk: Medium (functional rewrite, same wire protocol)
Deliverable: Generic server — existing clients work unchanged
Validation: Existing integration tests pass without modification

Phase 4: Add Streaming support

Effort: ~100 lines server + ~50 lines client, 2 days
Risk: Medium (new wire protocol semantics using flags byte)
Deliverable: Session-based hash API for firmware image verification
Validation: New test: hash 8KB data in 1KB chunks, compare with one-shot

Phase 5: Add ASPEED HACE backend

Effort: ~300 lines (new crate), 3–5 days
Risk: High (hardware integration, DMA, register access)
Deliverable: impl OneShot<Sha256/384/512> for HaceBackend
Validation: Same test suite as RustCrypto — results must match

Why No Mock Backend Is Needed

RustCryptoBackend is a pure software implementation with zero hardware dependencies — it compiles and runs identically on host, QEMU, and target. Unlike a mock that returns canned responses, RustCrypto provides real cryptographic validation against known test vectors (RFC 4231, NIST). Using it for host testing gives actual correctness assurance rather than tautological “mock returns what you told it to return” tests.

A mock would only be useful for:

  • Forcing specific error paths (e.g., HardwareFailure) that RustCrypto never produces
  • Deterministic timing tests
  • Fuzzing the server's error handling

These are narrow scenarios that don't justify a dedicated backend. Error-path testing can be done with a thin wrapper that injects faults around the real backend.

Impact Summary

MetricCurrentAfter Phase 3After Phase 5
Server lines398~120~120 (unchanged)
Add SHA3~30 lines, 3 files~15 lines, 1 file~15 lines, 1 file
Add BLAKE3~30 lines, 3 files~15 lines, 1 file~15 lines, 1 file
Backend swapServer rewriteChange type paramChange type param
Host tests✅ (RustCrypto runs on host)
Streaming❌ (Phase 4)
HW crypto

11. Appendix: Source Inventory

Hubris Digest Server Sources

FileLinesPurpose
hubris/drv/digest-server/src/main.rs1,354Server with Idol IDL integration
hubris/idl/openprot-digest.idol360Interface definition (RON syntax)
hubris/drv/openprot-digest-api/src/lib.rs222Client API types and error enum
hubris/task/hmac-client/src/main.rs208Test client task

OpenPRoT HAL Sources

FileLinesPurpose
bazel-stuff/hal/blocking/src/digest.rs~900DigestOp, DigestInit traits (owned + scoped)
bazel-stuff/hal/blocking/src/mac.rs~680MacOp, MacInit traits (owned + scoped)
bazel-stuff/platform/traits/hubris/src/lib.rs~400HubrisDigestDevice, CryptoSession
bazel-stuff/platform/impls/rustcrypto/src/controller.rs1,033RustCryptoController + tests

Pigweed Crypto Service Sources

FileLinesPurpose
services/crypto/api/src/protocol.rs265Wire protocol, CryptoOp enum, headers
services/crypto/server/src/main.rs398Crypto server (flat dispatch)
services/crypto/client/src/lib.rs509Client library
services/crypto/tests/src/main.rs~200Integration tests (QEMU)

Key Design Decisions Summary

DecisionHubris ApproachPigweed ApproachProposed Approach
Algorithm dispatchRuntime enum match × 6Runtime enum match × 1Compile-time monomorphization
Backend abstractionGod trait (HubrisDigestDevice)NoneComposable OneShot<A> traits
Output typesDigest<N> for hash, [u8;N] for HMAC[u8] everywhere[u8] everywhere (simplicity)
Session managementCryptoSession RAII wrapperNoneOptional Streaming<A> trait
IPC mechanismGenerated (Idol)Manual wire protocolKeep manual (simpler for Pigweed)
Error handlingDigestError enum (17 variants)CryptoError enum (12 variants)Keep CryptoError (sufficient)
Key managementSecureOwnedKey with ZeroizeRaw &[u8] slices&[u8] for now, SecureKey later
TestingRustCrypto (host-testable)QEMU onlyRustCrypto (host-testable)

12. Specification Gap Analysis

Cross-reference of the OpenPRoT specification (docs/src/specification/) against the currently implemented crypto operations in CryptoOp (14 ops).

12.1 Specification Sources

The following specification documents were reviewed:

  • specification/middleware/spdm.md — SPDM algorithm requirements (primary crypto source)
  • specification/services/attestation.md — Attestation architecture, RATS EAT, COSE, DICE
  • specification/services/fwupdate.md — PLDM firmware update (no additional crypto beyond integrity)
  • specification/middleware/pldm.md — PLDM monitoring/update (no additional crypto)
  • specification/middleware/mctp.md — Transport layer (no crypto)
  • specification/firmware_resiliency.md — NIST SP 800-193 (TBD sections, no specific algorithms yet)

12.2 Currently Implemented Operations

CategoryOperationsSpec Status
HashSHA-256, SHA-384, SHA-512Listed in SPDM hash algorithms
MACHMAC-SHA-256, HMAC-SHA-384, HMAC-SHA-512Needed for KDF / SPDM session key derivation
AEADAES-256-GCM encrypt/decryptListed in SPDM AEAD ciphers
Stream CipherAES-256-CTR encrypt/decryptNot in SPDM spec; internal use only
SignatureECDSA P-256 sign/verify, ECDSA P-384 sign/verifyListed in SPDM asymmetric algorithms

12.3 Missing — Mandatory

The SPDM spec states that hardware must support at minimum:

  • TPM_ALG_ECDSA_ECC_NIST_P384 (already implemented)
  • TPM_ALG_SHA3_384 (not implemented)
AlgorithmOp Codes NeededPriority
SHA3-384Sha3_384HashREQUIRED — mandatory minimum per SPDM spec

12.4 Missing — Listed in SPDM Spec (Optional)

These algorithms are explicitly listed in the SPDM algorithms section and may be used if supported by hardware.

12.4.1 Hash Algorithms

AlgorithmOp CodeNotes
SHA3-256Sha3_256HashListed under SPDM hash algorithms
SHA3-512Sha3_512HashListed under SPDM hash algorithms

12.4.2 Asymmetric Signature Algorithms

AlgorithmOp CodesNotes
EdDSA Ed25519Ed25519Sign, Ed25519VerifyListed under SPDM asymmetric
EdDSA Ed448Ed448Sign, Ed448VerifyListed under SPDM asymmetric

12.4.3 AEAD Ciphers

AlgorithmOp CodesNotes
AES-128-GCMAes128GcmEncrypt, Aes128GcmDecryptListed under SPDM AEAD
ChaCha20-Poly1305Chacha20Poly1305Encrypt, Chacha20Poly1305DecryptListed under SPDM AEAD

12.5 Missing — Implied by Protocol Requirements

These are not directly listed in the SPDM algorithms table but are required or implied by SPDM session establishment and attestation workflows.

AlgorithmCategoryRationale
ECDH P-256Key ExchangeSPDM KEY_EXCHANGE command; derives shared secret
ECDH P-384Key ExchangeSPDM KEY_EXCHANGE command; mandatory P-384 curve
X25519Key ExchangeSPDM key exchange with Ed25519 suite
HKDF-SHA-256KDFSPDM session key derivation (NIST SP 800-108 ref)
HKDF-SHA-384KDFSPDM session key derivation with SHA-384 suite

12.6 Summary

StatusCountAlgorithms
Implemented14 opsSHA-256/384/512, HMAC-SHA-256/384/512, AES-256-GCM enc/dec, AES-256-CTR enc/dec, ECDSA P-256/P-384 sign/verify
Mandatory gap1SHA3-384
Spec-listed optional9 opsSHA3-256, SHA3-512, Ed25519 sign/verify, Ed448 sign/verify, AES-128-GCM enc/dec, ChaCha20-Poly1305 enc/dec
Implied by sessions5+ opsECDH P-256/P-384, X25519, HKDF-SHA-256/384
Total new ops~15-20Depending on how key exchange and KDF are modeled

Notes:

  1. AES-256-CTR (currently implemented) is not in the SPDM spec. It may be useful for internal firmware encryption but is not required for protocol compliance.

  2. Key exchange (ECDH) and KDF (HKDF) may warrant their own op-code ranges rather than being crammed into existing categories. Suggested ranges:

    • Key exchange: 0x50-0x5F
    • KDF: 0x60-0x6F
    • SHA3: 0x04-0x06 (extend digest range)
  3. Ed25519/Ed448 signature ops could use 0x44-0x47 (extend ECDSA range into a general “signatures” range).

  4. The HAL layer will also need corresponding traits for any new algorithms that require hardware acceleration.


End of design review.