Skip to content

uno: RsaUnwrap bring-up — OAEP endianness fix + non-CRT RSA private-key import - #602

Open
radutta99 wants to merge 12 commits into
mainfrom
user/radutta/rsa-unwrap-bringup
Open

uno: RsaUnwrap bring-up — OAEP endianness fix + non-CRT RSA private-key import#602
radutta99 wants to merge 12 commits into
mainfrom
user/radutta/rsa-unwrap-bringup

Conversation

@radutta99

Copy link
Copy Markdown
Contributor

Summary

Brings up the RSA paths of RsaUnwrap on the uno HSM firmware. Three changes:

  1. OAEP endianness fixrsa_oaep_decrypt now flips the mod_exp_priv result LE→BE before the RFC 8017 EME-OAEP decode. The uno PKA is little-endian native (LE ciphertext in, LE result out), but the OAEP encoded message 0x00 ‖ maskedSeed ‖ maskedDB is big-endian; the std PAL does the same flip around OpenSSL. Without it, every RsaUnwrap failed with RsaDecryptFailed (0x08700012). This fixes the AES-key unwrap path.

  2. Non-CRT RSA private-key import — implements rsa_priv_der_to_vault. Adds a small no_std DER parser (der_tlv, der_int, parse_rsa_priv_der) handling PKCS#8 PrivateKeyInfo and bare PKCS#1 RSAPrivateKey. The PKCS#8 AlgorithmIdentifier is validated to be rsaEncryption (OID 1.2.840.113549.1.1.1 + NULL params), mirroring mcr-hsm's Asn1RsaEncryptionInfo. The parsed n/e/d are assembled into the PKA vault operand [d(k) ‖ n(k) ‖ e(4)] little-endian — matching mcr-hsm's RsaPrivKey::to_pka_bytes and what rsa_priv_pub_key reads back. The stack scratch holding the private exponent is scrubbed with zeroize. CRT import returns UnsupportedCmd (deferred — needs PKA-derived n1q/n2p).

  3. OAEP oversized-output error contractrsa_oaep_decrypt now returns RsaInvalidKeyLength (not InvalidArg) when the recovered message exceeds the caller's output buffer, matching the std PAL. The shared key-unwrap path maps this to RsaUnwrapInvalidKek, so an oversized recovered KEK is now reported correctly.

Testing

Hardware (Manticore EVB):

  • rsa_unwrap_generated_key: 12/17 (was 9/17)
  • rsa_unwrap_smoke: 3/5 (was 2/5) — oversized_kek_smoke now passes
  • get_unwrapping_key: 7/7 (unchanged)

Emu: precheck --nextest --package azihsm_ddi_mbor_types --features emu --filter smoke --profile ci-emu-smoke → 94/94 passed.

fmt / clippy (-D warnings, ms-nightly) / taplo all clean.

Deferred to follow-up PRs

The remaining rsa_unwrap failures each map cleanly to one of:

  • CRT RSA import — needs PKA-derived n1q/n2p (mcr-hsm computes these separately).
  • uno ECC key import — blocks ecc_key_smoke, ecc_keys_with_key_tag.
  • OpenKey-by-tag — a full SDK-wide DDI (op 1015 has no handler); needs a shared PAL-trait + vault change (the uno vault Entry already carries a session_or_tag field).

Base

Stacked on #601 (user/radutta/get-unwrapping-key), which is not yet merged — this PR targets that branch, not main.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Brings up RSA unwrap support on uno firmware by aligning RSA-OAEP decode with the PKA’s little-endian behavior, adding non-CRT RSA private-key import from DER into the uno vault layout, and tightening an OAEP error contract to match the std PAL.

Changes:

  • Reverse the mod_exp_priv result (LE→BE) before OAEP unpadding to fix RSA-OAEP decrypt on uno.
  • Implement rsa_priv_der_to_vault for non-CRT RSA keys via a minimal PKCS#8/PKCS#1 DER parser and vault operand assembly, plus stack scratch scrubbing.
  • Return RsaInvalidKeyLength (not InvalidArg) when OAEP plaintext exceeds the caller buffer; add zeroize dependency for firmware PAL.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
