From 7376d674cb117401f080af7413ec361b2d137812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sindre=20M=C3=B8gster=20Braaten?= Date: Thu, 18 Feb 2021 12:08:11 +0100 Subject: [PATCH] Added anonymous tokens documentation --- anonymous-tokens.md | 251 ++++++++++++++++++ ...notify_contacts_anonymous_tokens_en.drawio | 1 + ...pp_notify_contacts_anonymous_tokens_en.svg | 3 + gaen-processes.md | 2 + 4 files changed, 257 insertions(+) create mode 100644 anonymous-tokens.md create mode 100644 diagrams/Smittestopp_notify_contacts_anonymous_tokens_en.drawio create mode 100644 diagrams/Smittestopp_notify_contacts_anonymous_tokens_en.svg diff --git a/anonymous-tokens.md b/anonymous-tokens.md new file mode 100644 index 0000000..75e1c1f --- /dev/null +++ b/anonymous-tokens.md @@ -0,0 +1,251 @@ +# Anonymous tokens + +As a additional privacy measure, an an alternate authorization method for uploading TEKs has been introduced. +This method exchanges the JWT access token for an anonymous token, which removes the possibility of the two backend solutions, +Fhi.Smittestopp.Backend and Fhi.Smittestopp.Verification, to link verification attempts to uploaded TEKs through the token used for authorization. +You can find more information about what anonymous tokens are and how they work in [the github repo wiki](https://github.com/HenrikWM/anonymous-tokens/wiki). +The [readme of the github repo](https://github.com/HenrikWM/anonymous-tokens) also contains sample code for how to use the package. + +## New infection verification and TEK upload flow + +The TEK upload flow using anonymous tokens starts in exactly the same way as the default flow [described here](./gaen-processes.md#verify-infection-and-notify-exposure), +but triggers a new behaviour in the client through a new feature flag in the JWT access token issued to the client after verification. +The changes in the flow is shown from step 1.2 onwards in the diagram below. + +![Smittestopp components overview](diagrams/Smittestopp_notify_contacts_anonymous_tokens_en.svg) + +### New verification solution behaviour + +#### Feature flag + +Enabling/disabling the anonymous token feature is controlled from the verification solution. +Internally, this feature is enabled/disabled through the appsetting `common:anonymousTokens:enabled`. +If enabled, and the authenticated user is verified COVID-19 positive, +the JWT access token will include a new claim, `covid19_anonymous_token=v1_enabled`, which will activate the new behaviour in the client. The backend should still accept JWT tokens for authentication, regardless of this feature flag, to retain backwards compatibility with older versions of the app. + +#### Anonymous token endpoints + +Two new endpoints have been added to the verification solution, and are active if the feature flag is enabled. + +The first endpoint is used for exchanging a JWT-access token for an anonymous token. Access to this endpoint is granted through the claim `role=upload-approved` (combines positive test found and not blocked). + +```bash +curl --location --request POST 'https://localhost:5001/api/anonymoustokens' \ +--header 'Authorization: Bearer ' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "maskedPoint": "" +}' +``` + +Which should then respond with a response as follows + +```json +{ + "kid": "6215", + "signedPoint": "", + "proofChallenge": "", + "proofResponse": ".. +``` + +The validation of the incoming tokens is performed by the backend roughly as follows + +```c# +// using AnonymousTokens.Core.Services +// using AnonymousTokens.Server.Protocol +// _anonymousTokenKeySource : IAnonymousTokenKeySource +// _tokenVerifier = new TokenVerifier(seedStore : ISeedStore) +string[] anonymousToken = authHeader.Replace("Anonymous ", string.Empty).Split("."); +var submittedPoint = _anonymousTokenKeySource.ECParameters.Curve.DecodePoint(Convert.FromBase64String(anonymousToken[0])); +var tokenSeed = Convert.FromBase64String(anonymousToken[1]); +var keyId = anonymousToken[2]; + +var privateKey = _anonymousTokenKeySource.GetPrivateKey(keyId); + +var isValid = await _tokenVerifier.VerifyTokenAsync(privateKey, _anonymousTokenKeySource.ECParameters.Curve, tokenSeed, submittedPoint); +``` + +The `ISeedStore` must be implemented to prevent token replay attacks, and `_anonymousTokenKeySource.GetPrivateKey(keyId)` must always return the same private key for a given keyId as the one used by the verification solution, to be able to correctly verify the token. + +### New client behaviour + +Given that the feature flag is present in the JWT access token returned to the client, +the client will performs some additional steps prior to uploading the TEKs to the backend. + +1. Perform the necessary client side initialization of the anonymous token request. + ```c# + // using AnonymousTokens.Client.Protocol + // _initiator = new Initiator() + // _ecParameters = CustomNamedCurves.GetByOid(X9ObjectIdentifiers.Prime256v1) + var init = _initiator.Initiate(_ecParameters.Curve); + var state = new AnonymousTokenState + { + t = init.t, + r = init.r, + P = init.P + }; + var tokenRequest = new GenerateTokenRequestModel + { + MaskedPoint = Convert.ToBase64String(state.P.GetEncoded()) + }; + ``` +1. After requesting an anonymous token using the request object above, + and the JWT from verification, + the client randomizes the anonymous token, + using the tokenResponse returned from the verification server, + as well as the public key used for creating the token, + retrieved from the atks-endpoint. + ```c# + // using Org.BouncyCastle.Math + // X9ECParameters ecParameters = CustomNamedCurves.GetByName("P-256"); // or "crv" from the matching public key + var K = DecodePublicKey(atksResponse.Keys.Single(k => k.Kid == tokenResponse.kid)) + var Q = _ecParameters.Curve.DecodePoint(Convert.FromBase64String(tokenResponse.SignedPoint)); + var c = new BigInteger(Convert.FromBase64String(tokenResponse.ProofChallenge)); + var z = new BigInteger(Convert.FromBase64String(tokenResponse.ProofResponse)); + var token = _initiator.RandomiseToken(_ecParameters, K, state.P, Q, c, z, state.r); + ``` +1. The anonymous authentication header value can then be constructed as follows + ```c# + var t = state.t; + var W = token; + var keyId = tokenResponse.Kid; + var encodedToken = Convert.ToBase64String(W.GetEncoded()) + "." + Convert.ToBase64String(t) + "." + keyId; + var authHeaderValue = $"Anonymous {encodedToken}" + ``` + +## Environment setup requirements + +### Shared private key + +The same private key is used to both issue anonymous tokens from the secure token server, as well as validating calls to the API protected by the anonymous tokens. As a result, the STS (Fhi.Smittestopp.Verification) and the API (Fhi.Smittestopp.Backend) must at all times agree on the private key used. + +### Rotating shared private key + +An issued anonymous token will remain valid as long as the private key used to issue it is still accepted by the API. +To avoid cases where tokens issued could be used far into the future, it is desired to limit the timespan each private key is valid for. +However, a too short timespan for each key is not desired either, as that would potentially enable reidentification of the anonymous token through the private key used to issue it. + +To avoid having to constantly generate new private keys, and keeping these in sync between the STS and the API, +a method of generating short lived private keys from a shared long lived master key has been adopted. +Then the requirements are reduced to agreeing on the shared master key, +and how long each generated short lived private key should be valid for. (And optionally, defining a rollover period). + +The first step then is to generate the private key interval number (also used as kid) for a given point in time, which should be done as follows. + +```c# +// pointInTime = DateTimeOffset.UtcNow +// _config.KeyRotationInterval = TimeSpan.FromDays(3) // default value +var intervalNumber = pointInTime.ToUnixTimeSeconds() / Convert.ToInt64(_config.KeyRotationInterval.TotalSeconds) +``` + +Given the interval number, and the master private key bytes, the short lived private key should then be generated as follows. + +```c# +// keyIntervalNumber = intervalNumber from above +// masterKeyBytes = master private key bytes shared between the two server applications +var keyByteSize = (int) Math.Ceiling(ecParameters.Curve.Order.BitCount / 8.0); +var counter = 0; + +BigInteger privateKey; +do +{ + var privateKeyBytes = GeneratePrivateKeyBytes(keyByteSize, masterKeyBytes, keyIntervalNumber, counter); + privateKey = new BigInteger(privateKeyBytes); + counter++; + if (counter > 1000) + { + throw new GeneratePrivateKeyException("Maximum number of iterations exceeded while generating private key within curve order."); + } +} while (privateKey.CompareTo(ecParameters.Curve.Order) > 0); // Private key must be within curve order + +return privateKey; +``` + +The while-loop included as a safeguard to ensure the generated key is within the curve order, as it otherwise could produce a weak private key. + +The method GeneratePrivateKeyBytes is defined as follows + +```c# +private static byte[] GeneratePrivateKeyBytes(int numBytes, byte[] masterKeyBytes, long keyIntervalNumber, int counter) +{ + var keyIntervalBytes = BitConverter.GetBytes(keyIntervalNumber); + var counterBytes = BitConverter.GetBytes(counter); + var ikm = masterKeyBytes; + var salt = keyIntervalBytes.Concat(counterBytes).ToArray(); + var hkdf = new HkdfBytesGenerator(new Sha256Digest()); + var hParams = new HkdfParameters(ikm, salt, null); + hkdf.Init(hParams); + byte[] keyBytes = new byte[numBytes]; + hkdf.GenerateBytes(keyBytes, 0, numBytes); + return keyBytes; +} +``` + +Once you have a private key, the public key can be calculated as follows: + +```c# +// BigInteger privateKey = +// X9ECParameters ecParameters = CustomNamedCurves.GetByName("P-256"); +ECPoint publicKey = ecParameters.G.Multiply(privateKey).Normalize() +``` \ No newline at end of file diff --git a/diagrams/Smittestopp_notify_contacts_anonymous_tokens_en.drawio b/diagrams/Smittestopp_notify_contacts_anonymous_tokens_en.drawio new file mode 100644 index 0000000..f3abef2 --- /dev/null +++ b/diagrams/Smittestopp_notify_contacts_anonymous_tokens_en.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/diagrams/Smittestopp_notify_contacts_anonymous_tokens_en.svg b/diagrams/Smittestopp_notify_contacts_anonymous_tokens_en.svg new file mode 100644 index 0000000..ef77af2 --- /dev/null +++ b/diagrams/Smittestopp_notify_contacts_anonymous_tokens_en.svg @@ -0,0 +1,3 @@ + + +
App
App
Verification solution
Verification solution
Infected user
Infec...
MSIS
MSIS
ID-porten
ID-porten
Backend
Backend
ID-token (ID-porten)
  • National identifier
  • Pseudonym*
