257 lines
12 KiB
Text
257 lines
12 KiB
Text
|
|
// 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;
|
||
|
|
}
|