CIRISVerify v14.0.0 is tagged (5cc6dab). Persist adopted it in v38.7.0 and hit three things — filing so you don't rediscover them. Two of them are not in the release's migration note, which says only "add a _ arm; pass None for valid_until; bytes are unchanged."
That note is accurate for a consumer that never binds an expiry. It was not sufficient for persist, and the second item below is the one that would have shipped a silent break.
1. non_exhaustive error enums — the mechanical half
31 error enums gained #[non_exhaustive], so exhaustive matches need a _ arm. Persist patched three sites. Cheap, and the compiler finds them all.
Where the wildcard goes matters more than that it exists. In persist's conformance test the enumerated list was deliberately exhaustive so a new variant would be a compile error demanding a decision — its own comment warned that a wildcard "would classify it silently, as convergence, which is the direction that goes green". So those arms panic rather than classify: the signal moved from compile time to test time without losing its loudness. Worth checking whether any of your matches on verify errors are load-bearing in that way before adding a permissive _.
For a runtime mapping the opposite applies — persist's maps an unknown variant to its most conservative refusal class, because a variant this build has never seen is a verification failure whose meaning we cannot claim to know, and mapping it to anything narrower would let a future verify release widen what we admit without us deciding to.
2. valid_until is now a BOUND subject member — the half that bites
This is the one to actually check. verify's KeyRecord::check_subject_binding now requires valid_until in the subject binding. If you mint or verify registration envelopes, a producer that does not stamp it produces envelopes verify's own checker rejects.
Persist caught this only because a conformance test compares the two projections member-for-member; it reported only ciris_verify_core binds: ["valid_until"]. Without that test it would have shipped as "every chain we mint stops rooting."
Note the asymmetry, which is deliberate on verify's side: KeyRecord binds valid_until; the provenance link does not (it has no field for it). A consumer minting one envelope for both checkers carries the union, so equality assertions between the two planes now fail for a correct producer.
If you build subject bindings yourself, adopt verify's type-agnostic SubjectBinding builder rather than re-spelling the members. Persist had a parallel spelling with a comment promising the two "must not diverge" — they diverged the moment verify added a member. A promise in a comment is not a mechanism; built through the builder, the next member arrives without an edit.
3. RFC-3339 spelling — one instant, two texts, compared as strings
The release note says valid_until is "canonicalized to one text form". That form is …T00:00:00Z. Persist rendered its typed column with chrono's plain to_rfc3339(), which writes …T00:00:00+00:00.
Same instant. Different bytes. Compared as strings, they disagree — so persist refused a record whose envelope named the very instant its own row carried, and the refusal message said the signers "never named" a subject they did name.
Check every place you render a timestamp into, or compare one against, a signed envelope. Persist's fix compares valid_until as an instant (parse both sides) while every other member stays byte-equal, because for an opaque identifier or base64 byte equality IS the semantics and normalising would be a way to admit something the signers did not write.
Exposure
- Persist: 7 Cargo pins flipped together, plus
pyproject.toml's Requires-Dist — that one is separate from the Cargo pins and has been missed on past verify majors.
- A repo-wide grep found no
match on verify's error enums in your src/, so item 1 may cost you nothing. Items 2 and 3 depend on whether you mint or check subject bindings.
Happy to answer specifics — persist is the first adopter and has the scar tissue.
CIRISVerify v14.0.0 is tagged (
5cc6dab). Persist adopted it in v38.7.0 and hit three things — filing so you don't rediscover them. Two of them are not in the release's migration note, which says only "add a_arm; passNoneforvalid_until; bytes are unchanged."That note is accurate for a consumer that never binds an expiry. It was not sufficient for persist, and the second item below is the one that would have shipped a silent break.
1.
non_exhaustiveerror enums — the mechanical half31 error enums gained
#[non_exhaustive], so exhaustivematches need a_arm. Persist patched three sites. Cheap, and the compiler finds them all.Where the wildcard goes matters more than that it exists. In persist's conformance test the enumerated list was deliberately exhaustive so a new variant would be a compile error demanding a decision — its own comment warned that a wildcard "would classify it silently, as convergence, which is the direction that goes green". So those arms panic rather than classify: the signal moved from compile time to test time without losing its loudness. Worth checking whether any of your matches on verify errors are load-bearing in that way before adding a permissive
_.For a runtime mapping the opposite applies — persist's maps an unknown variant to its most conservative refusal class, because a variant this build has never seen is a verification failure whose meaning we cannot claim to know, and mapping it to anything narrower would let a future verify release widen what we admit without us deciding to.
2.
valid_untilis now a BOUND subject member — the half that bitesThis is the one to actually check. verify's
KeyRecord::check_subject_bindingnow requiresvalid_untilin the subject binding. If you mint or verify registration envelopes, a producer that does not stamp it produces envelopes verify's own checker rejects.Persist caught this only because a conformance test compares the two projections member-for-member; it reported
only ciris_verify_core binds: ["valid_until"]. Without that test it would have shipped as "every chain we mint stops rooting."Note the asymmetry, which is deliberate on verify's side:
KeyRecordbindsvalid_until; the provenance link does not (it has no field for it). A consumer minting one envelope for both checkers carries the union, so equality assertions between the two planes now fail for a correct producer.If you build subject bindings yourself, adopt verify's type-agnostic
SubjectBindingbuilder rather than re-spelling the members. Persist had a parallel spelling with a comment promising the two "must not diverge" — they diverged the moment verify added a member. A promise in a comment is not a mechanism; built through the builder, the next member arrives without an edit.3. RFC-3339 spelling — one instant, two texts, compared as strings
The release note says
valid_untilis "canonicalized to one text form". That form is…T00:00:00Z. Persist rendered its typed column with chrono's plainto_rfc3339(), which writes…T00:00:00+00:00.Same instant. Different bytes. Compared as strings, they disagree — so persist refused a record whose envelope named the very instant its own row carried, and the refusal message said the signers "never named" a subject they did name.
Check every place you render a timestamp into, or compare one against, a signed envelope. Persist's fix compares
valid_untilas an instant (parse both sides) while every other member stays byte-equal, because for an opaque identifier or base64 byte equality IS the semantics and normalising would be a way to admit something the signers did not write.Exposure
pyproject.toml'sRequires-Dist— that one is separate from the Cargo pins and has been missed on past verify majors.matchon verify's error enums in yoursrc/, so item 1 may cost you nothing. Items 2 and 3 depend on whether you mint or check subject bindings.Happy to answer specifics — persist is the first adopter and has the scar tissue.