From cc079cd1f87523ff613b03730fdef95a07491906 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Wed, 2 Sep 2026 21:06:51 +0200 Subject: [PATCH] Implement the RPMB write path, which was never there An enrolment collected all ten samples and then SAVE_DATA answered -5. The cause was not the sensor or the storage framing: ServeRpmb only ever implemented Op::Read. A write fell through the branch with rc still -1 and was refused, whatever --rpmb-write said. QTEE could not commit the anti-rollback record, so it rolled the transaction back -- after it had already rewritten the group's index container on disk. The write sequence is per chunk: the data frames out, a Result Read Request out, the result frame back. A remainder is refused rather than partially committed, and a non-zero device result aborts instead of continuing into further chunks, because at that point the counter state is not what we think it is. The refusal was not the only failure. Two assumptions were wrong and both are recorded in the journal: --group-path does NOT isolate the group directory. The writes went to the Android group, the one holding the working template, not to a new group derived from the namespace path. Isolation has to come from pointing the SFS root at a separate tree, not from the namespace key. And the rolled-back transaction left the index rewritten, so QTEE rejected it and the template became unreachable -- ENUMERATE 0, and repeated unlink attempts refused only because the mount had been switched back to read-only. Restoring the index from the pre-enrolment backup brought it back: templates loaded 1. The RPMB counter never moved, which is why restoring an older index worked at all. Had the write path been implemented, it would have. --- implementations/main.cpp | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/implementations/main.cpp b/implementations/main.cpp index 2b71d38..b3b2ce2 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -499,6 +499,43 @@ void ServeRpmb(std::span sb) { // collects nblocks * 512 back. if (SecurityProtocolRetry(fd, false, frames, rp::FrameSize) == 0) rc = SecurityProtocolRetry(fd, true, frames, total); + } else { + // The authenticated write sequence, per chunk: the data frames out, a + // Result Read Request out, the result frame back. + // + // A remainder is refused rather than partially committed. The + // reference silently drops one, which would leave the store + // inconsistent with a counter that cannot be moved back. + auto plan = rp::PlanChunks(req->nblocks, req->blocksPerOp); + if (!plan.exact) { + std::println(" rpmb: {} blocks is not a whole number of {}-block chunks" + " -- refusing", req->nblocks, req->blocksPerOp); + } else { + std::array rrq{}; + rp::BuildResultReadRequest(rrq); + std::array result{}; + rc = 0; + for (std::uint32_t k = 0; k < plan.chunks && rc == 0; k++) { + std::byte* chunk = frames + static_cast(k) + * req->blocksPerOp * rp::FrameSize; + std::uint32_t bytes = req->blocksPerOp + * static_cast(rp::FrameSize); + if (SecurityProtocolRetry(fd, false, chunk, bytes) != 0 || + SecurityProtocolRetry(fd, false, rrq.data(), rp::FrameSize) != 0 || + SecurityProtocolRetry(fd, true, result.data(), rp::FrameSize) != 0) { + rc = -1; + break; + } + std::uint16_t res = rp::ResultOf(result); + std::println(" rpmb write chunk {}/{}: result=0x{:04x} ({}) counter={}", + k + 1, plan.chunks, res, rp::ResultString(res), + rp::WriteCounterOf(result)); + // Anything non-zero aborts rather than continuing into further + // chunks: the device rejected the frame and the counter state + // is not what we think it is. + if (res != rp::ResultOk) rc = -1; + } + } } ::close(fd);