fingerprintd/tests/Rpmb/main.cpp

198 lines
9 KiB
C++
Raw Permalink Normal View History

Port the RPMB listener's wire format, guard included Fingerprintd:Rpmb is 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. Framing and policy only; the SCSI transport stays in the daemon shell. The guard is the reason this module has tests rather than just constants. req_resp 0x0001 is Authentication Key Programming, and the RPMB key is one-time programmable in the UFS device -- relaying such a frame destroys that part's RPMB permanently and no reflash recovers it. QTEE has no legitimate reason to send one, so it is refused unconditionally, whatever the write policy says. It is tested for scanning every frame rather than the first, and for refusing a request that claims more frames than the buffer holds instead of reading past the end. The out-parameter at +0x08 is the field that failed every RPMB transaction for a week. librpmb passes it by address, so it reports bytes transferred, and QTEE compares it against what it expected and rejects the transaction on a mismatch. A read posts one request frame however large nblocks is while a write posts nblocks * 512, so the two directions genuinely do not report the same thing -- tested as such, because leaving the request's frame size there is the bug. +0x0c is kept exactly as the request supplied it. QTEE looks for the response frames at req + req[0x0c] and the request arrives with 0x18; librpmb's hardcoded 20 points four bytes early. Chunking refuses a remainder rather than following the reference, which silently drops one -- a partial authenticated write leaves the store inconsistent with a counter that cannot be moved back. Verified by mutation: checking only the first frame for key programming, reporting a flat frame size as bytes transferred, and admitting a remainder each fail the suite.
2026-09-02 16:48:57 +02:00
// 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<const std::byte> b, std::size_t off) {
std::uint32_t v = 0;
for (std::size_t i = 0; i < 4; i++)
v |= static_cast<std::uint32_t>(std::to_integer<unsigned>(b[off + i])) << (8 * i);
return v;
}
void Put32(std::span<std::byte> b, std::size_t off, std::uint32_t v) {
for (std::size_t i = 0; i < 4; i++)
b[off + i] = static_cast<std::byte>((v >> (8 * i)) & 0xFF);
}
void PutBe16(std::span<std::byte> b, std::size_t off, std::uint16_t v) {
b[off] = static_cast<std::byte>((v >> 8) & 0xFF);
b[off + 1] = static_cast<std::byte>(v & 0xFF);
}
// A request buffer holding `n` frames at dataOff 0x18, each with the given
// req_resp — the shape QTEE actually sends.
std::vector<std::byte> MakeRequest(Op op, std::uint32_t n, std::uint16_t reqResp,
std::uint32_t blocksPerOp = 1) {
std::vector<std::byte> buf(SharedBufferSize);
Put32(buf, OpOff, static_cast<std::uint32_t>(op));
Put32(buf, NblocksOff, n);
Put32(buf, FrameSizeOff, static_cast<std::uint32_t>(FrameSize));
Put32(buf, DataOffOff, 0x18);
Put32(buf, BlocksPerOpOff, blocksPerOp);
for (std::uint32_t k = 0; k < n; k++)
PutBe16(buf, 0x18 + static_cast<std::size_t>(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<std::uint16_t>(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<std::uint16_t>(ReqResp::AuthDataWrite));
auto rm = ParseRequest(many);
Check(rm && !AnyKeyProgramming(many, *rm), "six clean write frames pass");
PutBe16(many, 0x18 + 5 * FrameSize + FrameReqRespOff,
static_cast<std::uint16_t>(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<std::uint16_t>(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<std::uint16_t>(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<std::uint16_t>(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<std::uint16_t>(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<std::int32_t>(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<std::uint16_t>(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<std::byte> 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<std::byte> rrq(FrameSize);
BuildResultReadRequest(rrq);
Check(ReqRespOf(rrq) == static_cast<std::uint16_t>(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<std::byte> 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;
}