uno: RsaUnwrap bring-up — OAEP endianness fix + non-CRT RSA private-key import - #602
uno: RsaUnwrap bring-up — OAEP endianness fix + non-CRT RSA private-key import#602radutta99 wants to merge 12 commits into
Conversation
There was a problem hiding this comment.
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_privresult (LE→BE) before OAEP unpadding to fix RSA-OAEP decrypt on uno. - Implement
rsa_priv_der_to_vaultfor non-CRT RSA keys via a minimal PKCS#8/PKCS#1 DER parser and vault operand assembly, plus stack scratch scrubbing. - Return
RsaInvalidKeyLength(notInvalidArg) when OAEP plaintext exceeds the caller buffer; addzeroizedependency 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. |
There was a problem hiding this comment.
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
privateKeyOCTET STRING but does not verify that the innerRSAPrivateKeySEQUENCE 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)?;
There was a problem hiding this comment.
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 thezeroizecrate, 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 viaDmaBuf::zeroizeto match the established secret-scrub pattern.
buf[vault_len..].zeroize();
There was a problem hiding this comment.
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_dercurrently only parses the PKCS#1version,n,e,dintegers 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.
0f88cf9 to
4c437bd
Compare
There was a problem hiding this comment.
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_decryptnow correctly flips themod_exp_privresult from wire little-endian to big-endian before interpretingEM(RFC 8017). However, the samemod_exp_priv/mod_exp_pubtrait contract is wire little-endian for all RSA primitives (seefw/pal/traits/src/crypto/rsa.rs:214-219), while the Uno implementations of PKCS#1 v1.5 decrypt/verify/sign currently build/validateEMas if the mod-exp inputs/outputs were big-endian (e.g.rsa_pkcs1_decryptchecksem[0] == 0x00immediately aftermod_exp_priv, andrsa_pkcs1_signconstructsEM = 0x00||0x01||...then feeds it directly intomod_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();
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).
There was a problem hiding this comment.
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_vaultvalidates exponent length (el <= 4) but does not reject a zero public exponent. Since this parses untrusted recovered key material, acceptinge = 0would import an invalid RSA key and can lead to incorrect behavior (and a trivially broken public key if re-exported viarsa_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);
}
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.
There was a problem hiding this comment.
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_operandaccepts PKCS#8 / PKCS#1 keys without checking theversionfields. For untrusted recovered key material, it’s better to reject non-zero versions (PKCS#8PrivateKeyInfo.versionmust be 0; PKCS#1RSAPrivateKey.versionmust 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.
There was a problem hiding this comment.
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).
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.
There was a problem hiding this comment.
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_keypoints tosuper::asn1::..., butasn1is 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
deris the only dependency in this crate pinned with an explicit version instead of using the firmware workspace dependency set (everything else isworkspace = true). This makes it easier for crates in the same workspace to drift onto multiplederversions/features and increases maintenance + binary size risk. Consider addingdertofw/plat/uno/fw/Cargo.toml’s[workspace.dependencies]and switching this entry toder = { workspace = true, ... }.
der = { default-features = false, features = [
"derive",
"oid",
], version = "0.8" }
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.
Summary
Brings up the RSA paths of
RsaUnwrapon the uno HSM firmware. Three changes:OAEP endianness fix —
rsa_oaep_decryptnow flips themod_exp_privresult 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 message0x00 ‖ maskedSeed ‖ maskedDBis big-endian; the std PAL does the same flip around OpenSSL. Without it, every RsaUnwrap failed withRsaDecryptFailed (0x08700012). This fixes the AES-key unwrap path.Non-CRT RSA private-key import — implements
rsa_priv_der_to_vault. Adds a smallno_stdDER parser (der_tlv,der_int,parse_rsa_priv_der) handling PKCS#8PrivateKeyInfoand bare PKCS#1RSAPrivateKey. The PKCS#8AlgorithmIdentifieris validated to bersaEncryption(OID 1.2.840.113549.1.1.1 + NULL params), mirroring mcr-hsm'sAsn1RsaEncryptionInfo. The parsedn/e/dare assembled into the PKA vault operand[d(k) ‖ n(k) ‖ e(4)]little-endian — matching mcr-hsm'sRsaPrivKey::to_pka_bytesand whatrsa_priv_pub_keyreads back. The stack scratch holding the private exponent is scrubbed withzeroize. CRT import returnsUnsupportedCmd(deferred — needs PKA-derivedn1q/n2p).OAEP oversized-output error contract —
rsa_oaep_decryptnow returnsRsaInvalidKeyLength(notInvalidArg) when the recovered message exceeds the caller's output buffer, matching the std PAL. The shared key-unwrap path maps this toRsaUnwrapInvalidKek, 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_smokenow passesget_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_unwrapfailures each map cleanly to one of:n1q/n2p(mcr-hsm computes these separately).ecc_key_smoke,ecc_keys_with_key_tag.Entryalready carries asession_or_tagfield).Base
Stacked on #601 (
user/radutta/get-unwrapping-key), which is not yet merged — this PR targets that branch, notmain.