Harden 51Did parsing to answer with a reason instead of throwing - #27
Merged
Conversation
The OWID Rust library was hardened so an OWID reaches a caller only from a successful non-throwing read or from a creator that signs it. The crates.io release is 1.0.0 from before that work and the hardening branch is version 2.0.0, so a [patch.crates-io] entry cannot apply and the workspace takes owid as a git dependency pinned to the branch tip. The pin is temporary and will move to the merged commit. crates.io refuses git dependencies, so the fodid crates cannot be published until a 2.x owid release exists, and the publish script comment says so. A failed read is now Error::Parse carrying the OWID ParseError unchanged, so a caller branches on its status rather than on message text, and the 51Did payload rules answer with two named statuses of their own. PayloadTooShort means the payload cannot hold the five byte header, and the new InvalidTypePayloadLength means the header was read and the value the identifier type requires is missing. Longer payloads are accepted and left in place, because a longer identifier is a newer shape rather than a fault, and no read touches a key, so the docs now say a parsed FodId is not necessarily cryptographically valid. Every reading route runs the one walk. The tests assert the three facts of each result (succeeded, value only on success, status) across longer domains and payloads, each too short case, the OWID statuses passed through, and a structurally valid identifier that parses and then verifies as SignatureInvalid. fodid-cloud moves to the 2.0 accessors and names the reader's reason in its no-value message.
The README now explains reading against verifying, the reasons a read can fail and what each status means, the type specific lower bounds and the absence of an upper bound, and a before and after for callers of the owid 1.0 surface. Its code blocks are complete functions included through a doctest only item in lib.rs, so they are compiled and run on every cargo test and cannot quietly drift from the code.
fodid depended on an owid crate, first the 1.0.0 release on crates.io and then, after the OWID hardening, a git revision that crates.io refuses. Neither arrangement can stand. The crates.io release sits on a personal account, which cannot be the owner of a package 51Did depends on, no package is to be managed under SWAN, and OWID will move to Prebid, so the fodid crates must not depend on any owid package from any registry or repository. The OWID source now comes from the owid-rust submodule, the 51Degrees fork pinned at its main head, which is the squash merge of the hardening. ci/copy-owid-source.ps1 copies owid-rust/src into fodid/src/owid as a private module before a build. On the way in it renames lib.rs to mod.rs, prefixes crate:: paths with the module name and points the documentation examples at fodid, and it writes a NOTICE naming the commit beside the library's Apache 2.0 LICENSE. The copy is ignored by git and listed in the fodid manifest's include, so cargo package carries it. The OWID dependencies move into fodid without the fetch and endpoints features, so nothing reaches the network and the wasm build of fodid-cloud still works. This is the arrangement the .NET and Python 51Did packages already use. fodid re-exports every OWID type its public surface reaches, and the creator types behind an off by default creator feature that the tests and the example turn on, so a caller never needs an owid crate. The OWID library's own unit tests and documentation examples now run as part of fodid's suite.
The pull request workflow's native job already checks out submodules and now runs ci/copy-owid-source.ps1 before fmt, build, test, clippy and doc. The wasm job and the publish workflow check out the owid-rust submodule and run the same script before building. The publish script comment now says the fodid crates need no OWID crate on any registry, where it said they were waiting for one.
The repository README and AGENTS.md said fodid consumed owid from crates.io or as a git dependency. Both now say the source is compiled in from the owid-rust submodule, name the submodule URL, say that no OWID package has to exist on any registry, and give the two commands a fresh clone needs before building.
This was referenced Aug 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed and why
The OWID libraries were hardened so that an OWID reaches a caller only from a successful non-throwing parse or from a creator that signs it. The Rust port of that work is SWAN-community/owid-rust#4, and it is a breaking change to the
owidcrate. Thefodidandfiftyone-fodid-cloudcrates in this workspace build on that crate, so this pull request adapts them to the hardened surface and hardens 51Did parsing in the same spirit, meaning a read answers with a named reason instead of a message, never checks a signature, and never rejects a longer identifier merely for being longer.There is no
DidClientand no 4096 character input guard in this repository, so those parts of the shared brief do not apply here. Nothing in this workspace fetches a key during a read, and thefodidcrate does not enable the owidfetchfeature, so no HTTP code is compiled into it.The 51Did read contract (
fodid)FodId::from_base64,FodId::from_byte_array,FodId::from_owid,FromStr,TryFrom<&[u8]>andTryFrom<Owid>all run one walk, in this order:ParseErroris carried unchanged infodid::Error::Parse, and.status()on it names the OWID reason (MissingInput,InvalidBase64,UnexpectedEnd,ByteCountMismatch, and so on). No OWID status is mapped down to a generic one and no message text is inspected.Error::PayloadTooShort.Randomand 32 hash bytes forProbabilisticandHashedEmail, otherwise the newError::InvalidTypePayloadLength { id_type, expected, actual }.Reservedkeeps the existing best effort read. The minimums are the existingHEADER_LENGTH,RANDOM_PAYLOAD_LENGTHandPAYLOAD_LENGTHconstants, no new lengths were introduced.FodIdis returned without any signature check. The documentation now says in several places that a parsed 51Did is not necessarily cryptographically valid, and points atverify_status_with_public_key, whoseSignatureStatuskeeps a signature that does not match apart from a key that could not be read or obtained.Rust already has a non-throwing
Resultsurface, so notry_parsealias was added. TheErrorenum itself is the status, and a caller branches on the variant.Error::Owid(owid::Error)remains for the exceptional case, which is a caller using?on an OWID operation of a parsed value (serialising it again, verifying with an unreadable key). A read never produces that variant, and theFrom<owid::Error>conversion unwraps anowid::Error::Parseback intoError::Parseso a failed read has one representation whichever route a caller's?took.Before and after for a caller
The full migration notes are in the
fodidcrate documentation and README.fiftyone-fodid-cloudThe engine already turned a failed read into a no-value rather than a failure. Its message now reads "The 51Did value could not be read: ..." followed by the reason from the reader, because a payload that is too short for its type is not an envelope decode failure. The tests and their comments moved from
sign_bytestocreateand from field access to accessors.Public API changes
fodidError::Parse(owid::ParseError)where it wasError::Owid(owid::Error).Error::Owidremains for OWID operations other than reading.Error::InvalidTypePayloadLength { id_type: IdType, expected: usize, actual: usize }.From<owid::ParseError> for Error.fodid::ParseError,fodid::ParseStatus,fodid::SignatureStatusalongside the existingfodid::Owid, so a caller can branch on statuses without a directowiddependency.from_base64,from_byte_array,from_owid,FromStr,TryFrom, the accessors, the offset and length constants. TheDeref<Target = Owid>now reaches accessors (domain(),date(),payload(),signature(),version()) rather than public fields, which is the owid 2.0 change.fiftyone-fodid-cloudWhere the OWID code comes from
No
owidcrate is taken from crates.io or from git any more. James Rosewelldecided that no OWID package is to be owned by a personal account or managed
under SWAN, and that OWID will move to Prebid, so the
fodidcrate must notdepend on an OWID package at all. The OWID source is compiled into
fodidasthe private module
fodid::owid, copied before every build byci/copy-owid-source.ps1from the newowid-rustgit submodule, which pointsat the 51Degrees fork (https://github.com/51Degrees/owid-rust) at the merged
hardening commit
bbdb7aa4on its main branch. This is the same arrangementas the .NET package, which compiles the owid-dotnet source into
FiftyOne.Did.dll, and the Python package, which copies owid-python in as a
private module.
The copy is git ignored, carries a NOTICE naming the exact submodule commit
and the library's Apache 2.0 LICENSE, and is packaged into the published
crate through an explicit
includelist in the manifest, proven withcargo package --list -p fodid. The OWID types a caller needs (Owid,ParseError,ParseStatus,SignatureStatus,Cryptoand the rest) arere-exported from
fodid, and the two that create and sign a new envelope sitbehind a new
creatorfeature, off by default, so the reading crate compilesnothing that signs unless a test or tool asks for it. The publish workflow
runs the copy script before
ci/publish-crates.sh, whose comment now recordsthe arrangement, and the fodid crates are publishable on their own with
nothing to release on crates.io first. Nothing has been published from this
work. The existing
owid 1.0.0release on crates.io is still required by thealready published
fodid 4.5.2, and what to do with that release is aseparate decision.
Tests
Counts are
fodidplusfiftyone-fodid-cloud, with the two live cloud tests ignored as before.fodiddoctests, README examples included.The
fodidREADME is now compiled and run as documentation tests through a#[cfg(doctest)] #[doc = include_str!("../README.md")]item, which is where three of the five doctests come from, so the README examples are checked against the code on everycargo test. Theparse_and_verifyexample was run and printssignature : SignatureValid.New or reworked cases in
fodid/tests/fodid_tests.rs, each asserting the three facts (succeeded or not, value present only on success, status read off theErrorvariant and never off message text):Probabilistic,HashedEmailandRandompayloads areInvalidTypePayloadLengthwith the type and the existing minimum reported.PayloadTooShortwhatever the flags byte says.InvalidBase64, empty string and empty buffer areMissingInput, a truncated envelope isUnexpectedEnd, an unknown version byte isUnsupportedVersion.ByteCountMismatch, carried with its detail, and the 51Did rules are never reached.SignatureStatus::Invalid, and so does one checked against another creator's key.SignatureStatus::InvalidKey, neverInvalid, and theResultform of that check is an error taken intoError::Owidrather thanOk(false).FromStr,TryFrom<&[u8]>andTryFrom<Owid>report the same statuses as the direct functions.Displaynames the status andsource()keeps the OWID parse error.fiftyone-fodid-cloudgains a test that a well formed OWID whose payload is shorter than the 51Did header becomes a no-value whose message namesPayloadTooShort.Neutralisation
Each new check was disabled in turn (
if false && ...), thefodidsuite run, and the check restored.payload_shorter_than_the_header_is_payload_too_short,constructor_from_owid_short_payload_errors,constructor_from_bytes_short_payload_errors,the_other_reading_routes_report_the_same_statuses,error_display_names_the_status_and_keeps_the_owid_source).probabilistic_payload_one_byte_short_is_invalid_type_payload_length,hashed_email_payload_one_byte_short_is_invalid_type_payload_length,random_payload_shorter_than_guid_is_invalid_type_payload_length,random_payload_with_only_the_header_is_invalid_type_payload_length,constructor_from_owid_short_payload_errors,constructor_from_bytes_short_payload_errors,the_other_reading_routes_report_the_same_statuses,error_display_names_the_status_and_keeps_the_owid_source).Whole workspace
Across the whole workspace,
cargo fmt --all -- --checkis clean,cargo clippy --all-targets --all-features -- -D warningspasses (exit 0, every test target compiled),RUSTDOCFLAGS=-D warnings cargo doc --all-features --no-depspasses (exit 0), and the CI wasm steps forfiftyone-fodid-cloud(cargo buildandcargo clippy -D warningswith--no-default-features --target wasm32-wasip1) pass.cargo test --all-featuresover the whole workspace could not be completed on the machine used, because the build drive ran out of space part way through linking the native on-premise crates (0 bytes free, error 112, unrelated to the code). Onlyfodidandfiftyone-fodid-clouddepend onowidorfodid, so the other crates' tests cannot be affected by this change, and the two changed crates were tested from a clean build after the disk was recovered (67 passed, 2 ignored). CI runs the full suite on this pull request.Checked with no issue
sign_bytes,sign_string,Owid::new, public field access onOwid). The only uses were infodid,fiftyone-fodid-cloudand theparse_and_verifyexample, all fixed. Theexamples/workspace does not usefodidon main.cargo fmt --all -- --checkclean.What remains
FodId::hashtomatch_key; this pull request does not build on it, so whichever merges second will need the tests and docs reconciled. #21 was not touched.owid 1.0.0release on crates.io can be yanked, which is James Rosewell's call as its owner.Produced with AI assistance under James Rosewell's direction and needs human review.