Skip to content

Commit e093107

Browse files
authored
[doc] Byte orientation documentation for prod debug unlock fuses (#3635)
1 parent 373728e commit e093107

7 files changed

Lines changed: 154 additions & 28 deletions

File tree

auth-manifest/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ The Caliptra SOC manifest has two main components: [Preamble](#preamble) and [Im
99
Those signatures are also stored in the Preamble.
1010
The Caliptra firmware's ECC and PQC private keys endorse the manifest's public keys, and these endorsements (signatures) are part of the Preamble as well.
1111

12-
*Note: All fields are little endian unless specified*
12+
*Note: Do not treat the entire manifest as little endian. Scalar `u32` fields use little-endian layout. ECC key and signature fields are stored as big-endian `u32` words. Raw byte-array fields, such as image hashes, use the exact byte sequence produced by standard tools like OpenSSL unless a field description explicitly says otherwise.*
1313

1414
| Field | Size (bytes) | Description |
1515
| ---------------------------------- | ------------ | ----------- |

hw-model/src/lib.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,8 +251,14 @@ pub struct InitParams<'a> {
251251

252252
pub otp_direct_access_cmd_reg_offset: u32,
253253

254-
// Keypairs for production debug unlock levels, from low to high
255-
// ECC384 and MLDSA87 keypairs (in hardware format i.e. little-endian)
254+
// Keypairs for production debug unlock levels, from low to high.
255+
// These byte slices must match the exact bytes hashed into the production debug unlock
256+
// public-key-hash fuses and sent in AUTH_DEBUG_UNLOCK_TOKEN.
257+
// ECC384: 96 bytes in mailbox byte order, i.e. X||Y with each 4-byte group byte-reversed
258+
// relative to the standard big-endian coordinate bytes. Example: if the raw ECC bytes begin
259+
// `ab cd ef 01 23 45 67 89`, store them here as `01 ef cd ab 89 67 45 23`.
260+
// MLDSA87: 2592 raw public-key bytes copied exactly as produced by the MLDSA implementation.
261+
// Example: if the MLDSA bytes begin `72 c0 f1 3b 7d 93 7e 22`, store them here unchanged.
256262
pub prod_dbg_unlock_keypairs: Vec<(&'a [u8; 96], &'a [u8; 2592])>,
257263

258264
// Whether or not to set the debug_intent signal.

rom/dev/README.md

Lines changed: 127 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -366,18 +366,18 @@ The following flows are conducted when the ROM is operating in the production mo
366366
| Unlock Level | 1 | Debug unlock Level (Number 1-8). |
367367
| Reserved | 3 | Reserved field. |
368368
| Challenge | 48 | Random number sent in `AUTH_DEBUG_UNLOCK_CHALLENGE` mailbox command payload. |
369-
| ECC Public Key | 96 | ECC P-384 public key used to verify the Message Signature <br> **X-Coordinate:** Public Key X-Coordinate (48 bytes) <br> **Y-Coordinate:** Public Key Y-Coordinate (48 bytes). See [Byte order of cryptographic fields](../runtime/README.md#byte-order-of-cryptographic-fields). |
370-
| MLDSA Public Key | 2592 | MLDSA-87 public key used to verify the Message Signature. See [Byte order of cryptographic fields](../runtime/README.md#byte-order-of-cryptographic-fields). |
371-
| ECC Signature | 96 | ECC P-384 signature of the Message hashed using SHA2-384. <br> **R-Coordinate:** Random Point (48 bytes) <br> **S-Coordinate:** Proof (48 bytes). See [Byte order of cryptographic fields](../runtime/README.md#byte-order-of-cryptographic-fields). |
372-
| MLDSA Signature | 4628 | MLDSA-87 signature of the Message hashed using SHA2-512 (4627 bytes + 1 Reserved byte). See [Byte order of cryptographic fields](../runtime/README.md#byte-order-of-cryptographic-fields). |
369+
| ECC Public Key | 96 | ECC P-384 public key used to verify the Message Signature <br> **X-Coordinate:** Public Key X-Coordinate (48 bytes) <br> **Y-Coordinate:** Public Key Y-Coordinate (48 bytes). See [Byte order of cryptographic fields](../../runtime/README.md#byte-order-of-cryptographic-fields). |
370+
| MLDSA Public Key | 2592 | MLDSA-87 public key used to verify the Message Signature. See [Byte order of cryptographic fields](../../runtime/README.md#byte-order-of-cryptographic-fields). |
371+
| ECC Signature | 96 | ECC P-384 signature of the Message hashed using SHA2-384. <br> **R-Coordinate:** Random Point (48 bytes) <br> **S-Coordinate:** Proof (48 bytes). See [Byte order of cryptographic fields](../../runtime/README.md#byte-order-of-cryptographic-fields). |
372+
| MLDSA Signature | 4628 | MLDSA-87 signature of the Message hashed using SHA2-512 (4627 bytes + 1 Reserved byte). See [Byte order of cryptographic fields](../../runtime/README.md#byte-order-of-cryptographic-fields). |
373373

374374
7. On receiving this payload, ROM performs the following validations:
375375
- Ensures the value in the `Length` field matches the size of the payload.
376376
- Confirms that the `Debug unlock level` is not zero and does not exceed the value specified in the `SS_NUM_OF_PROD_DEBUG_UNLOCK_AUTH_PK_HASHES` register.
377377
- Calculates the address of the public key hash fuse as follows: <br>
378378
**SS_PROD_DEBUG_UNLOCK_AUTH_PK_HASH_REG_BANK_OFFSET register value + ( (Debug Unlock Level - 1) * SHA2-384 hash size (48 bytes) )**
379379
- Retrieves the SHA2-384 hash (48 bytes) from the calculated address using DMA assist.
380-
- Computes the SHA2-384 hash of the message formed by concatenating the ECC and MLDSA public keys in the payload.
380+
- Computes the SHA2-384 hash of the message formed by concatenating the ECC and MLDSA public keys in the payload. See [Production debug unlock public key hashes: byte ordering](#production-debug-unlock-public-key-hashes-byte-ordering) for the exact byte order and fuse programming details.
381381
- Compares the retrieved and computed hashes. It the comparison fails, the ROM blocks the debug unlock request by setting the registers outlined in step 3.
382382
- Upon hash comparison failure, the ROM exits the payload validation flow and completes the mailbox command.
383383

@@ -1296,7 +1296,7 @@ and are also **not** subject to dword reversal.
12961296
12971297
For a detailed description of byte ordering conventions for all mailbox cryptographic fields
12981298
(including ECC, ML-DSA, and SHA digest fields with OpenSSL examples), see the
1299-
[Byte order of cryptographic fields](../runtime/README.md#byte-order-of-cryptographic-fields)
1299+
[Byte order of cryptographic fields](../../runtime/README.md#byte-order-of-cryptographic-fields)
13001300
section in the Runtime README.
13011301
13021302
### Computing public key hashes: step-by-step example
@@ -1835,6 +1835,127 @@ FUSE_VENDOR_PK_HASH. See
18351835
[Public key hash byte ordering](#public-key-hash-byte-ordering-dword-reversal)
18361836
for details and worked examples.
18371837

1838+
##### Production debug unlock public key hashes: byte ordering
1839+
1840+
The production debug unlock flow uses SHA2-384 hashes of the concatenated
1841+
ECC and MLDSA public keys to authenticate debug unlock tokens. These hashes
1842+
are stored in the MCI register bank at addresses computed from
1843+
`SS_PROD_DEBUG_UNLOCK_AUTH_PK_HASH_REG_BANK_OFFSET`.
1844+
1845+
**Hash input construction:**
1846+
1847+
The hash is SHA2-384 over the raw mailbox wire bytes of the concatenated
1848+
ECC and MLDSA public keys from the `AUTH_DEBUG_UNLOCK_TOKEN` payload.
1849+
The mailbox wire format for each key type is:
1850+
1851+
- **ECC public key (96 bytes)**: Each 4-byte group of the X and Y
1852+
coordinates is **dword-reversed** from the standard OpenSSL output.
1853+
1854+
```
1855+
openssl ec output: AB CD EF 01 23 45 67 89 ... (X, 48 bytes)
1856+
11 22 33 44 55 66 77 88 ... (Y, 48 bytes)
1857+
1858+
Hash input (= mailbox wire bytes):
1859+
01 EF CD AB 89 67 45 23 ... (X, dword-reversed)
1860+
44 33 22 11 88 77 66 55 ... (Y, dword-reversed)
1861+
```
1862+
1863+
- **MLDSA public key (2592 bytes)**: The native MLDSA key bytes are
1864+
used **as-is** — no conversion.
1865+
1866+
```
1867+
MLDSA keygen output: 72 C0 F1 3B 7D 93 7E 22 ...
1868+
1869+
Hash input (= mailbox wire bytes):
1870+
72 C0 F1 3B 7D 93 7E 22 ... (identical)
1871+
```
1872+
1873+
To compute the same hash offline for fuse provisioning, reconstruct the
1874+
mailbox wire bytes: dword-reverse the ECC coordinates, keep MLDSA native,
1875+
concatenate, and hash:
1876+
1877+
```
1878+
ECC dword-reversed: 01 EF CD AB 89 67 45 23 ... (96 bytes)
1879+
MLDSA native: 72 C0 F1 3B 7D 93 7E 22 ... (2592 bytes)
1880+
1881+
hash_input = ECC_dword_reversed || MLDSA_native_bytes (2688 bytes)
1882+
1883+
$ openssl dgst -sha384 -binary combined.bin | xxd -p -c 48
1884+
→ 3f7a2b91c4e8d0f5...
1885+
```
1886+
1887+
**Provisioning: OpenSSL example**
1888+
1889+
To prepare `combined_keys.bin`, dword-reverse the ECC raw coordinates
1890+
and concatenate with the native MLDSA key bytes. Then compute the hash:
1891+
1892+
```
1893+
$ openssl dgst -sha384 -binary combined_keys.bin | xxd -p -c 48
1894+
3f7a2b91c4e8d0f5a1b2c3d4e5f60718293a4b5c6d7e8f90a0b1c2d3e4f5061728394a5b6c
1895+
```
1896+
1897+
Map the digest output to fuse register words — each 4-byte group becomes
1898+
one `u32` fuse word (same convention as all other SHA digest fuses):
1899+
1900+
```
1901+
openssl output: 3f 7a 2b 91 c4 e8 d0 f5 a1 b2 c3 d4 ...
1902+
~~~~~~~~~~~ ~~~~~~~~~~~ ~~~~~~~~~~~
1903+
Fuse word[0]: 0x3F7A2B91 [1]: 0xC4E8D0F5 [2]: 0xA1B2C3D4 ...
1904+
```
1905+
1906+
Write these 12 words to the MCI register bank at offset:
1907+
1908+
```
1909+
SS_PROD_DEBUG_UNLOCK_AUTH_PK_HASH_REG_BANK_OFFSET + ((level - 1) * 48)
1910+
```
1911+
1912+
**Mailbox payload: preparing fields from OpenSSL output**
1913+
1914+
The `AUTH_DEBUG_UNLOCK_TOKEN` mailbox command fields use the byte order
1915+
conventions described in
1916+
[Byte order of cryptographic fields](../../runtime/README.md#byte-order-of-cryptographic-fields).
1917+
The table below summarizes how to convert OpenSSL tool output into the
1918+
mailbox payload bytes for each field:
1919+
1920+
- **ECC P-384 public key (big-endian words)**: Extract the raw X and Y
1921+
coordinates (48 bytes each) from the PEM key, then **dword-reverse**
1922+
each 4-byte group before writing to the mailbox.
1923+
1924+
```
1925+
# Extract raw X||Y from PEM (96 bytes, big-endian):
1926+
$ openssl ec -pubin -in key.pem -outform DER 2>/dev/null \
1927+
| tail -c 96 | xxd -p -c 48
1928+
1929+
OpenSSL raw bytes: AB CD EF 01 23 45 67 89 ... (X, 48 bytes)
1930+
11 22 33 44 55 66 77 88 ... (Y, 48 bytes)
1931+
1932+
Mailbox bytes: 01 EF CD AB 89 67 45 23 ... (X, dword-reversed)
1933+
44 33 22 11 88 77 66 55 ... (Y, dword-reversed)
1934+
```
1935+
1936+
- **MLDSA-87 public key (little-endian words)**: Copy the raw key bytes
1937+
produced by an MLDSA implementation (e.g. OpenSSL 3.5+, `fips204` crate)
1938+
**directly** into the mailbox — no conversion needed.
1939+
1940+
```
1941+
MLDSA key bytes: 72 C0 F1 3B 7D 93 7E 22 ... (2592 bytes)
1942+
Mailbox bytes: 72 C0 F1 3B 7D 93 7E 22 ... (identical)
1943+
```
1944+
1945+
- **ECC P-384 signature (big-endian words)**: Same treatment as the public
1946+
key — dword-reverse each 4-byte group of the R and S coordinates.
1947+
1948+
- **MLDSA-87 signature (little-endian words)**: Copy raw signature bytes
1949+
directly — no conversion needed. The trailing byte (byte 4628) is
1950+
reserved and should be zero.
1951+
1952+
**Note:** The hash used for fuse provisioning is computed over the exact
1953+
same bytes that appear on the mailbox wire. There is no additional
1954+
transformation — the SHA accelerator's internal endianness handling is
1955+
transparent and produces `SHA384(wire_bytes)`. Therefore the provisioning
1956+
hash and the runtime verification hash are both computed over
1957+
`ECC_dword_reversed || MLDSA_native`.
1958+
18381959
#### SVN fuses (little-endian 128-bit bitmap)
18391960

18401961
**FUSE_FIRMWARE_SVN** and **FUSE_SOC_MANIFEST_SVN** are 128-bit one-hot encoded bitmaps stored

rom/dev/tests/rom_integration_tests/test_debug_unlock.rs

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -406,11 +406,11 @@ fn test_dbg_unlock_prod_success() {
406406
sha512.update(challenge.challenge);
407407
let sha512_digest = sha512.finalize();
408408

409-
// Sign the digest in the little endian format, as required by the hardware.
409+
// Sign the raw SHA-512 digest bytes; MLDSA mailbox encoding happens when packing the signature.
410410
let mldsa_signature = signing_mldsa_key
411411
.try_sign_with_seed(&[0; 32], &sha512_digest, &[])
412412
.unwrap();
413-
// Convert the signature slice to little endian dwords, as required by the hardware.
413+
// Pack the signature bytes into little-endian u32 words for the mailbox field.
414414
let mldsa_signature = {
415415
let mut sig = [0; 4628];
416416
sig[..4627].copy_from_slice(&mldsa_signature);
@@ -580,13 +580,14 @@ fn test_dbg_unlock_prod_invalid_token_challenge() {
580580
pk[48..].copy_from_slice(ecc_key.y().unwrap());
581581
pk
582582
};
583-
// Convert to hardware format i.e. little endian.
583+
// Convert to hardware format i.e. big endian for ECC.
584584
let ecc_pub_key = u8_to_u32_be(&ecc_pub_key_bytes);
585585
let ecc_pub_key_bytes = ecc_pub_key.as_bytes();
586586

587587
let (verifying_mldsa_key, _signing_mldsa_key) = fips204::ml_dsa_87::try_keygen().unwrap();
588588
let mldsa_pub_key_bytes = verifying_mldsa_key.into_bytes();
589-
let mldsa_pub_key = u8_to_u32_be(&mldsa_pub_key_bytes);
589+
// Convert to hardware format i.e. little endian for MLDSA.
590+
let mldsa_pub_key = u8_to_u32_le(&mldsa_pub_key_bytes);
590591
let mldsa_pub_key_bytes = mldsa_pub_key.as_bytes();
591592

592593
let security_state = *SecurityState::default()
@@ -710,15 +711,15 @@ fn test_dbg_unlock_prod_invalid_signature() {
710711
pk[48..].copy_from_slice(ecc_key.y().unwrap());
711712
pk
712713
};
713-
// Convert to hardware format i.e. little endian.
714+
// Convert to hardware format i.e. big endian for ECC.
714715
let ecc_pub_key = u8_to_u32_be(&ecc_pub_key_bytes);
715716
let ecc_pub_key_bytes = ecc_pub_key.as_bytes();
716717

717718
let (verifying_mldsa_key, signing_mldsa_key) = fips204::ml_dsa_87::try_keygen().unwrap();
718719
let mldsa_pub_key_bytes = verifying_mldsa_key.into_bytes();
719720

720-
// Convert to hardware format i.e. little endian.
721-
let mldsa_pub_key = u8_to_u32_be(&mldsa_pub_key_bytes);
721+
// Convert to hardware format i.e. little endian for MLDSA.
722+
let mldsa_pub_key = u8_to_u32_le(&mldsa_pub_key_bytes);
722723
let mldsa_pub_key_bytes = mldsa_pub_key.as_bytes();
723724

724725
let security_state = *SecurityState::default()
@@ -878,7 +879,7 @@ fn test_dbg_unlock_prod_wrong_public_keys() {
878879

879880
let (different_verifying_mldsa_key, _) = fips204::ml_dsa_87::try_keygen().unwrap();
880881
let different_mldsa_pub_key_bytes = different_verifying_mldsa_key.into_bytes();
881-
let different_mldsa_pub_key = u8_to_u32_be(&different_mldsa_pub_key_bytes);
882+
let different_mldsa_pub_key = u8_to_u32_le(&different_mldsa_pub_key_bytes);
882883

883884
let security_state = *SecurityState::default()
884885
.set_debug_locked(true)
@@ -995,14 +996,14 @@ fn test_dbg_unlock_prod_wrong_cmd() {
995996
pk[48..].copy_from_slice(ecc_key.y().unwrap());
996997
pk
997998
};
998-
// Convert to hardware format i.e. little endian.
999+
// Convert to hardware format i.e. big endian for ECC.
9991000
let ecc_pub_key = u8_to_u32_be(&ecc_pub_key_bytes);
10001001
let ecc_pub_key_bytes = ecc_pub_key.as_bytes();
10011002

10021003
let (verifying_mldsa_key, _signing_mldsa_key) = fips204::ml_dsa_87::try_keygen().unwrap();
10031004
let mldsa_pub_key_bytes = verifying_mldsa_key.into_bytes();
1004-
// Convert to hardware format i.e. little endian.
1005-
let mldsa_pub_key = u8_to_u32_be(&mldsa_pub_key_bytes);
1005+
// Convert to hardware format i.e. little endian for MLDSA.
1006+
let mldsa_pub_key = u8_to_u32_le(&mldsa_pub_key_bytes);
10061007
let mldsa_pub_key_bytes = mldsa_pub_key.as_bytes();
10071008

10081009
let security_state = *SecurityState::default()
@@ -1170,9 +1171,7 @@ fn test_dbg_unlock_prod_unlock_levels_success() {
11701171
sha512.update(challenge.challenge);
11711172
let sha512_digest = sha512.finalize();
11721173

1173-
// Convert to hardware format i.e. big endian before signing for MLDSA.
1174-
// let binding = u8_to_u32_be(&sha512_digest);
1175-
// let msg = binding.as_bytes();
1174+
// No byte swapping is needed before MLDSA signing; sign the raw SHA-512 digest bytes.
11761175

11771176
let mldsa_signature = signing_mldsa_key
11781177
.try_sign_with_seed(&[0; 32], &sha512_digest, &[])

runtime/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2880,8 +2880,8 @@ Command Code: `0x5357_4545` ("SWEE")
28802880
| **Name** | **Type** | **Description** |
28812881
| -------- | -------- | --------------- |
28822882
| chksum | u32 | Checksum over other input arguments, computed by the caller. Little endian. |
2883-
| exported_cdi_handle | u8[32] | The Exported CDI handle returned by the DPE `DeriveContext` command. Little endian. |
2884-
| tbs | u8[48] | The bytes to be signed. Little endian. |
2883+
| exported_cdi_handle | u8[32] | The exported CDI handle returned by the DPE `DeriveContext` command. Opaque byte array; copy the bytes exactly as returned. |
2884+
| tbs | u8[48] | The 48-byte SHA-384 digest to be signed. Raw byte array; no byte swapping is required. |
28852885

28862886
*Table: `SIGN_WITH_EXPORTED_ECDSA` output arguments*
28872887
| **Name** | **Type** | **Description** |
@@ -2904,7 +2904,7 @@ Command Code: `5256_4348` ("RVCH")
29042904
| **Name** | **Type** | **Description** |
29052905
| -------- | -------- | --------------- |
29062906
| chksum | u32 | Checksum over other input arguments, computed by the caller. Little endian. |
2907-
| exported_cdi_handle | u8[32] | The Exported CDI handle returned by the DPE `DeriveContext` command. Little endian. |
2907+
| exported_cdi_handle | u8[32] | The exported CDI handle returned by the DPE `DeriveContext` command. Opaque byte array; copy the bytes exactly as returned. |
29082908

29092909
The `exported_cdi` can be created by calling `DeriveContext` with the `export-cdi` and `create-certificate` flags.
29102910

runtime/tests/runtime_integration_tests/test_debug_unlock.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,7 @@ fn test_dbg_unlock_prod_wrong_public_keys() {
580580

581581
let (different_verifying_mldsa_key, _) = fips204::ml_dsa_87::try_keygen().unwrap();
582582
let different_mldsa_pub_key_bytes = different_verifying_mldsa_key.into_bytes();
583-
let different_mldsa_pub_key = u8_to_u32_be(&different_mldsa_pub_key_bytes);
583+
let different_mldsa_pub_key = u8_to_u32_le(&different_mldsa_pub_key_bytes);
584584

585585
let security_state = *SecurityState::default()
586586
.set_debug_locked(true)

sw-emulator/lib/periph/src/mci.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ impl MciRegs {
359359
// Copy hash into fuses array (64 bytes / 16 u32s)
360360
let base_idx = i * (SS_MANUF_DBG_UNLOCK_FUSE_SIZE / size_of::<u32>());
361361
hash.chunks(4).enumerate().for_each(|(j, chunk)| {
362-
// Program the hash in hardware format i.e. little endian.
362+
// Program the hash as big-endian u32 words; the bus presents each word's bytes little-endian at byte addresses.
363363
let value = u32::from_be_bytes(chunk.try_into().unwrap());
364364
fuses[base_idx + j] = value;
365365
});

0 commit comments

Comments
 (0)