Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions fw/plat/uno/fw/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ cortex-m = "0.7.7"
cortex-m-rt = "0.7.5"
critical-section = "1.2.0"
darling = "0.20"
der = { default-features = false, features = [
"derive",
"oid",
], version = "0.8" }
embassy-executor = "0.10.0"
embassy-futures = "0.1.2"
embassy-sync = "0.8.0"
Expand All @@ -112,6 +116,7 @@ static_assertions = "1.1.0"
syn = { features = ["full"], version = "2" }
tock-registers = { git = "https://github.com/tock/tock.git", rev = "release-2.2" }
zerocopy = "0.8.48"
zeroize = { default-features = false, version = "1.8.1" }

[workspace.lints.rust]
future_incompatible = { level = "deny", priority = -1 }
Expand Down
2 changes: 2 additions & 0 deletions fw/plat/uno/fw/pal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ embassy-time = { workspace = true }
tock-registers = { workspace = true }

bitfield-struct = { workspace = true }
der = { workspace = true }
open-enum = { workspace = true }
zerocopy = { workspace = true }
zeroize = { workspace = true }

[features]
default = []
Expand Down
103 changes: 103 additions & 0 deletions fw/plat/uno/fw/pal/src/asn1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! ASN.1 / DER decoders for imported private keys, built on the no-alloc
//! [`der`] crate (the same dependency `fw/core/crypto/x509-chain` already uses).
//!
//! The wrapped-key import paths (`RsaUnwrap`) recover a plaintext DER private
//! key and must decode it into the raw components the PKA vault operand is
//! assembled from. This module owns only the **pure ASN.1 decode** (PKCS#8 / PKCS#1, and — as they land — SEC1 EC); the
//! platform-specific little-endian PKA operand layout lives in the
//! per-algorithm modules ([`rsa`](crate::crypto::rsa), [`ecc`](crate::crypto::ecc)) so this
//! stays a small, hardware-agnostic parsing layer.
//!
//! ## Scope and forward-looking extension points
//!
//! - **RSA non-CRT (today):** [`parse_rsa_private_key`] →
//! [`RsaPrivateKeyAsn1`]; `rsa` assembles `[d ‖ n ‖ e]`.
//! - **RSA CRT (#608):** reuses [`RsaPrivateKeyAsn1`] as-is — it already decodes
//! the CRT fields (`prime1`, `prime2`, `exponent1`, `exponent2`,
//! `coefficient`). Only the CRT operand assembly (and the derived `n1q`/`n2p`
//! PKA math) is added in `rsa`; **no change is needed here**.
//! - **ECC (#604):** add the SEC1 `ECPrivateKey` / PKCS#8 EC `PrivateKeyInfo`
//! decoders alongside the RSA ones below, plus a `parse_ec_private_key`
//! returning the scalar + curve OID; consumed by `ecc`.

use der::Decode;
use der::Sequence;
use der::asn1::Null;
use der::asn1::ObjectIdentifier;
use der::asn1::OctetStringRef;
use der::asn1::UintRef;

// ── RSA (PKCS#8 PrivateKeyInfo / PKCS#1 RSAPrivateKey) ─────────────────────

/// rsaEncryption OID (1.2.840.113549.1.1.1) — the algorithm identifier of an
/// RSA key inside a PKCS#8 `PrivateKeyInfo`.
const RSA_ENCRYPTION: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.1");

/// PKCS#8 `AlgorithmIdentifier` for RSA: the `rsaEncryption` OID with the
/// explicit `NULL` parameters required by RFC 3279 §2.3.1.
#[derive(Sequence)]
struct RsaAlgorithmIdentifier {
oid: ObjectIdentifier,
parameters: Null,
}

/// PKCS#8 `PrivateKeyInfo` (RFC 5208) whose `privateKey` OCTET STRING wraps a
/// PKCS#1 `RSAPrivateKey`.
#[derive(Sequence)]
struct RsaPrivateKeyInfo<'a> {
version: u8,
algorithm: RsaAlgorithmIdentifier,
private_key: &'a OctetStringRef,
}

/// PKCS#1 `RSAPrivateKey` (RFC 8017 A.1.2), two-prime form (version 0).
///
/// The full SEQUENCE is decoded so the `der` reader validates every field. The
/// non-CRT import path reads only `modulus` / `public_exponent` /
/// `private_exponent`; the CRT import path (#608) additionally reads the CRT
/// quintuple (`prime1`, `prime2`, `exponent1`, `exponent2`, `coefficient`),
/// which is why they are decoded and exposed here rather than skipped.
#[derive(Sequence)]
pub(crate) struct RsaPrivateKeyAsn1<'a> {
pub(crate) version: u8,
pub(crate) modulus: UintRef<'a>,
pub(crate) public_exponent: UintRef<'a>,
pub(crate) private_exponent: UintRef<'a>,
pub(crate) prime1: UintRef<'a>,
pub(crate) prime2: UintRef<'a>,
pub(crate) exponent1: UintRef<'a>,
pub(crate) exponent2: UintRef<'a>,
pub(crate) coefficient: UintRef<'a>,
}