ID-token (ID-porten)...
Infection status:
  • HasPositiveTest****: true/false
  • Sampling date (if available)
Infection status:...
1.1.2 Check infection status
  • National identifier
1.1.2 Check infection status...
1.1.1 Start authentication
1.1.1 Start authentication
Access token
  • ID*** (unique per verification)
  • Status: Positive/Negative
  • Sample date (if available)
  • IsBlocked**: true/false
Access token...
1.1 Start infection verification
1.1 Start infection verifica...
1 Start exposure notification
1 Start exposure notificat...
1.1.1.1 Authenticate
1.1.1.1 Authenticate
Completed authentication
Completed authentication
1.4 Upload diagnosis keys
  • Diagnosis keys (TEK + infectiousness)
  • Anonymous token
    • Token seed
    • Signed token seed
    • Key ID
1.4 Upload diagnosis keys...
GAEN
GAEN
1.3 Retrieve TEKs
1.3 Retrieve TEKs
TEKs
TEKs
Steps 1.2 and 1.3  are only performed if Access token has
  Status = Positive
  IsBlocked = false
from step 1.1
Steps 1.2 and 1.3  are on...
* Pseudonym from ID-porten is unique for the national identifier and the verification solution, meaning if the same user authenticates through ID-porten to an other system, they will have a different pseudonym. This is used to keep track of the number of infection verifications performed per person.

