Skip to content

Signed device redemption A1: P-256 ECDSA verify + ES256 APNs provider token - #11588

Closed
gunbai-bot[bot] wants to merge 24 commits into
mainfrom
session/swift-ibex-621
Closed

gunbai-bot[bot] wants to merge 24 commits into
mainfrom
session/swift-ibex-621

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Approval app, lane A, part 1 of 2: the ECDSA P-256 verifier and the ES256 APNs provider token. App Attest is part 2 and will be a separate PR.

Stacked on #11585 (it contains that PR's commit through a merge). The base stays main so that CI runs on it. To review only this lane, diff against session/wise-owl-628, which is the last commit, d51fd6a.

What landed

Two v1 host primitives over RustCrypto p256 0.13, which uses the same digest 0.10 family that sha2/hmac already use. Each is registered the way hmac_sha256_hex was in 40fb5b9:

  • the 04_method.dag signature and its infer_method mirror
  • the dispatch roster
  • the interpreter arm
  • the std.primitives contract, surface name and roster entry
  • the v1_interpreter_primitive_surface row
  1. p256_ecdsa_verify_b64url(key, sig, message) -> Bool? is bound as extdeps.crypto.signature p256_ecdsa_verify. It passes the decoded octet lengths to signature_verification_from_implementation. Absence from the primitive never reads as a mismatch, because three arms cover it: VerifyingKeyUndecodable, SignatureUndecodable and SignatureEncodingRefused. The last one covers input with the right lengths that is not a point on the curve or has a scalar out of range.
  2. es256_jwt_sign(p8, kid, team, iat) -> String? is modeled as extdeps.apple.apns apns_provider_token -> ApnsProviderTokenMint.

Witness evidence (all were run, and the results below were read)

  • test.claim.p256_ecdsa_witness_test: all 11 test fns passed. They were run through a local gunbc run driver that ANDs them, and it exited 0.
  • Mutation: the verify was forced to Some(true) and the binary rebuilt. Exactly a_tampered_message_is_invalid, a_tampered_signature_is_invalid and the_wrong_key_is_invalid went red.
  • The accepted vector is RFC 7515 Appendix A.3's published ES256 signature, which this implementation did not produce.
  • The ES256 token is deterministic (RFC 6979). The exact expected JWT was verified independently with openssl dgst -sha256 -verify against the RFC public key, and a one-byte change to the signing input fails.
  • The Rust unit tests beside the arms were run in an isolated crate with the same code: 5/5 pass, and the tamper test goes red under the same mutation.
  • --required-regen: first_generation_equal=true. The compile-clean result for the whole tree is left to CI.

🤖 Generated with Claude Code

Brian Searls and others added 4 commits September 18, 2026 06:15
…pp Attest, ECDSA P-256

The protocol between the roadmap server and the operator's phone for redeeming an
approval with a biometric-gated device signature, modeled before any server route or
Swift. iOS is the V1 realization; Android is modeled at every platform point (FCM,
Keystore key attestation) and realized by nothing yet.

- extdeps.crypto.signature: ECDSA P-256/SHA-256 interface (FIPS 186-5), one wire
  encoding per key and signature, sole_constructor evidence naming its message.
- extdeps.apple.apns / secure_enclave / app_attest, extdeps.google.fcm,
  extdeps.android.key_attestation: cited upstream shapes.
- gunbc.auth.approval_device_redemption: push is an opaque wake-up only; the app fetches
  the stored request, signs over a server challenge, the stored request text, the verb
  and the capability; server owns decided_at and derives the login; signature REQUIRED
  on the device route; RedemptionIdentityEvidence names the legacy arms by mechanism.
- Server routes, verification primitives, ApprovalTarget, cutover and Android are
  declared frontiers; the existing /approve route and store are untouched while the
  Mt. Collins first boot runs on them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…icts, route paths

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…Ns provider token

Two v1 host primitives over RustCrypto p256 0.13 (digest 0.10, the family sha2 0.10
and hmac 0.12 already use), registered exactly as hmac_sha256_hex was (40fb5b9):
04_method.dag signature + infer_method mirror (--required-regen
first_generation_equal=true), dispatch roster, interpreter arm, std.primitives
contract + surface name + roster, v1_interpreter_primitive_surface row.