/// Decodes a recovered RSA private key — a PKCS#8 `PrivateKeyInfo` wrapping a
/// PKCS#1 `RSAPrivateKey` — into the validated [`RsaPrivateKeyAsn1`].
Comment thread
radutta99 marked this conversation as resolved.
///
/// The `der` crate enforces canonical DER (definite minimal-length encodings,
/// no trailing bytes via `from_der`) and rejects negative / non-minimal
/// INTEGERs. On top of that this rejects key versions the two-prime SEQUENCE
/// does not model, so callers get a fully-structurally-validated key. Field
/// sizes (modulus width, exponent width) are a PKA concern and are checked by
/// the caller during operand assembly.
pub(crate) fn parse_rsa_private_key(der_bytes: &[u8]) -> Option<RsaPrivateKeyAsn1<'_>> {
// The recovered wire key is always a PKCS#8 PrivateKeyInfo (the format the
// RsaUnwrap collateral is generated in); a bare PKCS#1 RSAPrivateKey is not
// accepted.
let pki = RsaPrivateKeyInfo::from_der(der_bytes).ok()?;
// RFC 5208 §5: PrivateKeyInfo `version` is v1 (= 0). RFC 5958 adds v2
// (= 1) with public-key / attributes, which this parser does not model
// — reject rather than silently drop the extra fields.
if pki.version != 0 || pki.algorithm.oid != RSA_ENCRYPTION {
return None;
}
let key = RsaPrivateKeyAsn1::from_der(pki.private_key.as_bytes()).ok()?;
// RFC 8017 §A.1.2: `version` is 0 for two-prime keys. Version 1 (multi-prime)
// would require `otherPrimeInfos`, which our SEQUENCE does not declare.
if key.version != 0 {
return None;
}
Some(key)
}
13 changes: 5 additions & 8 deletions fw/plat/uno/fw/pal/src/crypto/ecc_det.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ use azihsm_fw_uno_drivers_upka::UpkaEccCurve;
use azihsm_fw_uno_drivers_upka::mont_operand_size;

use super::ecc::PRIME384_LE;
use super::reverse_copy;
use crate::UnoHsmPal;

// =============================================================================
Expand Down Expand Up @@ -481,8 +482,7 @@ impl UnoHsmPal {
let v_be: &[u8] = &drbg.v[..];
if ct_in_range(&v_be[..field], &drbg.n_be[..field]) {
// Candidate `k` (big-endian) in [1, n-1]; emit little-endian.
k[..field].copy_from_slice(&drbg.v[..field]);
k[..field].reverse();
reverse_copy(&mut k[..field], &drbg.v[..field]);
return Ok(());
}
self.rfc6979_reseed(io, &mut drbg).await?;
Expand Down Expand Up @@ -562,8 +562,7 @@ impl UnoHsmPal {
let v_be: &[u8] = &drbg.v[..];
if ct_in_range(&v_be[..field], &drbg.n_be[..field]) {
// Candidate k in [1, n-1]; stage little-endian and sign.
k[..field].copy_from_slice(&drbg.v[..field]);
k[..field].reverse();
reverse_copy(&mut k[..field], &drbg.v[..field]);
match self.ecc_sign_with_k(io, curve, k, digest, d, r, s).await {
Ok(()) => return Ok(()),
// Degenerate r/s — advance the DRBG and retry.
Expand Down Expand Up @@ -610,14 +609,12 @@ impl UnoHsmPal {
// msg = V ‖ 0x00 ‖ int2octets(x) ‖ bits2octets(h1).
drbg.msg[..field].copy_from_slice(&drbg.v[..field]);
drbg.msg[field] = 0x00;
drbg.msg[field + 1..field + 1 + field].copy_from_slice(&d[..field]);
drbg.msg[field + 1..field + 1 + field].reverse();
reverse_copy(&mut drbg.msg[field + 1..field + 1 + field], &d[..field]);
{
// h1 = bits2octets(digest): big-endian digest reduced mod n.
let (_, tail) = drbg.msg.split_at_mut(field + 1 + field);
let h1: &mut [u8] = &mut tail[..field];
h1.copy_from_slice(&digest[..field]);
h1.reverse();
reverse_copy(h1, &digest[..field]);
if h1[..] >= drbg.n_be[..] {
be_sub_assign(h1, &drbg.n_be);
}
Expand Down
25 changes: 25 additions & 0 deletions fw/plat/uno/fw/pal/src/crypto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,28 @@ use azihsm_fw_hsm_pal_traits::HsmCrypto;
use crate::UnoHsmPal;

impl HsmCrypto for UnoHsmPal {}

/// Copies `src` into `dst[..src.len()]` in reverse byte order.
///
/// The crypto drivers cross two endian conventions constantly: PKA operands
/// and the vault layout are little-endian, while DER/SEC1 integers and the
/// RFC 6979 DRBG work big-endian. Converting between them is always "copy
/// these bytes backwards", so the loop lives here once instead of being
/// rewritten per call site.
///
/// `src` and `dst` must not overlap; use `copy_within` plus an in-place
/// `reverse` when they can. Bytes of `dst` beyond `src.len()` are left
/// untouched, so a caller needing a fixed-width zero-padded field must clear
/// them itself.
///
/// # Panics
///
/// Debug builds assert that `dst` is at least as long as `src`; a shorter
/// `dst` silently truncates, which is a caller bug.
#[inline]
pub(crate) fn reverse_copy(dst: &mut [u8], src: &[u8]) {
debug_assert!(dst.len() >= src.len());
for (d, s) in dst.iter_mut().zip(src.iter().rev()) {
*d = *s;
}
}
Loading
Loading