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.
This commit is contained in:
parent
ffad29ba4e
commit
1db0762078
4 changed files with 459 additions and 1 deletions
257
interfaces/Fingerprintd-Rpmb.cppm
Normal file
257
interfaces/Fingerprintd-Rpmb.cppm
Normal file
|
|
@ -0,0 +1,257 @@
|
||||||
|
// 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<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;
|
||||||
|
}
|
||||||
|
inline void StoreU32(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);
|
||||||
|
}
|
||||||
|
// JEDEC frame fields are BIG endian.
|
||||||
|
inline std::uint16_t LoadBe16(std::span<const std::byte> b, std::size_t off) {
|
||||||
|
return static_cast<std::uint16_t>(
|
||||||
|
(std::to_integer<unsigned>(b[off]) << 8) | std::to_integer<unsigned>(b[off + 1]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::optional<Request> ParseRequest(std::span<const std::byte> frame) {
|
||||||
|
if (frame.size() < BlocksPerOpOff + 4)
|
||||||
|
return std::nullopt;
|
||||||
|
Request r;
|
||||||
|
r.op = static_cast<Op>(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<const std::byte> buf, const Request& r) {
|
||||||
|
std::size_t need = static_cast<std::size_t>(r.dataOff) +
|
||||||
|
static_cast<std::size_t>(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<std::uint32_t>(FrameSize)
|
||||||
|
: nblocks * static_cast<std::uint32_t>(FrameSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void WriteReply(std::span<std::byte> buf, std::int32_t status,
|
||||||
|
std::uint32_t bytesTransferred) {
|
||||||
|
detail::StoreU32(buf, StatusOff, static_cast<std::uint32_t>(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<const std::byte> frame) {
|
||||||
|
return detail::LoadBe16(frame, FrameReqRespOff);
|
||||||
|
}
|
||||||
|
inline std::uint16_t ResultOf(std::span<const std::byte> frame) {
|
||||||
|
return detail::LoadBe16(frame, FrameResultOff);
|
||||||
|
}
|
||||||
|
inline std::uint32_t WriteCounterOf(std::span<const std::byte> frame) {
|
||||||
|
std::uint32_t v = 0;
|
||||||
|
for (std::size_t i = 0; i < 4; i++)
|
||||||
|
v = (v << 8) | std::to_integer<unsigned>(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<const std::byte> frame) {
|
||||||
|
return ReqRespOf(frame) == static_cast<std::uint16_t>(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<const std::byte> buf, const Request& r) {
|
||||||
|
for (std::uint32_t k = 0; k < r.nblocks; k++) {
|
||||||
|
std::size_t off = r.dataOff + static_cast<std::size_t>(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<std::byte> 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;
|
||||||
|
}
|
||||||
|
|
@ -13,4 +13,5 @@ the normal world, and reports the matched finger id.
|
||||||
|
|
||||||
export module Fingerprintd;
|
export module Fingerprintd;
|
||||||
export import :Sfs;
|
export import :Sfs;
|
||||||
|
export import :Rpmb;
|
||||||
export import :Ta;
|
export import :Ta;
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,10 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
ApplyStandardArgs(*Core, args);
|
ApplyStandardArgs(*Core, args);
|
||||||
Core->type = ConfigurationType::LibraryStatic;
|
Core->type = ConfigurationType::LibraryStatic;
|
||||||
{
|
{
|
||||||
std::array<fs::path, 3> ifaces = {
|
std::array<fs::path, 4> ifaces = {
|
||||||
"interfaces/Fingerprintd",
|
"interfaces/Fingerprintd",
|
||||||
"interfaces/Fingerprintd-Sfs",
|
"interfaces/Fingerprintd-Sfs",
|
||||||
|
"interfaces/Fingerprintd-Rpmb",
|
||||||
"interfaces/Fingerprintd-Ta",
|
"interfaces/Fingerprintd-Ta",
|
||||||
};
|
};
|
||||||
std::array<fs::path, 0> impls = {};
|
std::array<fs::path, 0> impls = {};
|
||||||
|
|
@ -46,6 +47,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.AddTest("Sfs").Dependencies({ Core.get() });
|
cfg.AddTest("Sfs").Dependencies({ Core.get() });
|
||||||
|
cfg.AddTest("Rpmb").Dependencies({ Core.get() });
|
||||||
cfg.AddTest("Ta").Dependencies({ Core.get() });
|
cfg.AddTest("Ta").Dependencies({ Core.get() });
|
||||||
|
|
||||||
ProjectLint::AddProjectLintRules(cfg);
|
ProjectLint::AddProjectLintRules(cfg);
|
||||||
|
|
|
||||||
198
tests/Rpmb/main.cpp
Normal file
198
tests/Rpmb/main.cpp
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue