// SPDX-License-Identifier: GPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // lint-disable-file fixed-width-types /* Fingerprintd:Rpmb unit tests. Two things here are not ordinary parsing bugs. The key-programming guard protects against an IRREVERSIBLE action: the RPMB authentication key is one-time programmable in the UFS device, and relaying a frame that reprograms it destroys that part's RPMB permanently, with no reflash recovering it. It is tested for coverage over every frame in a request, not just the first, and for refusing a malformed request rather than reading past the buffer. The out-parameter at +0x08 is the field that made every RPMB transaction fail for a week. QTEE compares it against what it expected to be transferred and rejects the whole transaction on a mismatch, so a read and a write do not report the same thing. */ import std; import Fingerprintd; using namespace fingerprintd::rpmb; namespace { int Failures = 0; void Check(bool cond, std::string_view msg) { if (!cond) { std::println(std::cerr, "FAIL: {}", msg); ++Failures; } } std::uint32_t Get32(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; } void Put32(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); } void PutBe16(std::span b, std::size_t off, std::uint16_t v) { b[off] = static_cast((v >> 8) & 0xFF); b[off + 1] = static_cast(v & 0xFF); } // A request buffer holding `n` frames at dataOff 0x18, each with the given // req_resp — the shape QTEE actually sends. std::vector MakeRequest(Op op, std::uint32_t n, std::uint16_t reqResp, std::uint32_t blocksPerOp = 1) { std::vector buf(SharedBufferSize); Put32(buf, OpOff, static_cast(op)); Put32(buf, NblocksOff, n); Put32(buf, FrameSizeOff, static_cast(FrameSize)); Put32(buf, DataOffOff, 0x18); Put32(buf, BlocksPerOpOff, blocksPerOp); for (std::uint32_t k = 0; k < n; k++) PutBe16(buf, 0x18 + static_cast(k) * FrameSize + FrameReqRespOff, reqResp); return buf; } } int main() { // ---- The guard against an irreversible action { // A single key-programming frame is refused. auto buf = MakeRequest(Op::Write, 1, static_cast(ReqResp::AuthKeyProgram)); auto r = ParseRequest(buf); Check(r.has_value(), "key-program request parses"); Check(r && AnyKeyProgramming(buf, *r), "a key-programming frame is caught"); // Hidden behind five legitimate frames it is still caught — the guard // scans every frame, not just the first. auto many = MakeRequest(Op::Write, 6, static_cast(ReqResp::AuthDataWrite)); auto rm = ParseRequest(many); Check(rm && !AnyKeyProgramming(many, *rm), "six clean write frames pass"); PutBe16(many, 0x18 + 5 * FrameSize + FrameReqRespOff, static_cast(ReqResp::AuthKeyProgram)); Check(rm && AnyKeyProgramming(many, *rm), "a key-programming frame in the LAST slot is caught"); // An ordinary authenticated write and read are not mistaken for it. auto w = MakeRequest(Op::Write, 1, static_cast(ReqResp::AuthDataWrite)); auto rw = ParseRequest(w); Check(rw && !AnyKeyProgramming(w, *rw), "a normal write is not refused"); auto rd = MakeRequest(Op::Read, 6, static_cast(ReqResp::AuthDataRead)); auto rr = ParseRequest(rd); Check(rr && !AnyKeyProgramming(rd, *rr), "a normal read is not refused"); // A request claiming more frames than the buffer holds is refused // rather than read past. auto bad = MakeRequest(Op::Write, 1, static_cast(ReqResp::AuthDataWrite)); Put32(bad, NblocksOff, 100000); auto rb = ParseRequest(bad); Check(rb && AnyKeyProgramming(bad, *rb), "a malformed request is refused, not read past"); Check(rb && !FramesInBounds(bad, *rb), "and it fails the bounds check"); } // ---- The out-parameter that QTEE checks { // A read posts one request frame however large nblocks is; a write // posts nblocks * 512 and the reference reports a flat 512. Check(BytesTransferred(Op::Read, 6) == 6 * FrameSize, "read reports the total"); Check(BytesTransferred(Op::Write, 6) == FrameSize, "write reports one frame"); Check(BytesTransferred(Op::Read, 6) != BytesTransferred(Op::Write, 6), "the two directions do NOT report the same thing"); auto buf = MakeRequest(Op::Read, 6, static_cast(ReqResp::AuthDataRead)); auto r = ParseRequest(buf); Check(r && r->frameSize == FrameSize, "frame size arrives as 512"); WriteReply(buf, StatusOk, BytesTransferred(Op::Read, r->nblocks)); Check(static_cast(Get32(buf, StatusOff)) == 0, "status written"); Check(Get32(buf, TransferredOff) == 6 * FrameSize, "bytes transferred, not frame size"); Check(Get32(buf, TransferredOff) != FrameSize, "leaving the request's 512 there is what failed every transaction"); // The data offset is left exactly as the request supplied it. QTEE // looks for the frames at req + req[0x0c]; librpmb's hardcoded 20 // points four bytes early. Check(Get32(buf, DataOffOff) == 0x18, "data offset preserved"); Check(Get32(buf, DataOffOff) != LibrpmbHardcodedDataOff, "not overwritten with 20"); } // ---- Request decoding { auto buf = MakeRequest(Op::Read, 6, static_cast(ReqResp::AuthDataRead)); auto r = ParseRequest(buf); Check(r && r->op == Op::Read, "op 0x102 = read"); Check(r && r->nblocks == 6, "nblocks"); Check(r && r->dataOff == 0x18, "data offset"); Check(r && FramesInBounds(buf, *r), "frames in bounds"); std::vector stub(8); Check(!ParseRequest(stub).has_value(), "short request rejected"); auto zero = MakeRequest(Op::Read, 0, 0); auto rz = ParseRequest(zero); Check(rz && !FramesInBounds(zero, *rz), "zero blocks is not in bounds"); } // ---- Chunking: a remainder must not commit a partial transaction { Check(PlanChunks(6, 6).exact && PlanChunks(6, 6).chunks == 1, "6/6 = one chunk"); Check(PlanChunks(12, 6).exact && PlanChunks(12, 6).chunks == 2, "12/6 = two chunks"); Check(!PlanChunks(7, 6).exact, "7 blocks in 6-block chunks is refused"); Check(!PlanChunks(3, 6).exact, "fewer blocks than a chunk is refused"); Check(!PlanChunks(6, 0).exact, "a zero chunk size is refused, not divided by"); Check(!PlanChunks(0, 6).exact, "zero blocks is refused"); } // ---- The Result Read Request constant { std::vector rrq(FrameSize); BuildResultReadRequest(rrq); Check(ReqRespOf(rrq) == static_cast(ReqResp::ResultRead), "req_resp = 0x0005"); std::size_t nonZero = 0; for (std::byte b : rrq) if (b != std::byte{0}) nonZero++; Check(nonZero == 1, "exactly one non-zero byte, as in librpmb's .data"); } // ---- Frame fields are big endian { std::vector f(FrameSize); PutBe16(f, FrameResultOff, 0x0000); PutBe16(f, FrameReqRespOff, 0x0300); Check(ResultOf(f) == ResultOk, "result OK"); Check(ReqRespOf(f) == 0x0300, "req_resp big endian"); f[FrameWriteCounterOff] = std::byte{0x00}; f[FrameWriteCounterOff + 1] = std::byte{0x00}; f[FrameWriteCounterOff + 2] = std::byte{0x24}; f[FrameWriteCounterOff + 3] = std::byte{0x05}; Check(WriteCounterOf(f) == 0x2405, "write counter big endian"); } // ---- Result strings, including the expiry bit Check(ResultString(0x0000) == "OK", "0"); Check(ResultString(0x0002) == "authentication failure", "2"); Check(ResultString(0x0007) == "key not yet programmed", "7"); Check(ResultString(0x0080 | 0x0003) == "counter failure", "bit 7 is the expiry flag, not part of the code"); Check((ResultCounterExpired & 0x007F) == 0, "the expiry bit is outside the code"); // ---- Transport constants Check(BsgDevice == "/dev/bsg/0:0:0:49476", "the RPMB WLUN node, not ufs-bsg0"); Check(RpmbWlun == 49476, "UPIU 0xC4 -> SCSI WLUN 0xC144"); Check(SecurityProtocolUfs == 0xEC, "JEDEC UFS security protocol"); Check(FrameSize == 512, "JEDEC frame size"); if (Failures == 0) std::println("Rpmb: all tests passed"); return Failures; }