Delete a finger's template, not just its name
FF_CMD_TA_REMOVE, recovered the way AUTHENTICATE was: read the stub, read the handler. The 0x2006 stub at 0xa15c is a bare `ldp w0, w1, [payload]`, so the request is two u32s -- gid at +0, fid at +4 -- and the 0x2000 dispatcher validates no length. Walking the jump table reproduces authenticate at 0xa180, which is the address already on record, so the table read is sound. Three preconditions, all the trustlet's own. The gid must be the ACTIVE group (it compares against device+0x30, the field SET_ACTIVE_GROUP writes). The fid must be non-zero: zero is not "remove all", it is an error the trustlet logs and refuses. And the fid must be among the loaded templates, because it removes by the SLOT INDEX it finds, not by id. It persists: on a hit the trustlet formats ff_template_<gid>_<slot>.bin and calls ff_file_delete, which arrives on our gpfile listener as an unlink -- so this only works with the store served writable. Proven harmlessly first. --probe-remove sends one command with no map involvement, and a fid the group does not hold answers rc=-2 with the real template untouched -- which is what established that both words are read where we send them, before anything was deleted. Then for real, through fprintd-delete: both 347202-byte containers and their .bak companions unlinked, templates loaded 1 -> 0, and a re-enrolment afterwards completed 20 stages with SAVE_DATA rc=0, so the store is consistent after a removal rather than merely emptier. The ordering the transcript shows is worth keeping: the group index is rewritten and the RPMB anti-rollback counter bumped BEFORE each unlink. That is precisely why an orderly removal leaves a valid store where restoring an older container leaves a tampered one -- the counter has already moved past it. The delete reply now waits for the worker, because only that thread invokes the trustlet and fprintd's Delete methods are synchronous. Names are dropped before templates on purpose: a template that survives a failed removal is a slot leak, while a name that survives a successful one keeps offering a finger that can no longer match.
This commit is contained in:
parent
b228287c5b
commit
41e86f84f4
4 changed files with 178 additions and 13 deletions
|
|
@ -66,7 +66,7 @@ namespace {
|
||||||
|
|
||||||
// Bumping this is what publishes a package: the registry answers 409 for a
|
// Bumping this is what publishes a package: the registry answers 409 for a
|
||||||
// version it already has, which a build treats as a no-op.
|
// version it already has, which a build treats as a no-op.
|
||||||
constexpr const char* Version = "0.1.2";
|
constexpr const char* Version = "0.1.3";
|
||||||
|
|
||||||
bool g_verbose = false;
|
bool g_verbose = false;
|
||||||
// 500 ms was the research harness's pace, chosen so a human could read the
|
// 500 ms was the research harness's pace, chosen so a human could read the
|
||||||
|
|
@ -1269,6 +1269,46 @@ public:
|
||||||
// callbacks at all -- so a group that is already active does not need
|
// callbacks at all -- so a group that is already active does not need
|
||||||
// selecting again. `force` is for the cases that genuinely change the
|
// selecting again. `force` is for the cases that genuinely change the
|
||||||
// store: an enrolment, or a template removed underneath us.
|
// store: an enrolment, or a template removed underneath us.
|
||||||
|
// Remove templates from the TRUSTLET, which is what makes a delete a
|
||||||
|
// delete. Dropping the name from the map only stops the finger being
|
||||||
|
// offered; the template keeps its slot, and with
|
||||||
|
// enable_duplicated_finger_checking on, that slot is what refuses the
|
||||||
|
// re-enrolment of the same finger.
|
||||||
|
//
|
||||||
|
// The group must be active first: the trustlet compares the gid against
|
||||||
|
// device+0x30 and will not search a group it has not loaded. Returns the
|
||||||
|
// number removed; a per-fid failure is logged and does not stop the rest,
|
||||||
|
// because a partial delete is still better than none and the caller has
|
||||||
|
// already committed to losing these fingers.
|
||||||
|
int RemoveTemplates(std::uint32_t gid, const std::vector<std::uint32_t>& fids) {
|
||||||
|
namespace ta = fingerprintd::ta;
|
||||||
|
if (fids.empty()) return 0;
|
||||||
|
if (SetActiveGroup(gid) < 0) {
|
||||||
|
std::println(std::cerr, "remove: group {} would not load, nothing removed", gid);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
int removed = 0;
|
||||||
|
for (std::uint32_t fid : fids) {
|
||||||
|
// The trustlet refuses this itself, with a log line nobody reads.
|
||||||
|
if (fid == 0) continue;
|
||||||
|
std::vector<std::byte> rm(ta::RemovePayloadSize);
|
||||||
|
ta::BuildRemovePayload(rm, gid, fid);
|
||||||
|
auto r = SendCommand(app_, ta::Cmd::Remove, rm);
|
||||||
|
if (r.rc == 0) {
|
||||||
|
std::println(" removed template fid={} from group {}", fid, gid);
|
||||||
|
removed++;
|
||||||
|
} else {
|
||||||
|
std::println(std::cerr, " REMOVE fid={} failed rc={} ({})", fid, r.rc,
|
||||||
|
ta::StrError(r.rc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The trustlet's own accounting moved, so ours must be re-read rather
|
||||||
|
// than assumed: a later claim that trusts a stale count skips the
|
||||||
|
// reload it now needs.
|
||||||
|
if (removed) SetActiveGroup(gid, /*force*/ true);
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
int SetActiveGroup(std::uint32_t gid, bool force = false) {
|
int SetActiveGroup(std::uint32_t gid, bool force = false) {
|
||||||
namespace ta = fingerprintd::ta;
|
namespace ta = fingerprintd::ta;
|
||||||
// > 0, never >= 0: caching a ZERO turns a failed load into a
|
// > 0, never >= 0: caching a ZERO turns a failed load into a
|
||||||
|
|
@ -1880,17 +1920,19 @@ private:
|
||||||
// jobs; results and progress come back through the GLib main loop.
|
// jobs; results and progress come back through the GLib main loop.
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
struct Job {
|
struct Job {
|
||||||
enum class Kind { Claim, Enroll, Verify } kind;
|
enum class Kind { Claim, Enroll, Verify, Remove } kind;
|
||||||
std::uint32_t uid = 0;
|
std::uint32_t uid = 0;
|
||||||
std::string finger;
|
std::string finger;
|
||||||
std::vector<std::uint32_t> acceptFids; // Verify: which fids count
|
std::vector<std::uint32_t> acceptFids; // Verify: which fids count
|
||||||
GDBusMethodInvocation* invocation = nullptr; // Claim replies asynchronously
|
// Remove: which fids to drop
|
||||||
|
GDBusMethodInvocation* invocation = nullptr; // Claim/Remove reply asynchronously
|
||||||
};
|
};
|
||||||
|
|
||||||
// Everything the worker sends back to the main thread. Delivered by g_idle_add
|
// Everything the worker sends back to the main thread. Delivered by g_idle_add
|
||||||
// so the D-Bus emission happens on the thread that owns the connection.
|
// so the D-Bus emission happens on the thread that owns the connection.
|
||||||
struct Event {
|
struct Event {
|
||||||
enum class Kind { Ready, StartFailed, ClaimDone, EnrollStatus, VerifyStatus } kind;
|
enum class Kind { Ready, StartFailed, ClaimDone, EnrollStatus, VerifyStatus,
|
||||||
|
RemoveDone } kind;
|
||||||
bool ok = false;
|
bool ok = false;
|
||||||
bool done = false;
|
bool done = false;
|
||||||
std::string status;
|
std::string status;
|
||||||
|
|
@ -1968,6 +2010,17 @@ private:
|
||||||
PostEvent(std::move(ev));
|
PostEvent(std::move(ev));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case Job::Kind::Remove: {
|
||||||
|
int n = session_.RemoveTemplates(j.uid, j.acceptFids);
|
||||||
|
std::println("remove: {} of {} template(s) removed for uid {}",
|
||||||
|
n < 0 ? 0 : n, j.acceptFids.size(), j.uid);
|
||||||
|
auto ev = std::make_unique<Event>(Event{ .kind = Event::Kind::RemoveDone });
|
||||||
|
ev->ok = n >= 0;
|
||||||
|
ev->templates = n;
|
||||||
|
ev->invocation = j.invocation;
|
||||||
|
PostEvent(std::move(ev));
|
||||||
|
break;
|
||||||
|
}
|
||||||
case Job::Kind::Enroll: {
|
case Job::Kind::Enroll: {
|
||||||
auto o = session_.Enrol(
|
auto o = session_.Enrol(
|
||||||
j.uid, cancel_,
|
j.uid, cancel_,
|
||||||
|
|
@ -2258,6 +2311,20 @@ void PostEvent(std::unique_ptr<Event> ev) {
|
||||||
g_pendingClaim = nullptr;
|
g_pendingClaim = nullptr;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case Event::Kind::RemoveDone:
|
||||||
|
if (ev->invocation) {
|
||||||
|
if (ev->ok) {
|
||||||
|
g_dbus_method_invocation_return_value(ev->invocation, nullptr);
|
||||||
|
} else {
|
||||||
|
// The names are already gone from the map by this point.
|
||||||
|
// Saying so is better than a bare failure: the fingers
|
||||||
|
// will not be offered again, but their templates still
|
||||||
|
// hold slots, which is what will refuse a re-enrolment.
|
||||||
|
ReturnError(ev->invocation, "Internal",
|
||||||
|
"names removed, but the trustlet templates were not");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
case Event::Kind::EnrollStatus:
|
case Event::Kind::EnrollStatus:
|
||||||
if (!ev->status.empty())
|
if (!ev->status.empty())
|
||||||
EmitDevice("EnrollStatus", g_variant_new("(sb)", ev->status.c_str(), ev->done ? TRUE : FALSE));
|
EmitDevice("EnrollStatus", g_variant_new("(sb)", ev->status.c_str(), ev->done ? TRUE : FALSE));
|
||||||
|
|
@ -2384,17 +2451,28 @@ void HandleDevice(GDBusMethodInvocation* inv, std::string_view method, GVariant*
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
auto m = LoadMap(uid);
|
auto m = LoadMap(uid);
|
||||||
|
// The fids have to be read BEFORE the names go: the map is the only
|
||||||
|
// place that remembers which template belongs to which finger.
|
||||||
|
std::vector<std::uint32_t> fids;
|
||||||
|
for (const auto& e : m.Entries())
|
||||||
|
if (!one || e.finger == *one) fids.push_back(e.fid);
|
||||||
if (one) m.Remove(*one); else m.Clear();
|
if (one) m.Remove(*one); else m.Clear();
|
||||||
SaveMap(uid, m);
|
SaveMap(uid, m);
|
||||||
if (g_claim.held && g_claim.uid == uid) g_claim.fingers = m;
|
if (g_claim.held && g_claim.uid == uid) g_claim.fingers = m;
|
||||||
// The trustlet-side template is NOT removed. FF_CMD_TA_REMOVE (0x2006)
|
// The name is dropped first and the template second, in that order on
|
||||||
// exists but its payload has not been reverse-engineered, and guessing
|
// purpose: a finger whose template survives a failed removal is a slot
|
||||||
// at a command that writes to the store is how an index gets
|
// leak, while a name that survives a successful one would keep
|
||||||
// invalidated. Until it is, a deleted finger loses its name and stops
|
// offering a finger that can no longer match.
|
||||||
// being offered, but its template still occupies a slot in the group.
|
std::println("deleting finger name(s) for uid {}; removing {} trustlet template(s)",
|
||||||
std::println("deleted finger name(s) for uid {} -- trustlet template(s) NOT removed "
|
uid, fids.size());
|
||||||
"(FF_CMD_TA_REMOVE not yet implemented)", uid);
|
if (fids.empty()) {
|
||||||
g_dbus_method_invocation_return_value(inv, nullptr);
|
g_dbus_method_invocation_return_value(inv, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Only the worker thread ever invokes the trustlet, so the reply waits
|
||||||
|
// for it -- fprintd's Delete methods are synchronous to the client.
|
||||||
|
g_worker->Post(Job{ .kind = Job::Kind::Remove, .uid = uid,
|
||||||
|
.acceptFids = fids, .invocation = inv });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (method == "EnrollStart" || method == "VerifyStart") {
|
if (method == "EnrollStart" || method == "VerifyStart") {
|
||||||
|
|
@ -2553,6 +2631,29 @@ int RunDaemon() {
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Diagnostic modes: the probe flow, kept for a phone with no bus client at hand.
|
// Diagnostic modes: the probe flow, kept for a phone with no bus client at hand.
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
// --probe-remove=<fid>: send ONE 0x2006 with no map involvement.
|
||||||
|
//
|
||||||
|
// It exists because the recovered payload had to be provable without
|
||||||
|
// destroying anything. Every wrong call the trustlet can receive here is a
|
||||||
|
// LOGGED REFUSAL rather than damage -- a fid it does not hold is not found, a
|
||||||
|
// gid that is not the active group is "hasn't been loaded", and zero is
|
||||||
|
// refused outright -- so the negative cases establish that the two words are
|
||||||
|
// being read where we think they are, at no risk to an enrolled finger. The
|
||||||
|
// positive case deletes a template container and cannot be undone: QTEE seals
|
||||||
|
// every stored object to a hardware anti-rollback counter, so a removed
|
||||||
|
// template is gone, not archived.
|
||||||
|
int RunProbeRemove(std::uint32_t gid, std::uint32_t fid) {
|
||||||
|
namespace ta = fingerprintd::ta;
|
||||||
|
Session s;
|
||||||
|
if (!s.Start()) return 1;
|
||||||
|
int loaded = s.SetActiveGroup(gid);
|
||||||
|
std::println("=== probe: REMOVE gid={} fid={} ({} template(s) loaded) ===",
|
||||||
|
gid, fid, loaded);
|
||||||
|
int removed = s.RemoveTemplates(gid, { fid });
|
||||||
|
std::println("PROBE RESULT: {} template(s) removed", removed);
|
||||||
|
return removed > 0 ? 0 : 2;
|
||||||
|
}
|
||||||
|
|
||||||
int RunProbe(bool doAuth, bool doEnrol, bool doCalSave, bool doLearnProbe,
|
int RunProbe(bool doAuth, bool doEnrol, bool doCalSave, bool doLearnProbe,
|
||||||
std::uint32_t gid, int frames) {
|
std::uint32_t gid, int frames) {
|
||||||
namespace ta = fingerprintd::ta;
|
namespace ta = fingerprintd::ta;
|
||||||
|
|
@ -2674,6 +2775,7 @@ int main(int argc, char** argv) {
|
||||||
std::span<char*> args(argv, static_cast<std::size_t>(argc));
|
std::span<char*> args(argv, static_cast<std::size_t>(argc));
|
||||||
bool probe = false, daemon = false, doAuth = false, doEnrol = false, doCalSave = false;
|
bool probe = false, daemon = false, doAuth = false, doEnrol = false, doCalSave = false;
|
||||||
bool doLearnProbe = false;
|
bool doLearnProbe = false;
|
||||||
|
std::uint32_t probeRemoveFid = 0;
|
||||||
std::string probeTa;
|
std::string probeTa;
|
||||||
std::uint32_t gid = 0;
|
std::uint32_t gid = 0;
|
||||||
int frames = 120;
|
int frames = 120;
|
||||||
|
|
@ -2693,6 +2795,10 @@ int main(int argc, char** argv) {
|
||||||
if (a == "--enrol") { doEnrol = true; probe = true; }
|
if (a == "--enrol") { doEnrol = true; probe = true; }
|
||||||
if (a == "--cal-save") { doCalSave = true; probe = true; g_verbose = true; }
|
if (a == "--cal-save") { doCalSave = true; probe = true; g_verbose = true; }
|
||||||
if (a == "--probe-learn") { doLearnProbe = true; probe = true; g_verbose = true; }
|
if (a == "--probe-learn") { doLearnProbe = true; probe = true; g_verbose = true; }
|
||||||
|
if (a.starts_with("--probe-remove=")) {
|
||||||
|
probeRemoveFid = static_cast<std::uint32_t>(std::stoul(std::string(a.substr(15))));
|
||||||
|
g_verbose = true;
|
||||||
|
}
|
||||||
if (a.starts_with("--frames=")) frames = std::stoi(std::string(a.substr(9)));
|
if (a.starts_with("--frames=")) frames = std::stoi(std::string(a.substr(9)));
|
||||||
if (a.starts_with("--frame-gap=")) g_frameGapMs = std::stoi(std::string(a.substr(12)));
|
if (a.starts_with("--frame-gap=")) g_frameGapMs = std::stoi(std::string(a.substr(12)));
|
||||||
if (a == "--undecided=nomatch") g_undecidedIsNoMatch = true;
|
if (a == "--undecided=nomatch") g_undecidedIsNoMatch = true;
|
||||||
|
|
@ -2715,6 +2821,7 @@ int main(int argc, char** argv) {
|
||||||
if (!daemon || g_logDirExplicit) StartTranscript(g_logDir);
|
if (!daemon || g_logDirExplicit) StartTranscript(g_logDir);
|
||||||
if (!probeTa.empty()) return RunProbeTaLoad(probeTa);
|
if (!probeTa.empty()) return RunProbeTaLoad(probeTa);
|
||||||
if (daemon) return RunDaemon();
|
if (daemon) return RunDaemon();
|
||||||
|
if (probeRemoveFid) return RunProbeRemove(gid, probeRemoveFid);
|
||||||
if (probe) return RunProbe(doAuth, doEnrol, doCalSave, doLearnProbe, gid, frames);
|
if (probe) return RunProbe(doAuth, doEnrol, doCalSave, doLearnProbe, gid, frames);
|
||||||
|
|
||||||
std::println(std::cerr,
|
std::println(std::cerr,
|
||||||
|
|
@ -2724,6 +2831,9 @@ int main(int argc, char** argv) {
|
||||||
" --probe-ta-load=PATH load one TA image and report the loader result\n"
|
" --probe-ta-load=PATH load one TA image and report the loader result\n"
|
||||||
" --auth | --enrol | --cal-save diagnostic loops (see README)\n"
|
" --auth | --enrol | --cal-save diagnostic loops (see README)\n"
|
||||||
" --probe-learn send one UPDATE_TEMPLATE, no finger needed\n"
|
" --probe-learn send one UPDATE_TEMPLATE, no finger needed\n"
|
||||||
|
" --probe-remove=FID [--gid=N] send one REMOVE. A fid the group does not\n"
|
||||||
|
" hold is refused harmlessly; one it DOES hold\n"
|
||||||
|
" is deleted and cannot be recovered\n"
|
||||||
" --ta-log print the trustlet's own log lines\n"
|
" --ta-log print the trustlet's own log lines\n"
|
||||||
" --learn=0|1 [--learn-frames=N] fold a matched press back into the\n"
|
" --learn=0|1 [--learn-frames=N] fold a matched press back into the\n"
|
||||||
" template, as stock does (default on, 8)\n"
|
" template, as stock does (default on, 8)\n"
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ export namespace fingerprintd::ta {
|
||||||
Cancel = 0x2004,
|
Cancel = 0x2004,
|
||||||
ResetLockout = 0x200a,
|
ResetLockout = 0x200a,
|
||||||
Enumerate = 0x2005,
|
Enumerate = 0x2005,
|
||||||
|
Remove = 0x2006,
|
||||||
SetActiveGroup = 0x2007,
|
SetActiveGroup = 0x2007,
|
||||||
Authenticate = 0x2008,
|
Authenticate = 0x2008,
|
||||||
};
|
};
|
||||||
|
|
@ -330,6 +331,45 @@ export namespace fingerprintd::ta {
|
||||||
out[AuthCoveredOff] = static_cast<std::byte>(covered ? 1 : 0);
|
out[AuthCoveredOff] = static_cast<std::byte>(covered ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// REMOVE (TA 0xd7b0, reached from the 0x2006 stub at 0xa15c, which is a
|
||||||
|
// bare `ldp w0, w1, [payload]`):
|
||||||
|
// +0x00 u32 gid
|
||||||
|
// +0x04 u32 fid
|
||||||
|
// Declared length 0x08. The 0x2000-range dispatcher range-checks the
|
||||||
|
// command id and jumps; it validates no length, so the payload is exactly
|
||||||
|
// the two fields.
|
||||||
|
//
|
||||||
|
// Three preconditions, all of them the trustlet's own:
|
||||||
|
//
|
||||||
|
// gid must equal the ACTIVE group. ff_trustlet_remove compares it
|
||||||
|
// against device+0x30 -- the same field SET_ACTIVE_GROUP writes and
|
||||||
|
// AUTHENTICATE checks -- and logs "templates with gid(%u != %u) hasn't
|
||||||
|
// been loaded." on a mismatch.
|
||||||
|
//
|
||||||
|
// fid must be NON-ZERO. Zero is not "remove them all": the trustlet
|
||||||
|
// logs "error at %s[%s:%u]: removing template with fid equ 0." and
|
||||||
|
// refuses. Removing every finger means calling this once per fid.
|
||||||
|
//
|
||||||
|
// The fid must be among the templates currently LOADED. The trustlet
|
||||||
|
// walks its loaded list for a matching id and removes by SLOT INDEX,
|
||||||
|
// not by id -- libfp_template_remove takes the index it found.
|
||||||
|
//
|
||||||
|
// It persists. On a hit the trustlet logs "template (gid = %u, fid = %u)
|
||||||
|
// is found at slot %d.", formats "%s/ff_template_%d_%d.bin" and calls
|
||||||
|
// ff_file_delete, which arrives on the gpfile listener as an unlink -- so
|
||||||
|
// the daemon must be serving the store WRITABLE or the container survives
|
||||||
|
// the call that reported success.
|
||||||
|
inline constexpr std::size_t RemovePayloadSize = 0x08;
|
||||||
|
inline constexpr std::size_t RemoveGidOff = 0x00;
|
||||||
|
inline constexpr std::size_t RemoveFidOff = 0x04;
|
||||||
|
|
||||||
|
inline void BuildRemovePayload(std::span<std::byte> out, std::uint32_t gid,
|
||||||
|
std::uint32_t fid) {
|
||||||
|
std::ranges::fill(out.first(RemovePayloadSize), std::byte{0});
|
||||||
|
detail::StoreU32(out, RemoveGidOff, gid);
|
||||||
|
detail::StoreU32(out, RemoveFidOff, fid);
|
||||||
|
}
|
||||||
|
|
||||||
// SET_ACTIVE_GROUP writes its gid to device+0x30 and AUTHENTICATE compares
|
// SET_ACTIVE_GROUP writes its gid to device+0x30 and AUTHENTICATE compares
|
||||||
// its own against the same field (0xeb08), logging
|
// its own against the same field (0xeb08), logging
|
||||||
// "templates with gid(%u != %u) hasn't been loaded." and returning -200 on
|
// "templates with gid(%u != %u) hasn't been loaded." and returning -200 on
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
# Alpine, so an APKBUILD that compiled from source could not be built by
|
# Alpine, so an APKBUILD that compiled from source could not be built by
|
||||||
# anyone but us either.
|
# anyone but us either.
|
||||||
pkgname=fingerprintd
|
pkgname=fingerprintd
|
||||||
pkgver=0.1.2
|
pkgver=0.1.3
|
||||||
pkgrel=0
|
pkgrel=0
|
||||||
pkgdesc="Fingerprint daemon for the Fairphone 6 (FocalTech FT9391 behind QTEE)"
|
pkgdesc="Fingerprint daemon for the Fairphone 6 (FocalTech FT9391 behind QTEE)"
|
||||||
url="https://forgejo.catcrafts.net/Catcrafts/fingerprintd"
|
url="https://forgejo.catcrafts.net/Catcrafts/fingerprintd"
|
||||||
|
|
|
||||||
|
|
@ -239,6 +239,21 @@ int main() {
|
||||||
Check(std::to_integer<unsigned>(au[AuthRelightOff]) == 0, "flags clearable");
|
Check(std::to_integer<unsigned>(au[AuthRelightOff]) == 0, "flags clearable");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- REMOVE payload. Recovered from the 0x2006 stub at 0xa15c, which is
|
||||||
|
// a bare `ldp w0, w1, [payload]` into ff_trustlet_remove.
|
||||||
|
{
|
||||||
|
std::vector<std::byte> rm(RemovePayloadSize);
|
||||||
|
BuildRemovePayload(rm, 10000, 1768306590);
|
||||||
|
Check(RemovePayloadSize == 0x08, "declared length is exactly the two fields");
|
||||||
|
Check(Get32(rm, RemoveGidOff) == 10000, "gid at +0");
|
||||||
|
Check(Get32(rm, RemoveFidOff) == 1768306590, "fid at +4");
|
||||||
|
// Order matters and is not symmetric: the trustlet compares the FIRST
|
||||||
|
// word against device+0x30 (the active group) and searches its loaded
|
||||||
|
// list for the SECOND. Swap them and it reports the wrong-group error.
|
||||||
|
Check(RemoveGidOff < RemoveFidOff, "gid precedes fid");
|
||||||
|
Check(static_cast<std::uint32_t>(Cmd::Remove) == 0x2006, "command id");
|
||||||
|
}
|
||||||
|
|
||||||
// ---- ENROLL payload: an all-zero token is accepted when trusted
|
// ---- ENROLL payload: an all-zero token is accepted when trusted
|
||||||
// enrolment is off, which is why pmOS needs no Gatekeeper.
|
// enrolment is off, which is why pmOS needs no Gatekeeper.
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue