Skip to content

M1-33 Devices, device keys and DPoP verification

No Date Ruling Reason
M1-33/1 2026-10-12 Base int/m1-wave6 at 785916c, branch feat/M1-33-identity-devices-dpop in its own worktree; the branch is not merged locally and the worktree stays, the PR lane ports it. Text this ticket adds is English, also in the module README (its German headings stay). Instructions of the launcher, which override the brief's merge step and the brief's language.
M1-33/2 2026-10-12 listUserDevices authorizes device.list on the resource kind device without an id and with OwnerID = the path person, not on the kind user of the brief's table. An unknown person is not_found before authorize; listMyDevices keeps the brief's resource user = caller and getDevice the device itself. With the kind user another person is visible (people see the directory) and the requirement self or owner fails with 403 forbidden, which breaks the brief's outcome column and 05 rule 2 ("No permission to see gives 404"). Devices are owned resources (M1-28/D4): visible to their person and to effective owners only, so the device kind gives 404 from authz itself and no rights logic moves out of authz. The pending rule still answers 403 device_pending for the own list from a pending device.
M1-33/3 2026-10-12 A missing device or person and an invisible one answer the same 404 not_found without a detail (app.invisible). 02 section 9.2 and 05 rule 2: the bodies must not differ (TestGetDevice_PendingOwnOnly, TestListUserDevices_SelfOwnerOther).
M1-33/4 2026-10-12 identity.Device and the API response leave out verification_code; M1-38 adds it for the pending device itself. 08 section 5.2 derives the code from the token pepper and auth.device_verification_code_digits, and the brief M1-38 owns approval with the code and its response mapping.
M1-33/5 2026-10-12 TestCredentialSource_Devices checks active, pending, revoked, a pending device older than any pending window (stays pending), a foreign principal and an unknown device (unknown). Session and token facts stay not active until M1-34 and M1-44, so a request that names a session or token is unauthenticated (fail closed). Devices have no expiry of their own; treating a pending device past auth.device_pending_ttl as revoked belongs to M1-38 (its brief, design notes of authentication). The brief's word "expired" is covered as the old pending device.
M1-33/6 2026-10-12 kernel/jose bounds JSON nesting with the constant MaxJSONDepth = 32, the default of http.max_json_depth. The kernel reads no configuration and ParseCompactJWS has a fixed signature; JOSE objects of the profile nest at most three levels, so the bound only stops hostile input.
M1-33/7 2026-10-12 The JWS vectors of ADR 0067 run as TestJWS_RFC7515ExampleES256 (appendix A.3 through ParseCompactJWS and Verify) and TestVerify_RFC8037ExampleEdDSA (appendix A.4 through the internal signature check, because its payload is plain text and the profile accepts JSON objects only). The thumbprint vectors are those of the brief (RFC 9449 and RFC 8037); the RSA vector of RFC 7638 section 3.1 comes with RSA in M1-41. The profile has no RSA key type yet; every vector the profile can express is tested. The literal segments stand apart in the test source so that the secret scanner (rule jwt) does not take the public examples for tokens; the fuzz seed files write the leading e of each segment as \x65 for the same reason.
M1-33/8 2026-10-12 kernel/jose has a few exported helpers beyond the brief: JWKFromPublicKey, JWK.Canonical, JWK.MarshalJSON, JWK.PublicKey, ES256Signer, EdDSASigner, Header, AlgES256, AlgEdDSA, ErrInvalidJWK, ErrInvalidJWS, ErrSignature. ParseCompactJWS with an empty typ checks no type; zip is refused besides crit, jku, x5u, x5c. The test helpers, the stored canonical JWK and the token issuers of M1-34 need them; ADR 0067 item 2 lists zip. An empty typ exists for foreign tokens without one (the RFC 7515 example).
M1-33/9 2026-10-12 device_keys.public_jwk stores the canonical public JWK (only crv, kty, x, y); the attestation is stored as received. The repository has KeyByJKT beyond the port for the login tickets. The canonical form is exactly what the thumbprint covers, so no unknown member of a client survives; 08 section 6.1 keeps the attestation unchanged.
M1-33/10 2026-10-12 Device registration also applies SEC-IN-4 to app_version, rejects an approver on a pending registration, and has no length limit for the name. Both are display texts of the device list; an approver only belongs to an active device (03 approved_by). No configuration key names a device name limit, so none is invented; the body limit bounds the input.
M1-33/11 2026-10-12 bothy_app gets SELECT, INSERT, UPDATE on devices and device_keys, no DELETE. Revocation sets state and revoked_at; the rows are security history (retention class security), and the retention job of that class grants its delete in its own migration.
M1-33/12 2026-10-12 The jti store keeps no size cap; an insert looks at up to four entries of its shard and removes the expired ones, and a sweeper removes every expired entry each auth.dpop_proof_max_age (stop step dpop_replay_sweeper, phase HTTP). 08 section 7.3 names no limit (the window and the rate limits bound it); a full scan per insert would make inserts linear in the shard size. The window length is the interval after which every entry of a window has expired.
M1-33/13 2026-10-12 The token pepper is resolved once at the start (step 6d identity, after authz), hex-decoded and must have 32 bytes, or the start ends. secrets init already creates it (token_pepper, generator key), so M1-09 needs no change. A missing or malformed pepper must fail the start, not the first request; rotating the pepper changes the nonce key and needs a restart, which only invalidates outstanding nonces (clients retry with the new one).
M1-33/14 2026-10-12 api.page_size_default and api.page_size_max are registered in their own key group (apiConfigKeys, chapter 06); a default above the maximum ends the start (app.NewDevices). The cursor binds the caller, the listed person and the sort, so /me/devices and /users/{own id}/devices share cursors. Every later list reads the same two keys; both paths of the own devices are one list, and a cursor never moves to another caller or person.
M1-33/15 2026-10-12 identity.DPoPError maps to kernel/errs through Unwrap, which returns the 401 problem with WWW-Authenticate: DPoP error="<code>", algs="ES256 EdDSA" and DPoP-Nonce; the verifier logs the internal reason at level debug with the correlation id. domain/dpop names its rejection ProofError (lint rule errname). The problem writer finds the error through errs.As, so every caller renders it without extra code; the body carries no reason (no oracle).
M1-33/16 2026-10-12 wire.Handlers embeds *httpapi.Devices; the start fills a nil module handler with the one built in step 6d. API times are UTC truncated to the millisecond. Tests keep injecting their own handlers through Deps.Handlers; the schema Timestamp asks for millisecond precision.
M1-33/17 2026-10-12 The DPoP table test is TestDPoP_IDT14 with one subtest per case (plus a missing header, a header jwk of another key and a header without jwk); TestRegister_Validation runs in domain/device, TestRegister_Persists and the rules that need the store in app. One table keeps the expected code next to each case; the domain rules need no database.
M1-33/18 2026-10-12 wire.NewChain takes the request validator of the enabled operations from enabledValidator, compiled once per process (sync.OnceValues). With three enabled operations the validator warms their response schemas; under -race and a host load above 50 every test server of internal/wire paid that again, so readiness waits of 30 s timed out (a timing probe measured 37 s per build on the head and 9.6 s on the base at load 58). The spec and the operation list are fixed in the binary and the validator is safe for concurrent use, so one build serves every chain; bothy serve builds one chain anyway.
M1-33/19 2026-10-13 Port onto main: the migration is 00008_identity_devices.sql (00007 is M1-20), its Down test goes down to version 7, the start step identity follows authz (6d), and chapter references name the English files and section titles. The same renumbering as the integration (ADR 0007 rule 4); a Down test to version 6 would also remove the idempotency keys of M1-20.