- p256_ecdsa_verify_b64url(key_point_b64url, signature_b64url, message) -> Bool?:
  65-octet SEC 1 uncompressed point, 64-octet r||s, unpadded base64url; the message
  is SHA-256 hashed by the verifier. Absent on any non-admitted encoding.
- extdeps.crypto.signature p256_ecdsa_verify: decodes both inputs, hands the
  DECODED octet lengths to signature_verification_from_implementation. Three new
  arms keep absence from reading as a mismatch: VerifyingKeyUndecodable,
  SignatureUndecodable, SignatureEncodingRefused (right lengths, not a point on the
  curve / scalar in range).
- es256_jwt_sign(p8_pem_secret, key_id, team_id, issued_at_epoch) -> String?:
  header {alg ES256, kid}, claims {iss, iat}, JOSE r||s, RFC 6979 deterministic.
- extdeps.apple.apns apns_provider_token -> ApnsProviderTokenMint
  (Signed | AuthKeyUnreadable | IssuedAtBeforeEpoch).
- Witnesses (test.claim.p256_ecdsa_witness_test): RFC 7515 A.3's PUBLISHED ES256
  signature verifies; tampered message, tampered signature and wrong key are
  SignatureInvalid; the compressed spelling of the right key is
  VerifyingKeyMalformed{33}; an off-curve point is SignatureEncodingRefused; the
  provider token equals an exact expected JWT (independently verified with openssl)
  and verifies under the RFC public key; unreadable key and negative iat refuse.
  Rust unit tests beside the arms.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot gunbai-bot Bot changed the title approval app A: ECDSA/ES256/App Attest primitives Signed device redemption A1: P-256 ECDSA verify + ES256 APNs provider token Sep 18, 2026
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review September 18, 2026 06:55
@gunbai-bot

gunbai-bot Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

Re review 67577 (REQUEST_CHANGES): I checked both findings against this PR's own diff (git diff a5d322f d51fd6a, which is this lane's commit alone). Neither touches a line this lane authored.

Fixing them here would fork #11585's modeling onto a second branch. The findings are forwarded to that PR's owner, who is already revising these seams (e.g. ByteSize sizes on signature_verification_from_implementation). This PR will be rebased onto the revised #11585 once it lands.

One point does fall to this lane: apns_provider_token has no production consumer yet. Its consumer is the APNs send route under the approval_device_cutover_frontier in gunbc.auth.approval_device_redemption, and I will name it there when rebasing.

— sent from swift-ibex-621

Brian Searls and others added 4 commits September 18, 2026 07:32
…ective framing, App Attest current checks

- AdmittedDeviceRedemption and VerifiedDeviceEnrollment are sole_constructor; the decision
  commit consumes only the admitted value, and the login comes from the code's issuance.
- Enrolment code, enrolment and revocation are generations of ONE CAS slot, so consuming the
  code is creating the enrolment; expired, reused, other-bytes and Android refuse.
- Every signed/MAC'd message is length-framed (code-point counts): injective for any field
  content, where the unit-separator join was not (capability_text itself contains it).
- capability_tag_hex names the tag's real encoding.
- App Attest: AppIdPrefix (not team id), validation category and bundle version refusals,
  seam-minted VerifiedAttestation/VerifiedAssertion carrying key, receipt and client data;
  redemption joins the assertion to the enrolled attest key and the exact signed bytes.
- APNs: top-level custom-data carrier; a 200 without apns-id is undecodable.
- Reads that return capabilities are assertion-authenticated; residual stated.
- std.measure Second/ByteSize for APNs ceilings and signature sizes (review 67564).
- Unconsumed declarations removed or given named consumers; Android attestation corrected;
  iOS realization frontier added; stale approval_push references fixed.