** The verificaiton solutions stores the number of verifications performed verifications per pseudonym (hashed) for the last 24 hours.If the same pseudonym exceeds 3 verifications, any issued Access-tokens will have the IsBlocked flag set to true, which blocks the Backend from accepting new diagnosis key uploads..

*** ID in the access token is a unique ID generated per verification, and is used to stop users from reusing a single verification result to upload diagnosis keys from multiple devices (tracked in backend).

**** MSIS will return HasPositiveTest=true if the person has had a positive COVID-19 test within the last 14 days, and the person is at least 16 years old.
* Pseudonym from ID-porten is unique for the national...
Anonymous token
  • Signed randomized token seed
  • Key-ID
Anonymous token...
1.2 Request anonymous token
  • JWT access token
  • Randomized token seed
1.2 Request anonymous token...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/gaen-processes.md b/gaen-processes.md index 71fe92b..bd5328e 100644 --- a/gaen-processes.md +++ b/gaen-processes.md @@ -39,6 +39,8 @@ The app then retrieved all TEKs for the last 14 days from GAEN, combines these w ![Smittestopp components overview](diagrams/Smittestopp_notify_contacts_en.png) +An alternative flow for performing infection verification and diagnosis keys upload, using [anonymous tokens](https://github.com/HenrikWM/anonymous-tokens/wiki), has also been implemented and is [described further here](anonymous-tokens.md). + ## Detecting exposure The figure below illustrates the process for detecting and notifying a user of exposure.