Repository navigation
Add the dated public key fetch and the key schedule Java had none of - #6
Merged
Merged
Conversation
Java was the only port with no way to obtain a creator public key, so
every integrator had to build the request and choose the key themselves,
with the three traps the other ports each fell into still open to them.
PublicKeyFetch builds and performs the well known request, being
/owid/api/v{n}/public-key?date={minutes}&format=pkcs, with the version
taken from the version byte of the identifier and the minutes counted
from 2020-01-01 out of the date the identifier carries. Only
HttpURLConnection is used, so there is still no runtime dependency and
the library still runs on Java 8. Keys are held against the URL they
came from, as the specification asks.
PublicKeySchedule chooses between the keys a creator has published, and
the rule is the latest key whose start is at or before the date asked
about, which is what the cloud applies. DatedPublicKey holds only that
start. The moment key material was generated is absent on purpose,
because creators generate keys in batches weeks ahead of the weeks the
keys cover, so many keys share one generation moment while starting on
different days, and choosing by that moment picks a key that has not
started yet.
The outcome is reported as OwidSignatureStatus rather than a boolean, so
a key that could not be fetched is KEY_UNAVAILABLE and one that could not
be read is INVALID_KEY, and neither reads as a forgery.
PublicKeyFetchException carries the status, the domain and the response
code, so no caller has to read message text to tell an outage from a
signature that does not match. A domain carrying anything other than
letters, digits, dots and hyphens is refused before a request is made,
because the domain arrives inside an identifier that came from outside.
Tested against two real fixtures, being a 51Did creator context
identifier created on 4 September 2026 for 51d.es and the thirty weekly
keys that domain published from 11 May to 30 November 2026. Thirteen of
those keys share one generation moment and start on thirteen different
weeks, so the batch shape is exercised on real data. The live end point
answers 401 without a credential, so the fetch runs against a stand in on
the loopback address serving that same schedule.
The file already promises that the examples in it are compiled and run, so the two new ones say which test runs them and what the fetch example can and cannot show without a network.
Reading every exit path of the fetch found one that could leave without a status. A caller passing a scheme other than http or https reaches a connection of another type, and the cast to HttpURLConnection would then fail as an unchecked exception, past the status every route into this class promises to answer with. The type is now checked and a scheme that makes no HTTP request is reported as a key that could not be obtained, with a test for it.
…ted request as the cloud does PublicKeySchedule offered latest() and documented it as the key a request naming no date is served. That is wrong twice over. A schedule is published ahead of time, so the last key by start is usually one whose period has not begun, and the cloud answers an undated request with the key in force at the moment of the request, clamping a date in the future to that moment. Serving the last key where the current one was meant is the same fault the .NET port carried in its answer to a request with no date. The method is now last(), documented as not being the key in force, and current() returns the key in force now. The stand in end point in the tests answers the way the cloud does, at a fixed moment a week after the fixture identifier was signed so the undated case stays repeatable, and the test that used the last key as the current key now says what it checks.
…ay what a creator that ignores the date costs Two keys sharing a start were settled in favour of the one supplied last, where the 51Degrees cloud and the .NET port settle it in favour of the first, so the same schedule could name different keys in different ports. The schedule now agrees with them. The write path repeated the minute arithmetic that minutesSinceBase holds, which the pull request description said it did not, so it now goes through that function. The README and the fetch documentation said a creator that ignores the date returns what an undated request would have received anyway, as though that were harmless, when every identifier signed under an earlier key then reads as not matching. The stand in end point refuses a date that is not a number with a 400, as the cloud does, and its comments say the fixed request moment is ten days after the fixture, which it is.
The cache was keyed by URL but no test distinguished that from a cache keyed by domain, which would hand an identifier from one week the key of another. Two identifiers from different weeks now fetch two different keys with one request each, and the second ask for a week is answered from what is held.
This was referenced Sep 5, 2026
Merged
Merged
Automation51D
pushed a commit
to 51Degrees/pipeline-java
that referenced
this pull request
Sep 6, 2026
…n: BUILD: Move owid-java to the key selection fix, exclude PublicKeyFetch' The 51Degrees/owid-java fork took SWAN-community/owid-java#6 on 5 September 2026, which adds PublicKeySchedule and DatedPublicKey and selects the creator's signing key by the start of its period rather than the moment it was generated. The submodule moves from 694f6333 to 764996e0, whose tree is identical to SWAN main at b92f1cd2. pipeline.did compiles the submodule source with -Xlint:all -Werror at source and target 1.8, and on JDK 21 the new HTTP-only PublicKeyFetch class trips the deprecation of URL(String). The module does not use that class, because DidClient fetches the cloud's published key list from id/key through its own HttpTransport, so it is excluded alongside Endpoints and the pom comment says why. mvn -pl pipeline.did test on JDK 21: 127 tests, 0 failures, 2 skipped. Fixture check: the real 4 September identifier verifies under the key starting 31 August 2026 and answers Invalid on the schedule shifted a week.
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.
The gap
Java was the only OWID port with no way to obtain a creator public key at
all. There was no
HttpURLConnection, noHttpClientand no dated keyselection anywhere in
src/main/java, so every Java integrator had to buildthe request and choose the right key themselves.
That matters because choosing the key is exactly where the other ports went
wrong, each in a different way, and all of those have now been fixed.
the date the key came into force, so it picked a key that had not started
yet and reported a genuine identifier as
SignatureInvalid. Fixed inSelect the signing key by when its period starts, not when it was made owid-dotnet#13
/owid/api/v1/creator, which answers 404. Fixed inFIX: ask the creator for the key at the OWID's own version owid-js#11
dateparameter, so it only ever received the key forthe current week. Fixed in
FIX: Send the OWID date when fetching the creator public key owid-rust#6
in FIX: Report why a network verify failed, and say what the import path is owid-go#13
Leaving Java with nothing meant every Java integrator had all three traps
still open to them.
What was built
PublicKeyFetchbuilds and performs the well known request, being/owid/api/v{n}/public-key?date={minutes}&format=pkcs. The version in thepath comes from the version byte of the identifier being checked rather than
from a constant, and the minutes are counted from 2020-01-01 out of the date
the identifier itself carries. Only
HttpURLConnectionis used, so thelibrary keeps its promise of no runtime dependency and still runs on Java 8,
which has no HTTP client of its own. Keys already fetched are held against
the URL they came from, as the specification asks, and
clearCacheemptiesthat store.
PublicKeySchedulechooses between the keys a creator has published,without any request, for a caller that already holds the schedule. The rule
is the latest key whose start is at or before the date asked about, which is
the rule the cloud itself applies.
DatedPublicKeyholds that start andnothing else. The moment key material was generated is absent on purpose,
because creators generate keys in batches weeks ahead of the weeks the keys
cover, so many keys share one generation moment while starting on different
days, and selecting by that moment picks a key that has not started yet. That
is the .NET defect, and a field the type does not hold cannot be selected by.
Status rather than a boolean. The outcome is the existing
OwidVerificationResultcarrying anOwidSignatureStatus, following thenaming already in this repository rather than copying another language. A key
that cannot be fetched is
KEY_UNAVAILABLEand one that arrives in a formthis library cannot read is
INVALID_KEY. Neither isSIGNATURE_INVALID,because an outage or a badly served key leaves the signature unjudged and
reporting either as invalid would read as an attack.
PublicKeyFetchExceptioncarries the status, the creator domain and theHTTP response code, so no caller has to read message text to tell an outage
from a signature that does not match. This matches
KeyFetchErrorin the Goport.
A domain check. The domain arrives inside an identifier that came from
outside, so a value carrying anything other than letters, digits, dots and
hyphens is refused before any request is made, rather than being pasted into
a URL where it could add a path, a query, a port or credentials and send the
request somewhere other than the creator.
Io.minutesSinceBasewas added so the fetch and the wire format write agreeon what a date means without either repeating the arithmetic.
The README gained a section on verifying an identifier signed in an earlier
week, and both new examples in it are compiled and run by
ReadmeExampleTest, which is the convention this repository already follows.Fixtures
Two real fixtures, both public and carrying no secret, in
src/test/resources.identifier.txt, a 51Did creator context identifier created on4 September 2026 for the creator domain
51d.es.public-key-schedule.txt, the thirty weekly keys that domain publishedfrom 11 May to 30 November 2026, each with the date it came into force and
the moment the material was generated.
Thirteen of those keys share the generation moment of 2026-09-01T10:54:00Z
while starting on thirteen different weeks from 7 September to 30 November,
so the batch shape that broke .NET is exercised on real published data rather
than on keys invented for the occasion. The library itself holds no
generation moment, so the fixture keeps that field only for the test which
shows what selecting by it would have picked.
The live end point answers 401 without a credential, so the fetch tests run
against a stand in on the loopback address serving that same schedule, which
is what the Rust and Go work did for the same reason. The URL under test is
the one the library builds with only the host replaced, so a fault in the
path or the query is caught rather than papered over by a copy of the URL
builder living in the test.
How it was verified
Maven 3.9.16 on Java 21, the whole suite.
A green suite proves the code is not wrong, it cannot prove the tests would
notice if the rule changed, so the selection rule was replaced with "always
return the newest key", which is the shape of the .NET defect, and the suite
was run again.
The rule was then restored and the suite is green again, as above.
CI ran the whole matrix on commit a94ae0e and all eight jobs passed, being
Java 8, 11, 17 and 21 on both Ubuntu and Windows. The Java 8 job matters most
here, because the new code has to hold the Java 8 floor the library ships at,
and it reports the same 126 tests.
The tests that carry the risk are these.
genuineIdentifierVerifiesAgainstTheKeyInForceOnItsDatereads the realidentifier, picks the key the published schedule says was in force on
4 September 2026, being the week beginning 31 August, and gets
SIGNATURE_VALID. It uses no HTTP and no URL building, so it is the fixedpoint everything else is measured against.
selectionIgnoresTheMomentTheKeysWereGeneratedfinds the newest keygenerated on or before the identifier date, shows that thirteen keys share
that moment, shows that the key it picks had not started yet, and shows
that key producing
SIGNATURE_INVALIDon a genuine identifier. The sametest then shows the start based rule producing
SIGNATURE_VALIDon thesame data. This is the .NET defect reproduced and then closed.
datedFetchVerifiesAnIdentifierFromAnEarlierKeyWeekruns the real fetchand asserts both the outcome and that the request named minute 3510720,
which is what went over the wire.
undatedFetchLeavesAnEarlierWeeksIdentifierUnverifiedmakes the request aport that forgets the date would make, against the same end point, and
shows a genuine identifier reading as a forgery. This is the Rust defect.
aKeyTheEndPointCannotServeIsKeyUnavailable,anEndPointThatCannotBeReachedIsKeyUnavailable,aKeyThatCannotBeReadIsInvalidKeyandaSchemeThatIsNotHttpIsKeyUnavailablecover the four ways the questiongoes unanswered, none of which report as a forgery.
What remains
main.com.sun.net.httpserver, which ships with the Java 8 JDK and compilesunder
--release 8. Both Java 8 jobs in CI pass, so the floor holds.51d.es schedule is regenerated the fixture keeps working, because both the
identifier and the keys are stored here, but it will not notice a change to
how the cloud selects keys. A cross port check against the live end point
would notice, and that is not something this repository can do without a
credential.
PublicKeyScheduleis fed by a caller. Nothing here fetches a wholeschedule, because the public key end point serves one key per request. A
creator wanting to publish a schedule in one document would need an end
point that does not exist yet.
Review
Produced with AI assistance and needs human review before merging. The
fixtures are public material with no secret in them, and no internal
infrastructure address appears anywhere in the branch.
Review of 5 September 2026
Four adversarial reviews (specification, cross port, cloud alignment and test validity) were run over this and the other four port pull requests. What landed here, all confirmed by reading the code:
PublicKeySchedule.latest()was documented as the key a request naming no date is served. A schedule is published ahead of time, so that is a key that has not started, and it is the shape of the .NET defect in a second place. It is nowlast(), documented as not the key in force, andcurrent()returns the key in force now.OwidApiBase.TrySelectKeydoes: the key in force at a fixed request moment (14 September 2026, ten days after the fixture identifier, the moment every port's stand in uses), a date later than that read as that moment, and a 400 for a date that is not a number.Io.writeDatenow goes throughminutesSinceBase.The suite is now 127 tests, one more than the count above, all green on the eight CI jobs and locally.
The test validity review then showed by mutation that nothing distinguished the URL keyed cache from one keyed by domain, which would hand an identifier from one week the key of another.
keysAreHeldPerRequestAndNotPerDomainfails under that mutation and passes at head. 128 tests.