- gunbc.auth.approval_device_redemption_fixtures renders the cross-language vectors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…named in their frontiers

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… split wire contract into approval_device_wire

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto the revised #11585 seams (merge commits da8e3a6 and 33575e6). The sizes on the three refusal arms I added are now ByteSize (byte_size(count: …)). apns_provider_token is named in approval_device_routes_frontier as its consumer, and #11592 deletes ecdsa_verification_realization_frontier because App Attest now realizes the rest of it. Re-run on the merged tree: p256_ecdsa_witness_test 11/11. #11585's own findings from review 67577 were addressed on that branch (the unconsumed declarations and the dangling signature_verification_succeeded were removed there).

— sent from swift-ibex-621

…mption_fixtures regen)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

Re review 67600:

  1. v1 seed growth without an admission row. I checked this against gunbc.v1_maintenance_standing and agree the finding is real. Two new builtins make the seed more capable, and the purpose test in that carrier (serves the v2 self-host program) does not cover a product feature. The brief ordered v1 primitives on the hmac_sha256_hex precedent (40fb5b9), which carries no row either. I can't author an admission arm myself, because the carrier names a self-authored boundary change as exactly the danger. So I've escalated it for an operator ruling, recommending that the ruling be recorded on the carrier in this PR. Signed device redemption A2: App Attest attestation + assertion verifiers #11592's two App Attest builtins are the same class and wait on the same ruling.
  2. Prose-only *_note: NonEmptyStr rows (§4c). None of the rows cited is added by this lane. They are Model signed device redemption for approvals (APNs, Secure Enclave, App Attest, ECDSA P-256) #11585's (signature.dag verification_key_cannot_sign_note, the apns/app_attest/fcm/key_attestation notes, and approval_device_redemption enrolment_code_residual_note). I've forwarded this to that branch's owner.

— sent from swift-ibex-621

@gunbai-bot

gunbai-bot Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

Re review 67617:

  1. Unenumerated hand-Rust / SeedFeatureCompletion. I verified this and agree. The diff touches neither gunbc.seed_growth_admission nor gunbc.v1_maintenance_standing. The fix has two parts:

    • Admission. Whether this purpose is admitted at all is an operator decision under v1_maintenance_standing, and it's escalated (same as review 67600).
    • Enumeration. Once admitted, the hand items still need enumerating in seed_growth_justification_roster: identity, .dag authority, why Rust, owning lane, deletion trigger, boundary. That means one SeedGrowthJustification per PR, covering the builtin arms, helpers, tests and crate edges. Its deletion trigger comes from the ruling, so both land together in this PR.

    On keeping ECDSA/JWT at the seam only, as App Attest was: the brief asked for a working verifier and signer. A seam with no implementation leaves SignatureVerified unmintable from real bytes, which was the state before this PR.

  2. The frontier name now means something different. Correct, and my edit caused it: I narrowed the description to App Attest and left the name ecdsa_verification_realization_frontier. It will be renamed to app_attest_verification_realization_frontier in this PR, and the reference in the module comment will change with it. (Signed device redemption A2: App Attest attestation + assertion verifiers #11592 then deletes the frontier once App Attest realizes it.)

Both edits are held with the PR until the ruling is relayed.

— sent from swift-ibex-621

gunbc-ci-auto-heal and others added 2 commits September 18, 2026 08:44
…_realization_frontier (review 67617)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…itable, not validated (review 67625)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

Re review 67625:

  1. issued_at_epoch: Int re-mints EpochSecs. Verified and fixed in bdf846f. ApnsProviderToken.issued_at_epoch and the apns_provider_token parameter are now std.types EpochSecs. The < 0 check and the ApnsIssuedAtBeforeEpoch arm are deleted, along with the witness that exercised them, because that state is no longer writable. The host primitive keeps Int at its boundary, as you suggested, and still answers absent on a negative value. p256_ecdsa_witness_test re-run: 10/10.
  2. Hex in ApprovalCapability.tag_b64url (gunbc.auth.approval_device_redemption). This line is in Model signed device redemption for approvals (APNs, Secure Enclave, App Attest, ECDSA P-256) #11585's module, not this lane's, so I've forwarded it to that branch's owner rather than forking their construction here.

— sent from swift-ibex-621

@gunbai-bot

gunbai-bot Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

Re review 67641: this is the same admission finding as reviews 67600 and 67617. It's escalated for an operator ruling, and the PR is held for it. The ruling will be recorded on gunbc.v1_maintenance_standing in this PR, together with a drafted gunbc.seed_growth_admission SeedGrowthJustification that enumerates the arms, helpers, tests and crate edges. Its deletion trigger is 'a non-v1 host route binds RustCrypto'.

I checked the one new point, ArmEnumeration DeclaredHere { dissolution }, and I'm not taking it. In gunbc.v1.v1_interpreter_primitive_surface that arm records how an arm is enumerated, not a debt: it covers dispatch that is not an arm of a roster macro (its two rows are eval_method_call's if-guard short-circuit and eval_service_call's checkout carve-out). Both new arms are arm "free_call.…" entries in the v1_builtin_arms roster, so AuthoredInRoster is the true value. Marking them DeclaredHere would record a false fact about the dispatch to carry the dissolution. The dissolution belongs on the SeedGrowthJustification trigger.

— sent from swift-ibex-621

Brian Searls and others added 8 commits September 18, 2026 10:30
- Admission takes the signing input plus seam-minted verdicts; SignedRedemption, EnrolmentRequest,
  PresentedEnrollmentEvidence and PushRegistration are wire bodies in approval_device_wire, with
  their deferred consumer stated once; RedemptionIdentityEvidence deleted.
- Capability tag carried in its canonical base64url spelling and converted through
  capability_tag_hex (refusing a non-canonical spelling); witnesses start from issue_capability and
  refuse a hex tag placed in the base64url field.
- App Attest assertion result renamed AssertionAuthentic and scoped to the checks it makes; counter
  and challenge left to the consumer, whose replay authority is stated; stored public key joined.
- APNs: only Apple's documented priorities (10, 5); push type scoped to alert.
- Read transcript vectors and a discriminating witness; enrolment store (code/enrolment/revocation
  as one CAS slot) with record round-trip witnesses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	dag/gunbc/auth/approval_device_redemption.dag
…ane A)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ks already carry them

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…expectation typed

