Sealed-Jetson + SaaS architecture eliminates the credential-reuse-across-
machines threat that motivated hardware fingerprint binding. The binding's
only remaining effect was a real production failure mode on legitimate
hardware events.
Production:
- Drop PUT /users/hardware/set and POST /resources/check.
- Simplify POST /resources/get/{dataFolder?} (no Hardware field).
- Remove CheckHardwareHash, UpdateHardware, Security.GetHWHash.
- GetApiEncryptionKey signature: (email, password) — no hardwareHash.
- Drop SetHWRequest DTO and Hardware property from GetResourceRequest.
- Remove HardwareIdMismatch (40) and BadHardware (45) ExceptionEnum
entries; numeric codes left as a gap, not for reuse.
Wire-compat policy: drop entirely (no Loader; no in-flight legacy
clients). Stale callers will see 404s, which is the right loud failure.
Tombstones:
- User.Hardware DB column kept (nullable, unused) — separate cleanup
ticket for the migration per workspace "no rename without confirmation".
- User.LastLogin is now never written by app code (only writer was inside
the deleted CheckHardwareHash); flagged in batch_06_review for a future
ticket.
Tests:
- Delete e2e HardwareBindingTests (165 lines) and Azaion.Test
UserServiceTest (sole test was CheckHardwareHashTest).
- Drop Hardware payloads + /resources/check preconditions from e2e
ResourceTests, SecurityTests, ResilienceTests; drop hardwareId arg
from Azaion.Test SecurityTest.
- Add SecurityTests.Hardware_endpoints_are_removed_AZ_197 (AC-2 regression
asserting both removed routes return 404).
Docs:
- architecture.md: System Context note, ADR-003 new key formula, ADR-004
retired with rationale.
- diagrams/flows/flow_hardware_check.md: tombstoned.
Also archives the four batch-1+batch-2 task files into _docs/02_tasks/done/
(file moves were missed by the batch_05 commit).
Code review: PASS — see _docs/03_implementation/reviews/batch_06_review.md.
Co-authored-by: Cursor <cursoragent@cursor.com>
9.0 KiB
Remove Hardware ID Binding
Task: AZ-197_remove_hardware_id
Name: Remove hardware ID binding from resource flow (admin-side cleanup)
Description: Remove CheckHardwareHash, UpdateHardware, HardwareService, the PUT /users/hardware/set endpoint, and the hardware-hash component of API encryption-key derivation. The threat this protected against (credential reuse across machines via desktop installers) no longer exists in the target architecture.
Complexity: 3 points
Dependencies: None
Component: Admin API
Tracker: AZ-197
Epic: AZ-181
Problem
The Hardware field on User and the CheckHardwareHash flow were designed to bind a user account to a specific physical machine, preventing credential reuse across machines when users had desktop installers.
The target architecture has eliminated that threat:
- Edge devices ship as secured Jetsons with fTPM (secure boot, fTPM-protected key storage, no user filesystem access, no desktop installers distributed). Hardware identity is anchored in the fTPM, not in a SHA-384 of CPU/GPU/Memory/DriveSerial strings.
- Server / desktop access uses the SaaS path (browser → admin API). There is no installer to copy and no hardware fingerprint to take.
- The Loader component itself has been architecturally retired (Scenario X = Watchtower + rclone + flight-gate; see
suite/_docs/_repo-config.yamlunresolved:loader-retirement-arch-docand the assumptions log entries dated 2026-04-19). Provisioning was relocated fromloader/scripts/tosuite/_infra/provisioning/. There is noloader/workspace in the suite anymore, so this ticket is now purely admin-side.
The hardware binding therefore adds:
- Unnecessary complexity in the encryption-key derivation chain.
- A real production failure mode (
HardwareIdMismatch, error code 40) on legitimate drive-replacement / fTPM-attested rotations. - A maintenance cost on every endpoint and DTO that still carries the
Hardwarefield.
Outcome
- Resource download flow no longer requires a hardware fingerprint.
- API encryption-key derivation simplified to email + password only.
- All admin-API hardware-binding code paths removed.
- Hardware-binding tests removed (unit + e2e); other tests updated to stop sending
Hardware. - DB column
User.Hardwareleft in place but nullable and unused — no migration in this ticket (separate cleanup ticket if/when desired).
Scope
Included — Admin API production code
- Remove
CheckHardwareHashandUpdateHardwarefromIUserService/UserService(Azaion.Services/UserService.cs). - Remove
PUT /users/hardware/setendpoint fromAzaion.AdminApi/Program.cs. - Simplify
POST /resources/get/{dataFolder}(Program.cs+Azaion.Services/ResourcesService.cs): removerequest.Hardwareparameter usage; derive encryption key without the hardware hash. - Simplify
POST /resources/check: remove the hardware-binding side-effect entirely. If the endpoint becomes purely a "do I have any newer resources?" probe, keep it; if it becomes a no-op shell, remove it (decide based on what consumers still call today). - Update
Security.GetApiEncryptionKey(Azaion.Services/Security.cs) to drop thehardwareHashparameter from its signature; derive the key fromemail + passwordonly. - Remove (do not deprecate)
Security.GetHWHash— the codebase has no Loader to coordinate with anymore. - Remove
SetHWRequestDTO (Azaion.Common/Requests/SetHWRequest.cs). - Remove the
Hardwareproperty usage fromGetResourceRequest(Azaion.Common/Requests/GetResourceRequest.cs). The wire field may still be accepted (deserialized and ignored) for one release cycle to keep any in-flight legacy clients from breaking on 400s — pick the simplest of (drop entirely / accept-and-ignore) and document the choice in the implementation report. - Remove
HardwareIdMismatchandBadHardwarefromAzaion.Common/BusinessException.csExceptionEnum. - Leave
User.Hardwarecolumn in DB (nullable, unused). No migration here.
Included — Tests in this workspace
- Delete
e2e/Azaion.E2E/Tests/HardwareBindingTests.csentirely (every test in that file asserts behaviour that is being removed). - Update
e2e/Azaion.E2E/Tests/ResourceTests.cs,ResilienceTests.cs,SecurityTests.csto stop sending theHardwarefield on resource calls (or to assert the field is ignored, whichever matches the chosen wire-compat policy above). - Update
Azaion.Test/UserServiceTest.csandAzaion.Test/SecurityTest.csto remove tests asserting hardware-hash behaviour and to drop thehardwareHashargument from any retainedGetApiEncryptionKeycalls. - Trim test fixtures in
db-init/andAzaion.Testif they seed aUser.Hardwarevalue purely to satisfy hardware-binding flows.
Included — Workspace docs (pointer-only updates, no full rewrite)
- Mark
_docs/02_document/diagrams/flows/flow_hardware_check.mdas obsolete (header note + link to AZ-197 implementation report) — full deletion is fine if cleaner. - Mark
_docs/02_document/modules/common_requests_set_hw.mdas obsolete (the documented module no longer exists). - Note in
_docs/02_document/architecture.md(Security & Encryption section) that API encryption-key derivation no longer includes a hardware hash, and thatUser.Hardwareis a tombstoned column.
Excluded
- Database migration to drop the
hardwarecolumn fromusers(separate ticket if/when desired; harmless to leave nullable). - Changes to user registration or login flow (those don't touch the hardware path).
- Any change to the suite-level
_docs/00_top_level_architecture.md"Security & Encryption" / "Binary Split Security" sections — that's part ofunresolved:loader-retirement-arch-docand is owned at the suite level, not by this ticket. - Live-device decommissioning of fielded Loader containers (separate ops runway, tracked as a sibling of
loader-retirement-arch-doc). - Anything in the
loader/workspace — it does not exist in the suite anymore.
Acceptance Criteria
AC-1: Resource download works without hardware
Given a provisioned device user with valid email and password
When POST /resources/get/{dataFolder} is called without a Hardware field
Then the resource is returned and decrypts successfully using a key derived from email + password only
AC-2: Hardware-set endpoint is gone
Given the updated admin API
When PUT /users/hardware/set is called with any payload
Then the response is 404
AC-3: Encryption-key derivation is simplified
Given the updated Security.GetApiEncryptionKey
When it is called with (email, password)
Then it returns the key derived from email + password only — there is no hardwareHash parameter on the public signature
AC-4: Hardware-binding tests are gone
Given the updated test projects
When the test suite is built and listed
Then HardwareBindingTests does not exist and no remaining test asserts HardwareIdMismatch / error code 40 / hardware-hash binding
AC-5: Resource calls in remaining tests do not send Hardware
Given the updated ResourceTests, ResilienceTests, SecurityTests
When the resource-download / resource-check requests are inspected
Then no test sends a Hardware field on any resource request (or, if accept-and-ignore wire-compat was chosen, tests assert the response is unchanged whether Hardware is present or absent)
AC-6: ExceptionEnum no longer has hardware codes
Given Azaion.Common/BusinessException.cs
When the ExceptionEnum is read
Then HardwareIdMismatch and BadHardware entries are gone, and no production code references them
AC-7: Build is clean
Given the workspace after the changes
When dotnet build runs across the solution
Then it completes with no errors and no new warnings introduced by this ticket
AC-8: Test suite passes
Given the workspace after the changes
When the existing test suite (Azaion.Test + e2e/Azaion.E2E) is run via docker-compose.test.yml
Then all tests pass (the deleted HardwareBindingTests are not counted)
Constraints
- Wire-compat policy on the
Hardwarefield of resource requests must be chosen explicitly (drop / accept-and-ignore) and recorded in the implementation report — this is the only consumer-facing contract change in the ticket. - Do not rename
User.Hardwarecolumn or drop it from the entity in this ticket; only stop reading/writing it. Renaming/dropping requires a separate migration ticket per the workspace's "no rename without confirmation" rule.
Cross-architecture context
This ticket is the admin-side half of an architectural transition that has already happened:
- Loader retirement (Scenario X) —
suite/_docs/_repo-config.yaml→unresolved:loader-retirement-arch-doc - Suite-root restructure (2026-04-19) — see assumptions_log entries in the same file
- Admin-side hardware-binding cleanup — this ticket (AZ-197)
The matching suite-doc refresh (top-level architecture, Binary Split Security section) is tracked separately under the unresolved item above and is intentionally NOT in this ticket's scope.