198 lines
9 KiB
C++
198 lines
9 KiB
C++
|
|
// 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;
|
||
|
|
}
|