Skip to content

Add the dated public key fetch and the key schedule Python had none of - #4

Merged
jwrosewell merged 1 commit into
mainfrom
feature/dated-public-key-fetch
Sep 5, 2026
Merged

Add the dated public key fetch and the key schedule Python had none of#4
jwrosewell merged 1 commit into
mainfrom
feature/dated-public-key-fetch

Conversation

@jwrosewell

@jwrosewell jwrosewell commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds to the Python 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 of its own, because the fetch lives in owid.public_key_fetch, is imported only by a caller that asks for it, and a new contract test scans the package for any other route to the network.

Why

This port was written with key fetching scoped out ("retrieving a creator public key over HTTP is left to the caller"), 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

  • owid/public_key_fetch.py (new): public_key_url builds {scheme}://{domain}/owid/api/v{n}/public-key?date={minutes}&format=pkcs at the version the OWID carries; public_key_pem, signature_status, verify and clear_cache. 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 answers KEY_UNAVAILABLE or INVALID_KEY rather than a forgery. A caller may pass a transport of its own.
  • owid/public_key_schedule.py (new): DatedPublicKey and PublicKeySchedule with key_in_force, key_for, current, last, signature_status and verify. Two keys sharing a start settle in favour of the first supplied, as the cloud does. A naive datetime is read as UTC.
  • owid/endpoints.py: public_key_response_at answers 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.
  • owid/io.py: minutes_since_base, sharing write_date's arithmetic. owid/error.py: PublicKeyFetchError carrying the status, the domain and the response code. owid/__init__.py exports the new names without importing the fetch.
  • Tests: tests/test_public_key_fetch.py drives the real urllib path against a stand in for the end point on the loopback address (tests/key_end_point.py) 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/test_public_key_schedule.py and tests/test_public_key_response_at.py cover the choice of key, including the batch generated on 1 September that broke the .NET port. tests/test_network_contract.py keeps the no network promise. The fixtures under tests/data are 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.
  • README: the scope statement, a new section on verifying an identifier signed in an earlier week with two examples that tests/test_readme.py runs, the interface list, and the testing notes.

Verification

  • python -m unittest discover on Python 3.14 with cryptography 49: 161 tests, OK. Before this change the suite held 117, so 44 tests are new.
  • The two README examples run under the README test. The fetch one asks a creator in the reserved .invalid name space and answers KEY_UNAVAILABLE; the schedule one verifies the README's own identifier under the key in force on its date.

Notes

  • The 51Did client in pipeline-python (fiftyone_pipeline_did) fetches the whole schedule from id/key with a resource key and selects by start itself, so it is unaffected. The 51Degrees fork and pipeline-python's submodule pin can follow this merge.
  • Against 51d.es, the creator named in a 51Did identifier, the fetch answers KEY_UNAVAILABLE today, 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 through owid.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.

The package 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.

public_key_fetch asks /owid/api/v{n}/public-key?date={minutes}&format=pkcs
on the domain the OWID carries, at the version the OWID carries, through
urllib 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 KEY_UNAVAILABLE or INVALID_KEY rather than a forgery. A caller
may pass a transport of its own. 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.public_key_response_at
answers the date parameter for a creator that rotates its key as the
specification requires. io.minutes_since_base shares write_date's
arithmetic.

The core keeps no network access of its own: the fetch is imported only
by a caller that asks for it, and a contract test scans the package for
any other route to the network. The tests drive the real fetch against a
stand in for the end point 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. 161 tests pass, README examples included.
@jwrosewell
jwrosewell merged commit 5fe9a7c into main Sep 5, 2026
2 checks passed
@jwrosewell
jwrosewell deleted the feature/dated-public-key-fetch branch September 5, 2026 21:34
Automation51D pushed a commit to 51Degrees/pipeline-python that referenced this pull request Sep 6, 2026
…WID with the dated key fetch, and report whether a licence key was given'

Moves the owid-python submodule onto the 51Degrees fork head that carries
the dated public key fetch and the key schedule, which the Python port had
none of until SWAN-community/owid-python#4. The build time copy into the
package as the private module _owid takes whatever Python files the
submodule holds, so public_key_fetch.py and public_key_schedule.py are now
carried in a published wheel alongside the rest and need no change to
ci/copy-owid-source.ps1.

Nothing in DidClient changes as a result. 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, so the library's
PublicKeySchedule is not the right thing for it to call. The two are
deliberately different and both are correct.

Adds has_licence_key, which the Java and .NET clients already have and
this one did not. A licence key is needed only by redeem, so a client
without one still reads keys and verifies signatures, and a caller can now
ask rather than track it separately.

Verified with the submodule at 5fe9a7cc: 145 passed, 2 skipped.
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