fw/plat/uno/fw/pal/src/crypto/rsa.rs Adds DER parsing + non-CRT RSA private-key import; fixes OAEP endianness and output-length error contract.
fw/plat/uno/fw/pal/Cargo.toml Adds zeroize as a dependency for the uno PAL crate.
fw/plat/uno/fw/Cargo.toml Adds zeroize (no-default-features) to the uno firmware workspace dependencies.

Comment thread fw/plat/uno/fw/pal/src/crypto/rsa.rs Outdated
Comment thread fw/plat/uno/fw/pal/src/crypto/rsa.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

fw/plat/uno/fw/pal/src/crypto/rsa.rs:175

  • In the PKCS#8 path, the code descends into the privateKey OCTET STRING but does not verify that the inner RSAPrivateKey SEQUENCE consumes the entire OCTET STRING. Without that check, extra bytes inside the OCTET STRING are silently accepted.
        let (ot, oct_start, _ol, _on) = der_tlv(der, after_alg)?;
        if ot != 0x04 {
            return None;
        }
        let (st, inner_start, _sl, _sn) = der_tlv(der, oct_start)?;

Comment thread fw/plat/uno/fw/pal/src/crypto/rsa.rs
Comment thread fw/plat/uno/fw/pal/src/crypto/rsa.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment thread fw/plat/uno/fw/pal/src/crypto/rsa.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

fw/plat/uno/fw/pal/src/crypto/rsa.rs:332

  • buf[vault_len..].zeroize() scrubs a plain &mut [u8] using the zeroize crate, but this repo has a DMA-specific wipe primitive (DmaBuf::zeroize) that uses per-byte volatile writes + a compiler fence (fw/pal/traits/src/alloc.rs:148-160). For DMA-backed buffers, prefer wiping via DmaBuf::zeroize to match the established secret-scrub pattern.
        buf[vault_len..].zeroize();

Comment thread fw/plat/uno/fw/pal/src/crypto/rsa.rs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread fw/plat/uno/fw/pal/src/crypto/rsa.rs Outdated
Comment thread fw/plat/uno/fw/pal/src/crypto/rsa.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

fw/plat/uno/fw/pal/src/crypto/rsa.rs:199

  • parse_rsa_priv_der currently only parses the PKCS#1 version, n, e, d integers and then returns success without confirming that the mandatory remaining PKCS#1 fields (p, q, dp, dq, qinv) are present and that the key SEQUENCE is fully consumed. This makes the parser accept malformed/truncated RSA keys (or extra trailing bytes inside the SEQUENCE) even though the comment says those fields “follow”, and it weakens input validation for untrusted recovered key material.
    // n, e, d in order (p, q, dp, dq, qinv follow but are unused for non-CRT).
    let (ns, nl, after_n) = der_int(der, p)?;
    let (es, el, after_e) = der_int(der, after_n)?;
    let (ds, dl, _after_d) = der_int(der, after_e)?;
    Some(((ns, nl), (es, el), (ds, dl)))