- vectors.json is gunbc.generated_artifact ApprovalDeviceVectorsArtifact (new RepoConsumer
  CrossLanguageClientTest), so the required generated-artifact phase and the refusing merge driver
  gate it; the module's own regen/agree are deleted as a second route.
- device_store_write takes std.durable_compare_and_set CasExpectation; the Int decode and its
  absorbing else are gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…7674)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… owner (side-chat condition)

The issuer takes no login: issue_enrolment_code writes approval_operator_login. No HTTP issuer,
since the loopback dashboard is reachable by every on-host POSIX user; the enrolment POST only
consumes an already-issued code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	dag/gunbc/auth/approval_device_redemption.dag
@gunbai-bot

gunbai-bot Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

CI on 307a21d: required-witnesses-floor failed with function 'concat_lists' not found in scope at gunbc.auth.approval_device_redemption enrolment_members / revocation_json. That is #11585's module, and it was fixed on that branch in df7fa53 (concat replaces the bare concat_lists). Merged into this branch as 1cf415f. On the merged tree (checked on #11592's branch, which contains it), the local runs are: approval_device_redemption_witness_test 30/30, p256_ecdsa_witness_test 10/10, app_attest_witness_test 17/17. The required-witnesses-build lane was already green on 307a21d.

— sent from swift-ibex-621

…fact (from CI heal bundle)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gunbc-ci-auto-heal and others added 3 commits September 18, 2026 13:46
…6_jwt_sign; the JWS is assembled in .dag (review 67702)

The host kernel is only the P-256 signature. The APNs provider token's header, claims,
base64url segments and compact form are now assembled from
extdeps.languages.json.emit, a new extdeps.auth.jws (RFC 7515) and
extdeps.crypto.signature p256_ecdsa_sign, and the token is typed as
extdeps.auth.jwt JwtCompactSerialization. The unreachable issued_at < 0 check goes
with the old primitive.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

Re review 67702: both findings verified and fixed (head 7da0a5e).

  1. Wrong grain on es256_jwt_sign. It's replaced by p256_ecdsa_sign_b64url(p8_pem, message) -> String?, the signing twin of the verify primitive: the 64-octet r || s as unpadded base64url, and nothing else. The .dag now owns the JWS:
    • a new extdeps.auth.jws (RFC 7515): jws_base64url (§2, std.encoding UrlSafe with the padding omitted), jws_signing_input (§5.1) and jws_compact (§7.1), typed extdeps.auth.jwt JwtCompactSerialization
    • extdeps.crypto.signature p256_ecdsa_sign -> SignatureSigned { SignatureBytes } | SigningKeyUnreadable
    • apns_provider_token builds the header and claims with extdeps.languages.json.emit json_object, and ApnsProviderToken.jwt is now JwtCompactSerialization.
  2. Unreachable issued_at < 0. Removed along with the old primitive.

Because the emitter serializes {"alg": "ES256", "kid": …} with its own separators, the expected token changed. The new signing input was computed independently in Python, and the RFC 6979 signature was verified with openssl dgst -sha256 -verify against the RFC 7515 A.3 public key (a one-byte change fails).

Re-run: p256_ecdsa_witness_test 12/12, including two new witnesses on p256_ecdsa_sign (sign then verify under the RFC public key; unreadable key). approval_device_redemption_witness_test 30/30. --required-regen: first_generation_equal=true. This head also carries #11585's latest, which includes the vectors.json staging line the heal job wanted.

— sent from swift-ibex-621

# Conflicts:
#	.gitattributes
#	.github/workflows/witnesses.yml
#	dag/gunbc/generated_artifact.dag
#	dag/gunbc/generated_artifact_emit.dag
@gunbai-bot
gunbai-bot Bot marked this pull request as draft September 18, 2026 17:42
@gunbai-bot

gunbai-bot Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

Moved to draft. It will not merge as v1 host builtins. The operator ruled on 2026-09-18, relayed by the lane owner, verbatim: "basically if you're asking if we can use the rust escape hatch the answer is no - i wuld just take the chance to model them up front - you're just kicking the can down the road". The crypto is being modeled in .dag instead: bitwise emulation, a multi-limb bignum, SHA-256 and P-256 ECDSA, then RFC 6979, P-384, DER/X.509, CBOR and App Attest. It sits behind the same seams (signature_verification_from_implementation, the extdeps.apple.app_attest verification seams), and the .dag implementation becomes their only producer. The .dag-side modeling here carries over: the refusal vocabulary, extdeps.auth.jws, the typed answer coproducts, the pinned root anchor, and the witnesses with their genuine-device and RFC 7515 vectors. This PR will be closed when the replacement lands.

— sent from swift-ibex-621

Copy link
Copy Markdown
Contributor

Operator direction now forbids this v1/RustCrypto escape-hatch route. Keep this PR draft and non-landable. The replacement program is #11628 atop the modeled numeric/bit substrate #11627 and pure codec work #11629: implement SHA/HMAC/P-256/ES256 in .dag, switch the real consumers, then close this PR. Preserve this PR's independent vectors and mutation controls as evidence; do not preserve its interpreter/runtime implementation.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 20, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-20T15:11:30.171490Z 8c6e454 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@gunbai-bot

gunbai-bot Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor Author

Closing: this implements the approach the operator REFUSED. These PRs add P-256/ES256 (and App Attest) as v1 HOST PRIMITIVES in src/v1/stage0/src/v1_interpreter.rs -- the Rust escape hatch. The operator's ruling was explicit: 'if you're asking if we can use the rust escape hatch the answer is no - i would just take the chance to model them up front - you're just kicking the can down the road.' The replacement is #11645, which implements the same primitives in .dag (std.bitwise, std.bignat, extdeps.crypto.sha2, extdeps.crypto.nist_p256, std.bounded_nat) and DELETED these interpreter arms, their registrations and the Cargo edges in the same change. The model half of this work already landed separately in #11585 (extdeps.apple.apns, secure_enclave, app_attest), so nothing here is unique except the retired realization. Leaving these open is a hazard: they are large (+2896 / +4410), they touch files main already carries, and a future reader could merge a refused approach that reintroduces host crypto. Closing rather than leaving stale. If any specific piece here turns out not to exist in #11645 or #11585, reopen with that piece named.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant