// SPDX-License-Identifier: GPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // lint-disable-file fixed-width-types /* Fingerprintd:Rpmb — the RPMB listener's wire format. The other half of QTEE's storage. Where gpfile moves the container bytes, RPMB is the anti-rollback: an authenticated, monotonically counted area of the UFS device that lets QTEE tell a genuine store from an old one replayed back at it. 16 of 66 callbacks during a stock enrolment land here. Everything in this module is framing and policy — request decode, reply framing, the JEDEC result codes, and the two guards that make a write safe to relay. The SCSI transport lives in the daemon shell. Read out of librpmb.so and out of QTEE's own checking code, not guessed. The FP6 is UFS, so the device path is SECURITY PROTOCOL OUT/IN against the RPMB well-known LUN. */ export module Fingerprintd:Rpmb; import std; export namespace fingerprintd::rpmb { // ---- Transport -------------------------------------------------------- // The RPMB well-known LUN is UPIU 0xC4, which maps to SCSI WLUN 0xC144 = // 49476. /dev/bsg/ufs-bsg0 is the UPIU passthrough node and is NOT the // right target for these SCSI commands — aiming there fails in a way that // looks like the device refusing the request. inline constexpr std::string_view BsgDevice = "/dev/bsg/0:0:0:49476"; inline constexpr std::uint32_t RpmbWlun = 49476; inline constexpr std::uint8_t SecurityProtocolUfs = 0xEC; // JEDEC UFS inline constexpr std::uint16_t SecurityProtocolSpecific = 0x0001; // RPMB inline constexpr std::size_t FrameSize = 512; // The first command after a device reset answers sense key 6, ASC 0x29/02 // — a unit attention, not a failure. The reference has a whole function // for exactly this. Retry once rather than reporting an error. inline constexpr std::uint8_t SenseKeyUnitAttention = 6; inline constexpr std::uint8_t AscPowerOnReset = 0x29; // ---- Requests --------------------------------------------------------- // // +0x00 u32 op // +0x04 u32 nblocks ... and on the way out, the status // +0x08 u32 framesize ... and on the way out, bytes transferred // +0x0c u32 dataoff where the frames sit, relative to the request // +0x14 u32 blocks-per-op chunk size, for writes // +dataoff the 512-byte JEDEC frames enum class Op : std::uint32_t { Init = 0x101, Read = 0x102, Write = 0x103, PartitionConfig = 0x104, }; inline constexpr std::size_t OpOff = 0x00; inline constexpr std::size_t NblocksOff = 0x04; inline constexpr std::size_t StatusOff = 0x04; inline constexpr std::size_t FrameSizeOff = 0x08; inline constexpr std::size_t TransferredOff = 0x08; inline constexpr std::size_t DataOffOff = 0x0c; inline constexpr std::size_t BlocksPerOpOff = 0x14; inline constexpr std::size_t SharedBufferSize = 25600; struct Request { Op op = Op::Read; std::uint32_t nblocks = 0; std::uint32_t frameSize = 0; std::uint32_t dataOff = 0; std::uint32_t blocksPerOp = 0; }; namespace detail { inline std::uint32_t LoadU32(std::span b, std::size_t off) { std::uint32_t v = 0; for (std::size_t i = 0; i < 4; i++) v |= static_cast(std::to_integer(b[off + i])) << (8 * i); return v; } inline void StoreU32(std::span b, std::size_t off, std::uint32_t v) { for (std::size_t i = 0; i < 4; i++) b[off + i] = static_cast((v >> (8 * i)) & 0xFF); } // JEDEC frame fields are BIG endian. inline std::uint16_t LoadBe16(std::span b, std::size_t off) { return static_cast( (std::to_integer(b[off]) << 8) | std::to_integer(b[off + 1])); } } inline std::optional ParseRequest(std::span frame) { if (frame.size() < BlocksPerOpOff + 4) return std::nullopt; Request r; r.op = static_cast(detail::LoadU32(frame, OpOff)); r.nblocks = detail::LoadU32(frame, NblocksOff); r.frameSize = detail::LoadU32(frame, FrameSizeOff); r.dataOff = detail::LoadU32(frame, DataOffOff); // Read before the first transfer: the reference reuses this word as // its result buffer, so by the end of a write it no longer holds the // chunk size. r.blocksPerOp = detail::LoadU32(frame, BlocksPerOpOff); return r; } // Where the JEDEC frames sit. QTEE supplies this and it arrives as 0x18, // even though librpmb hardcodes 20 on the way out. inline bool FramesInBounds(std::span buf, const Request& r) { std::size_t need = static_cast(r.dataOff) + static_cast(r.nblocks) * FrameSize; return r.nblocks > 0 && need <= buf.size(); } // ---- Replies ---------------------------------------------------------- // // Both reply fields are INPUTS on the way in, so they may only be written // after the transfer. // // +0x08 is the one that cost a week. librpmb passes it to rpmb_ufs_read // BY ADDRESS (0x8248: add x3, x19, #0x8), so it is an out-parameter for // bytes transferred — and QTEE checks it (0x156eb9f8: cmp x24, x8; b.ne), // whose failure block sets -2 and logs (50000d)/(50000e fffffffe). Leaving // the request's frame size there failed every transaction. // // +0x0c is where QTEE looks for the response frames // (req + req[0x0c], bounds-checked at 0x156eba10). librpmb stores a // hardcoded 20; the request arrives with 0x18 and that is where the frames // actually are, so writing 20 sends QTEE four bytes early. Keep what the // request supplied. inline constexpr std::uint32_t LibrpmbHardcodedDataOff = 20; // A read posts ONE request frame however large nblocks is; a write posts // nblocks * 512. So the transferred count is direction-dependent, and the // reference's write path sets it to a flat 512. inline std::uint32_t BytesTransferred(Op op, std::uint32_t nblocks) { return op == Op::Write ? static_cast(FrameSize) : nblocks * static_cast(FrameSize); } inline void WriteReply(std::span buf, std::int32_t status, std::uint32_t bytesTransferred) { detail::StoreU32(buf, StatusOff, static_cast(status)); detail::StoreU32(buf, TransferredOff, bytesTransferred); // DataOffOff is deliberately left as the request supplied it. } // ---- JEDEC frames ----------------------------------------------------- // // Offsets from the end of a 512-byte frame, all big endian. inline constexpr std::size_t FrameWriteCounterOff = 500; inline constexpr std::size_t FrameAddressOff = 504; inline constexpr std::size_t FrameBlockCountOff = 506; inline constexpr std::size_t FrameResultOff = 508; inline constexpr std::size_t FrameReqRespOff = 510; enum class ReqResp : std::uint16_t { AuthKeyProgram = 0x0001, ReadWriteCounter = 0x0002, AuthDataWrite = 0x0003, AuthDataRead = 0x0004, ResultRead = 0x0005, }; inline std::uint16_t ReqRespOf(std::span frame) { return detail::LoadBe16(frame, FrameReqRespOff); } inline std::uint16_t ResultOf(std::span frame) { return detail::LoadBe16(frame, FrameResultOff); } inline std::uint32_t WriteCounterOf(std::span frame) { std::uint32_t v = 0; for (std::size_t i = 0; i < 4; i++) v = (v << 8) | std::to_integer(frame[FrameWriteCounterOff + i]); return v; } // Result codes. Bit 7 set means the write counter has expired. inline constexpr std::uint16_t ResultOk = 0x0000; inline constexpr std::uint16_t ResultCounterExpired = 0x0080; inline constexpr std::string_view ResultString(std::uint16_t r) { switch (r & 0x007F) { case 0x0000: return "OK"; case 0x0001: return "general failure"; case 0x0002: return "authentication failure"; case 0x0003: return "counter failure"; case 0x0004: return "address failure"; case 0x0005: return "write failure"; case 0x0006: return "read failure"; case 0x0007: return "key not yet programmed"; default: return "unknown"; } } // ---- The guard -------------------------------------------------------- // // NEVER REMOVE THIS. // // req_resp 0x0001 is Authentication Key Programming. The RPMB key is // ONE-TIME programmable in the UFS device: if it were ever reprogrammed, // this part's RPMB is spent permanently and no reflash recovers it. QTEE // has no legitimate reason to send it — the key is provisioned at // manufacture — so a frame carrying it is a bug or an attack, and it is // refused unconditionally regardless of whether writes are otherwise // allowed. inline bool IsKeyProgramming(std::span frame) { return ReqRespOf(frame) == static_cast(ReqResp::AuthKeyProgram); } // Scan every frame in the request, not just the first: the guard is only // as good as its coverage. inline bool AnyKeyProgramming(std::span buf, const Request& r) { for (std::uint32_t k = 0; k < r.nblocks; k++) { std::size_t off = r.dataOff + static_cast(k) * FrameSize; if (off + FrameSize > buf.size()) return true; // malformed: refuse if (IsKeyProgramming(buf.subspan(off, FrameSize))) return true; } return false; } // ---- The authenticated write sequence --------------------------------- // // Per chunk, three SCSI commands (librpmb rpmb_ufs_write, 0x9ebc): // SECURITY PROTOCOL OUT bpo * 512 the data frames // SECURITY PROTOCOL OUT 512 a Result Read Request // SECURITY PROTOCOL IN 512 the result frame // // The Result Read Request is a 512-byte constant in librpmb's .data whose // only non-zero bytes are req_resp = 0x0005. inline void BuildResultReadRequest(std::span frame) { std::ranges::fill(frame.first(FrameSize), std::byte{0}); frame[FrameReqRespOff] = std::byte{0x00}; frame[FrameReqRespOff + 1] = std::byte{0x05}; } // The reference runs nblocks / bpo chunks and silently drops a remainder, // which would commit a partial transaction and leave the store // inconsistent with QTEE's counter. Refuse instead. struct ChunkPlan { std::uint32_t chunks = 0; bool exact = false; }; inline ChunkPlan PlanChunks(std::uint32_t nblocks, std::uint32_t blocksPerOp) { if (blocksPerOp == 0 || nblocks == 0) return {}; std::uint32_t chunks = nblocks / blocksPerOp; return { chunks, chunks != 0 && chunks * blocksPerOp == nblocks }; } // An RPMB write advances a monotonic counter in the device and cannot be // undone. The daemon keeps it behind an explicit opt-in, the way the // harness did, so a first authentication run cannot touch the enrolled // template. inline constexpr std::int32_t StatusRefused = -1; inline constexpr std::int32_t StatusOk = 0; }