ct-diag-server is an HTTP server written in Go for storing and retrieving Diagnosis Keys, as defined in Apple/Google's draft specification of its Exposure Notification framework. It aims to respect the privacy of its users and store only the bare minimum of data needed for anonymous exposure notifications.
In anticipation of the general release of Apple and Google's native APIs (planned for May 2020) to assist health organizations with contact tracing, this application provides a reference implementation for the framework's server component: a central repository for submitting Diagnosis Keys after a positive test, and retrieving a collection of all previously submitted Diagnosis Keys, to be used on the device for offline key matching.
ℹ️ The terminology and usage corresponds with v1.2 of the specification, as found here and here.
👉 Are you an app developer or working for a government and/or health authority looking to implement this server? Please contact me if you have questions, or open an issue.
- Privacy by design: Doesn't store or log any personally identifiable information.
- Built for high workloads and heavy use:
- Aims to have a small memory footprint.
- Minimal data transfer: Diagnosis Keys are uploaded/downloaded as bytestreams, easily cachable by CDNs or upstream (government) proxy services.
- Ships with a Dockerfile, for easy deployment as a workload on a wide range of hosting platforms.
- Security: relies on Go's standard library where possible, and has minimal vendor dependencies.
- Solid test coverage, for easy auditing and review.
- Permissive license, easily forkable for other developers.
- HTTP server for storing and retrieving Diagnosis Keys. Uses
bytestreams for sending and receiving as little data as possible over the
wire: 21 bytes per Diagnosis Key (16 bytes for the
TemporaryExposureKey
, 4 bytes for theRollingStartNumber
and 1 byte for theTransmissionRiskLevel
). - Ships with PostgreSQL adapter for storage of Diagnosis Keys, but can easily be forked for different adapters.
- Caching interface, with in-memory implementation.
- Cursor based offsetting for listing Diagnosis Keys, with support for byte ranges and cache control headers.
💡 Check out the OpenAPI reference or import openapi.yaml in a compatible client for exploring the API and creating client code stubs. Also check out the example client code.
To be used for fetching a list of Diagnosis Keys. A typical client is either a mobile device or the intermediate platform/server of an app developer, for manual/custom distribution of the payload to clients. In either case, the keyset can be regarded as public; it doesn't contain PII.
GET /diagnosis-keys
The endpoint supports byte range requests as defined in RFC 7233.
The HEAD
method may be used to obtain Last-Modified
and Content-Length
headers
for cache control purposes.
A query parameter (after
) allows clients to only fetch keys that haven't been
handled on the device yet, to minimize redundant network traffic and parsing time.
Pass the last known/handled key (hexadecimal encoding) to retrieve only new keys
uploaded after the given key.
Name | Description |
---|---|
after |
Used for listing diagnosis keys uploaded after the given key. Format: hexadecimal encoding of a Temporary Exposure Key. Example: a7752b99be501c9c9e893b213ad82842 . (Optional) |
A 200 OK
response should be expected for normal requests (non-empty and empty),
and 206 Partial Content
for responses to byte range requests.
In case of an empty reply, a Content-Length: 0
header is written.
A 500 Internal Server Error
response indicates server failure, and warrants a retry.
Name | Description |
---|---|
Content-Type: application/octet-stream |
The HTTP response is a bytestream of Diagnosis Keys (see below). |
Content-Length: {n * 21} |
Content length is n * 21 , where n is the amount of returned Diagnosis Keys (byte range requests may yield different lengths). |
Cache-Control: public, max-age=0, s-maxage=600 |
For (upstream) caching purposes, this header may be used. |
The HTTP response body is a bytestream of Diagnosis Keys. A Diagnosis Key is 21
bytes and consists of three parts: the TemporaryExposureKey
itself (16 bytes), the RollingStartNumber
(4 bytes, big endian) and the TransmissionRiskLevel
(1 byte).
Because the amount of bytes per Diagnosis Key is fixed, there is no delimiter.
To be used for uploading a set of Diagnosis Keys by a mobile client device. Note: It's still undecided if this server should authenticate requests. Given the wide range of per-country use cases and processes, this is now delegated to the server operator to shield this endpoint against unauthorized access, and provide its own upstream proxy, e.g. tailored to handle auth-z for health personnel.
POST /diagnosis-keys
Any request headers (e.g. Content-Length
and Content-Type
) are not needed.
The HTTP request body should be a bytestream of 1 <= n
Diagnosis Keys, where
n
is the max upload batch size configured on the server (default: 14).
A diagnosis key consists of three parts: the TemporaryExposureKey
itself (16 bytes),
the RollingStartNumber
(4 bytes, big endian) and the TransmissionRiskLevel
(1 byte).
Because the amount of bytes per Diagnosis Key is fixed, there is no delimiter.
An unexpected end of the bytestream (e.g. incomplete key) results
in a 400 Bad Request
response.
Duplicate keys are silently ignored.
A 200 OK
response with body OK
should be expected on successful storage of the
keyset in the database.
A 400 Bad Request
response is used for client errors. A 500 Internal Server Error
response is used for server errors, and warrants a retry. Error reasons are written
in a text/plain; charset=utf-8
response body.
To be used for fetching an ENExposureConfiguration object (see Apple‘s sample code article).
GET /exposure-config
Name | Description |
---|---|
Content-Type: application/json |
The response contains an object in JSON (see below). |
A 200 OK
response should be expected. A 500 Internal Server Error
response
indicates server failure, and warrants a retry.
The HTTP response body is a ENExposureConfiguration object, encoded in JSON.
Example:
{
"minimumRiskScore": 0,
"attenuationLevelValues": [1, 2, 3, 4, 5, 6, 7, 8],
"attenuationWeight": 50,
"daysSinceLastExposureLevelValues": [1, 2, 3, 4, 5, 6, 7, 8],
"daysSinceLastExposureWeight": 50,
"durationLevelValues": [1, 2, 3, 4, 5, 6, 7, 8],
"durationWeight": 50,
"transmissionRiskLevelValues": [1, 2, 3, 4, 5, 6, 7, 8],
"transmissionRiskWeight": 50
}
👉 See issue tracker.
The project is currently under active development.
David Stotijn, Martin van de Belt, Milo van der Linden, Peter Hellberg, Arian van Putten.
Thanks to the community of Code for NL (#corona-apps
and #corona-ct-diag-server
on Slack) for all the
valuable feedback and discussions!