uno's rsa_oaep_decrypt fed the LE ciphertext to the LE-native mod_exp_priv
(correct) but then ran the RFC 8017 EME-OAEP decode directly on the LE result,
checking em[0] (the LSB) for the 0x00 leading byte and MGF1-unmasking
byte-reversed data -> RsaDecryptFailed for every real unwrap. Reverse the
mod-exp result LE->BE first (matching the std PAL's flip around OpenSSL) so the
OAEP decode sees the big-endian EM = 0x00 || maskedSeed || maskedDB.
Implement rsa_priv_der_to_vault on the uno PAL so RsaUnwrap can import a recovered RSA private key. Adds a no_std DER parser (der_tlv, der_int, parse_rsa_priv_der) handling PKCS#8 PrivateKeyInfo and bare PKCS#1 RSAPrivateKey. The PKCS#8 algorithm is validated to be rsaEncryption (OID 1.2.840.113549.1.1.1 + NULL params), mirroring mcr-hsm Asn1RsaEncryptionInfo. The parsed n/e/d are assembled into the PKA vault operand [d(k) || n(k) || e(4)] little-endian, matching mcr-hsm RsaPrivKey::to_pka_bytes and what rsa_priv_pub_key reads back.

The stack scratch that holds the private exponent is scrubbed with zeroize (volatile writes, not elidable). Non-CRT only; CRT import returns UnsupportedCmd (needs PKA-derived n1q/n2p, deferred).

HW-verified: unblocks rsa_unwrap_generated_key rsa_key, rsa_key_sizes, and incorrect_input_key_usage (12/17 in the suite now pass).
rsa_oaep_decrypt returned InvalidArg when the recovered message was larger than the caller output buffer. The std PAL returns RsaInvalidKeyLength here, and the shared key-unwrap path maps that to RsaUnwrapInvalidKek. Align uno with that contract so an oversized recovered KEK surfaces as RsaUnwrapInvalidKek.

HW-verified: rsa_unwrap_smoke oversized_kek_smoke now passes (smoke 3/5; remaining 2 need ECC/CRT import).
Address PR review comments: der_int now rejects negative DER INTEGERs (MSB set on the first content byte with no 0x00 sign pad) and non-minimal encodings, since it parses untrusted recovered key material; the integer zero (PKCS#8/PKCS#1 version field) is still accepted. rsa_oaep_decrypt now scrubs the secret-bearing em DMA scratch via DmaBuf::zeroize on every exit path (including ?-propagated errors) by running the fallible OAEP decode in an inner block.
Address PR review: rsa_priv_der_to_vault now scrubs the tail of buf after rewriting the vault operand (the leftover source DER still holds secret CRT components p/q/dp/dq/qinv; the scoped DMA buffer is reused without automatic wiping). parse_rsa_priv_der now rejects trailing garbage: the outer SEQUENCE must span the entire input (der.len()), and the PKCS#8 privateKey OCTET STRING must wrap exactly the inner RSAPrivateKey SEQUENCE (material is exact-length per unwrap_key returning payload_buf[..payload_len]).
Address PR review: der_tlv now rejects non-minimal long-form definite lengths. The most-significant length byte must be non-zero (no leading-zero padding), and long form must not encode a length that fits in short form (len >= 0x80). This tightens the parser against malformed untrusted key material, matching strict DER decoders.
Address PR review: rsa_priv_der_to_vault returned early on crt==true, DER parse failure, and size validation without wiping buf, leaving the recovered plaintext RSA private-key DER resident in reused DMA SRAM. Now zeroize buf on every early-error return. The success path is unchanged (it overwrites buf[..vault_len] with the vault operand and scrubs buf[vault_len..]).
…tent

Address PR review: (1) rsa_priv_der_to_vault now rejects (scrub + InvalidArg) when the vault operand length (2*modulus_len+4) exceeds buf.len(), which a malformed/truncated PKCS#1 (e.g. a very short d) could trigger, avoiding an out-of-bounds panic on the in-place slices. (2) parse_rsa_priv_der now requires the AlgorithmIdentifier NULL parameters to consume the rest of the algorithm SEQUENCE (null_next == after_alg), rejecting extra trailing fields.
@radutta99
radutta99 force-pushed the user/radutta/rsa-unwrap-bringup branch from 0f88cf9 to 4c437bd Compare July 30, 2026 01:46
@radutta99
radutta99 changed the base branch from user/radutta/get-unwrapping-key to main July 30, 2026 01:47
@radutta99
radutta99 requested a review from Copilot July 30, 2026 02:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

fw/plat/uno/fw/pal/src/crypto/rsa.rs:640

  • rsa_oaep_decrypt now correctly flips the mod_exp_priv result from wire little-endian to big-endian before interpreting EM (RFC 8017). However, the same mod_exp_priv/mod_exp_pub trait contract is wire little-endian for all RSA primitives (see fw/pal/traits/src/crypto/rsa.rs:214-219), while the Uno implementations of PKCS#1 v1.5 decrypt/verify/sign currently build/validate EM as if the mod-exp inputs/outputs were big-endian (e.g. rsa_pkcs1_decrypt checks em[0] == 0x00 immediately after mod_exp_priv, and rsa_pkcs1_sign constructs EM = 0x00||0x01||... then feeds it directly into mod_exp_priv). If/when those entry points are exercised on Uno, they will fail unless they perform the same LE↔BE reversals around modular exponentiation that OAEP now does.
            // `mod_exp_priv` is little-endian (PKA-native): both the ciphertext
            // operand and the `em` result are LE wire form. The RSA-OAEP decode
            // below (RFC 8017 EME-OAEP) operates on the big-endian encoded
            // message `EM = 0x00 ‖ maskedSeed ‖ maskedDB`, so flip the result
            // LE→BE first (the std PAL does the same flip around OpenSSL).
            em[..k].reverse();

@radutta99
radutta99 requested a review from vsonims July 30, 2026 18:13
parse_rsa_priv_der: capture the AlgorithmIdentifier fields from the single
der_tlv(p) call instead of re-parsing position p a second time.

write_le: write the reversed big-endian bytes into the low half, then zero only
the remaining high bytes, instead of zeroing the whole buffer and overwriting
the low bytes.

No behaviour change (der_tlv is pure; write_le still fully covers dst). HW:
rsa_unwrap 15/22 on the Manticore EVB, identical to before the change (the
RSA-import paths rsa_key / rsa_key_sizes / incorrect_der_len all pass).
Copilot AI review requested due to automatic review settings July 30, 2026 18:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

fw/plat/uno/fw/pal/src/crypto/rsa.rs:326

  • rsa_priv_der_to_vault validates exponent length (el <= 4) but does not reject a zero public exponent. Since this parses untrusted recovered key material, accepting e = 0 would import an invalid RSA key and can lead to incorrect behavior (and a trivially broken public key if re-exported via rsa_priv_pub_key). Consider rejecting an all-zero exponent during the size-validation step.
        if !matches!(modulus_len, 256 | 384 | 512) || el > 4 || dl > modulus_len {
            buf.zeroize();
            return Err(HsmError::InvalidArg);
        }

Comment thread fw/plat/uno/fw/pal/src/crypto/rsa.rs Outdated
Replace the hand-rolled DER TLV/INTEGER parser (der_tlv, der_int,
parse_rsa_priv_der) with declarative der 0.8 #[derive(Sequence)] structs
(PKCS#8 PrivateKeyInfo + PKCS#1 RSAPrivateKey). The der crate's canonical reader
enforces minimal-length encodings, rejects trailing bytes and negative /
non-minimal INTEGERs, and validates the full RSAPrivateKey structure natively,
so the security-critical hand-rolled validation is no longer needed.

der 0.8 is already linked no-alloc in fw/core/crypto/x509-chain; this uses the
same dependency (default-features = false, features = ["derive", "oid"]). The
borrowed UintRef / OctetStringRef decoders keep the parser alloc-free.

Addresses review: use an ASN.1 crate instead of hand-parsing DER.

HW: rsa_unwrap 15/22 on the Manticore EVB, identical to the hand-rolled parser
(rsa_key / rsa_key_sizes / incorrect_der_len pass). +3.8 KiB .text.
Copilot AI review requested due to automatic review settings July 30, 2026 23:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

fw/plat/uno/fw/pal/src/crypto/rsa.rs:143

  • rsa_der_to_vault_operand accepts PKCS#8 / PKCS#1 keys without checking the version fields. For untrusted recovered key material, it’s better to reject non-zero versions (PKCS#8 PrivateKeyInfo.version must be 0; PKCS#1 RSAPrivateKey.version must be 0 for two-prime keys). Without this, malformed keys with unexpected version values could be accepted despite being non-conformant.
    if let Ok(pki) = RsaPrivateKeyInfo::from_der(der_bytes) {
        if pki.algorithm.algorithm != RSA_ENCRYPTION {
            return None;
        }
        let inner = RsaPrivateKeyDer::from_der(pki.private_key.as_bytes()).ok()?;

Move the der-based PKCS#8 / PKCS#1 RSAPrivateKey decoders out of crypto/rsa.rs
into a dedicated hardware-agnostic module pal/src/asn1.rs (sibling to cert.rs).
crypto/ stays reserved for hardware-backed trait impls that call driver modules;
asn1.rs touches no hardware. rsa.rs now decodes via asn1::parse_rsa_private_key
and only owns the PKA little-endian operand assembly.

parse_rsa_private_key returns the full decoded RSAPrivateKey (the CRT quintuple
is exposed for the future CRT import path) and validates the PKCS#8 / PKCS#1
version fields, rejecting RFC 5958 v2 and multi-prime (v1) keys the two-prime
SEQUENCE does not model.

HW: rsa_unwrap 15/22 on the Manticore EVB, identical to before the move.
Copilot AI review requested due to automatic review settings July 31, 2026 01:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

fw/plat/uno/fw/pal/src/crypto/rsa.rs:84

  • This intra-doc link points at super::asn1, but the ASN.1 module is declared at the crate root (crate::asn1). If rustdoc warnings are denied, this will fail doc builds; even without that, the link is broken.
/// [`parse_rsa_private_key`](super::asn1::parse_rsa_private_key).

Comment thread fw/plat/uno/fw/pal/src/crypto/rsa.rs
CI runs `taplo fmt --check`, whose config enables reorder_inline_tables and
array wrapping. The `der` dependency must have its inline-table keys alphabetized
(default-features, features, version) and the features array expanded. Fixes the
CI fmt failure introduced when the der dependency was added.
Copilot AI review requested due to automatic review settings July 31, 2026 01:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

fw/plat/uno/fw/pal/src/crypto/rsa.rs:84

  • The intra-doc link for parse_rsa_private_key points to super::asn1::..., but asn1 is a crate-root module (crate::asn1). As written, this link is broken and can trigger rustdoc warnings (or deny-by-lint if enabled).
/// modulus. Structural validation (versions, algorithm OID) is done upstream by
/// [`parse_rsa_private_key`](super::asn1::parse_rsa_private_key).

fw/plat/uno/fw/pal/Cargo.toml:52

  • der is the only dependency in this crate pinned with an explicit version instead of using the firmware workspace dependency set (everything else is workspace = true). This makes it easier for crates in the same workspace to drift onto multiple der versions/features and increases maintenance + binary size risk. Consider adding der to fw/plat/uno/fw/Cargo.toml’s [workspace.dependencies] and switching this entry to der = { workspace = true, ... }.
der = { default-features = false, features = [
  "derive",
  "oid",
], version = "0.8" }

@radutta99
radutta99 requested a review from vsonims July 31, 2026 01:33
radutta99 added a commit that referenced this pull request Jul 31, 2026
Adapt the ECC private-key import to #602's der-based parsing. Add
parse_ec_private_key to pal::asn1 (PKCS#8 PrivateKeyInfo + SEC1 ECPrivateKey
der 0.8 #[derive(Sequence)] decoders, including the optional [0] parameters /
[1] publicKey context fields) returning (curve, scalar), and replace ecc.rs's
hand-rolled der_tlv-based parse_ec_priv_der with it.

write_le is now a shared pub(super) helper in rsa. This drops ecc.rs's
dependency on the der_tlv / write_le helpers that #602's der migration removed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants