Add the dated public key fetch and the key schedule PHP had none of - #3
Merged
Conversation
The library could verify only with a key the caller had already obtained, so every application had to build the request to a creator's well known end point, choose the key in force on the OWID's date, cache and time out on its own. The other ports carry that inside the library, and the specification (SWAN-community/owid#7) says a verifier sends the OWID's own date every time. PublicKeyFetch asks /owid/api/v{n}/public-key?date={minutes}&format=pkcs on the domain the OWID carries, at the version the OWID carries, through the HTTP stream wrapper with a ten second timeout. Keys are held per request URL up to 1024, a domain that is not a host name or a scheme that is not HTTP is refused before anything is opened, and a key that cannot be had or read answers KeyUnavailable or InvalidKey rather than a forgery. A caller may pass a transport of its own, which is the route for a host that turns off remote stream access. PublicKeySchedule and DatedPublicKey choose the key in force at a date out of a published schedule, being the latest start at or before the date, with current() the key in force now and last() documented as not that. Endpoints::publicKeyResponseAt answers the date parameter for a creator that rotates its key as the specification requires. Io::minutesSinceBase shares writeDate's arithmetic. OwidException is no longer final so that PublicKeyFetchException can carry the status, the domain and the code. The core keeps no network access: the contract test that scans the source for ways to reach the network now names PublicKeyFetch as the one class allowed to, and checks nothing else refers to it. The tests drive the real fetch against a stand in for the end point, PHP's built in web server on the loopback address serving the published 51d.es schedule, and check the choice of key against a genuine identifier the 51Degrees cloud issued on 4 September 2026, the fixtures the Java port uses. The plain runner gained the same checks. 151 PHPUnit tests and 142 plain runner checks pass, README examples included.
jwrosewell
added a commit
to 51Degrees/pipeline-php-did
that referenced
this pull request
Sep 6, 2026
…s do (#11) Moves the owid-php submodule onto the 51Degrees fork head that carries the dated public key fetch and the key schedule, which the PHP port had none of until SWAN-community/owid-php#3. DidClient itself needs nothing from that, because it fetches the whole published schedule from the cloud's own key endpoint with a resource key and selects by the start of a key's period itself, with a fifteen minute boundary tolerance the OWID library does not model. The two are deliberately different and both are correct. What this package was missing is the detailed answer. Comparing the four clients, .NET, Java and Python all report which of five things happened when an offline check does not verify, and PHP alone answered a bare false. A caller could not tell a forged identifier from one this package never checked, so an outage in the key endpoint and a genuine attack read the same, and the safe reading of a bare false is the one that reports your own outage as an attack. SignatureCheck names the five outcomes, and verifySignatureDetailed returns one. Only Verified says a key was tried and matched. Only Invalid says a key was tried and did not. NoKeyForDate, UnsupportedVersion and InvalidLength all say the signature was never examined. verifySignature now returns whether the outcome is Verified, so the two can never disagree, and every existing caller keeps the answer it had. The backing string of each case is the case name, which is the convention FodIdParseStatus already states, being that the string is the cross language name of the outcome so it can be logged or carried between services. Worth noting for whoever aligns the rest: the same five outcomes are spelled three different ways across .NET, Java and Python today, so that convention is not yet met across the packages. Changing those is a breaking change in two of them and is left alone here. Adds hasLicenceKey, which Java and .NET already have and this did not. Verified with the submodule at 247178f4: 125 tests, 695 assertions, 2 skipped, up from 117. The five outcome tests were each proved to fail when the outcome they pin is changed in DidClient.
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.
Summary
Adds to the PHP port the two things every other port now carries and this one did not: fetching a creator's public key from the well known end point for the date an OWID carries, and choosing a key out of a published schedule by the start of its period. The core keeps its promise of no network access, because the fetch lives in
PublicKeyFetch, is loaded only by a caller that uses it, and the existing contract test that scans the source for ways to reach the network now names that one class as the exception and checks nothing else refers to it.Why
This port was written with key fetching scoped out ("Fetching a creator public key over HTTP is out of scope"), so every application had to build the request, choose the key in force on the OWID's date, cache and time out on its own. The point of the library is that this is done once per language. The specification says where the key is and how to ask for it: the OWID's Domain field plus
/owid/api/v{n}/public-key, sending the OWID's own date, with the key in force being the one with the latest start at or before that date (SWAN-community/owid#7). The Java port gained the same two pieces on 5 September 2026 in SWAN-community/owid-java#6, and this change mirrors that shape.What changed
src/PublicKeyFetch.php(new):publicKeyUrlbuilds{scheme}://{domain}/owid/api/v{n}/public-key?date={minutes}&format=pkcsat the version the OWID carries;publicKeyPem,signatureStatus,verifyandclearCache. The request goes through the HTTP stream wrapper PHP ships with, so there is no new dependency. Keys are held per request URL up to 1024 before the store is emptied, each request waits at most ten seconds, a domain that is not a host name and a scheme that is not HTTP are refused before anything is opened, a body beyond 64 KiB is not a key, and a key that cannot be had or read answersKeyUnavailableorInvalidKeyrather than a forgery. A caller may pass a transport of its own, which is the route for a host that turns off remote stream access.src/PublicKeyFetchException.php(new) carries the status, the domain and the response code.src/OwidException.phpis no longerfinalso that it can be extended.src/DatedPublicKey.phpandsrc/PublicKeySchedule.php(new):keyInForce,keyFor,current,last,signatureStatusandverify. Two keys sharing a start settle in favour of the first supplied, as the cloud does. Starts are held in UTC.src/Endpoints.php:publicKeyResponseAtanswers the date parameter for a creator that rotates its key as the specification requires, being the key in force at the date, the key in force now for an undated or future date, 404 when nothing is in force and 400 for a date that is not a count of minutes.src/Io.php:minutesSinceBase, sharingwriteDate's arithmetic.tests/PublicKeyFetchTest.phpdrives the real stream wrapper path against a stand in for the end point (tests/KeyEndPoint.phprunning PHP's built in web server withtests/key_end_point_router.php) that answers as the cloud does, with the request moment fixed at 14 September 2026 so an undated request is served a key other than the one that signed the fixture.tests/PublicKeyScheduleTest.phpandtests/PublicKeyResponseAtTest.phpcover the choice of key, including the batch generated on 1 September that broke the .NET port.tests/ParseContractTest.phpkeeps the no network promise with the fetch as the one named exception.tests/run.php, the plain runner, loads the new classes and checks the same behaviour through a transport of its own. The fixtures undertests/dataare the genuine identifier the 51Degrees cloud issued on 4 September 2026 and the thirty published 51d.es keys, the same files the Java port uses.ReadmeTestruns and checks, the interface list, and the testing notes.Verification
php vendor/bin/phpuniton PHP 8.5 with PHPUnit 10.5: 151 tests, 8689 assertions, OK. Before this change the suite held 109, so 42 tests are new.php tests/run.php: 142 checks passed, 9 of them new..invalidname space and answersKeyUnavailable; the schedule one verifies the README's own identifier under the key in force on its date.Notes
DidClient) fetches the whole schedule fromid/keywith a resource key and selects by start itself, so it is unaffected. The 51Degrees fork and pipeline-php-did's submodule pin can follow this merge.51d.es, the creator named in a 51Did identifier, the fetch answersKeyUnavailabletoday, because that creator requires a resource key or licence key on both mandatory end points and answers 401 without one, which I checked against production on 5 September 2026. The Authentication section of the specification allows a creator to do that and requires a client to be prepared for it, so the creator is conformant and so is this port, whose duty there is to report the key as unavailable rather than the signature as forged. Presenting a credential is a separate matter, and only the JavaScript port has an answer to it today throughowid.fetchHeaders. This port follows the other five in leaving it to a transport the caller supplies. Bringing the six into line with JavaScript needs one decision taken across them, because the specification lets each creator choose its own header and parameter names.Produced with AI assistance and checked against the outputs above.