DPoP reference implementation

A small Python package that does the DPoP part of the authentication of the Profit Basetool Exchange API: it keeps the installation’s P-256 key where the platform keeps keys, and it builds the proof JWT for each request. It uses nothing but the Python standard library (3.11 or later); the platform’s own crypto is reached through ctypes. It is licensed under the MIT licence, so it can be copied into a client of any licence.

It is a reference, not a client: it sends no request, stores no token and runs no device login.

Usage #

from dpop_reference import (
    NonceCache, asks_for_nonce, authorization_header, build_proof, open_installation_key,
)

key = open_installation_key("ExampleClient DPoP", key_file="~/.config/example-client/dpop.pem")
print(key.thumbprint())

nonces = NonceCache()
url = "https://ingest.profit-base.online/exchange/v1/me/blueprints?cursor=f1.7.42"
headers = {
    "Authorization": authorization_header(access_token),
    "DPoP": build_proof(key, "GET", url, access_token=access_token, nonce=nonces.get(url)),
}

After every answer, call nonces.update(url, answer_headers). When asks_for_nonce(status, answer_headers) is true, build a new proof with nonces.get(url) and send the request once more. At the token endpoint, build the proof without access_token.

Function What it does
open_installation_key(name, key_file=...) Opens the installation’s key, creating it on first use.
delete_installation_key(name, key_file=...) Deletes it, for a disconnect or INSTALLATION_REVOKED.
build_proof(key, method, url, access_token=, nonce=) Builds and signs one proof.
normalize_htu(url) The htu of a URL: lower-case scheme and host, no default port, no query or fragment.
jwk_thumbprint(jwk) The RFC 7638 thumbprint — the installation’s identity.
access_token_hash(token) The ath claim.
NonceCache, asks_for_nonce(...) The latest DPoP-Nonce per server, and whether an answer is a nonce challenge.

Platforms #

Windows (dpop_reference.cng). The key is a persisted, non-exportable CNG key, created through ncrypt.dll under the name you pass:

  • in the TPM through the Microsoft Platform Crypto Provider when it is present and supports P-256, otherwise
  • in the Microsoft Software Key Storage Provider, with an export policy of 0.

The private key never enters the process: NCryptSignHash signs in the provider and already returns the 64-byte r || s form. On the software provider, code running as the same Windows user can still ask Windows to sign; only the TPM resists an administrator.

Linux (dpop_reference.openssl). The key is generated by OpenSSL 3’s libcrypto (libcrypto.so.3) and stored as a PKCS #8 PEM file, created exclusively with mode 0600 in a directory created with mode 0700; a file that others can read is refused. OpenSSL’s DER signature is converted to r || s. The client security requirements prefer the Secret Service; a file is the fallback, and a client that uses it shows the member that it does.

Other systems. macOS is not covered: its system libcrypto must not be loaded. Set DPOP_REFERENCE_LIBCRYPTO to the path of an OpenSSL 3 libcrypto to use the file backend anyway; the same variable overrides the library search everywhere.

Tests #

From the repository root:

python -m unittest discover -s docs/exchange/dpop-reference

The tests need no network. They check base64url without padding, the RFC 7638 thumbprint example, the jkt and ath examples of RFC 9449, htu normalisation, the DER conversion and the proof’s header and claims. For each backend available on the machine they create a key, sign, and verify the signature twice: with the backend itself, and with an independent pure-Python P-256 verifier that is checked against RFC 6979’s test vector. A backend the machine lacks — CNG outside Windows, the TPM without one, OpenSSL without a libcrypto 3 — is skipped, not failed. The CNG tests create keys under random names and delete them again.

Files #

File Content
dpop_reference/jose.py base64url, thumbprint, ath, htu, the DER and JWS signature forms
dpop_reference/proof.py the proof, the Authorization value, the nonce cache
dpop_reference/cng.py the Windows CNG backend
dpop_reference/openssl.py the OpenSSL 3 backend
dpop_reference/keystore.py the platform’s choice of backend
tests/ the tests and the independent verifier