Skip to content

Commit

Permalink
Added anonymous tokens documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
sindremb committed Feb 18, 2021
1 parent d3a4d7a commit 7376d67
Show file tree
Hide file tree
Showing 4 changed files with 257 additions and 0 deletions.
251 changes: 251 additions & 0 deletions anonymous-tokens.md
Original file line number Diff line number Diff line change
@@ -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 <JWT access token>' \
--header 'Content-Type: application/json' \
--data-raw '{
"maskedPoint": "<base64-encoded randomized token seed (t)>"
}'
```

Which should then respond with a response as follows

```json
{
"kid": "6215",
"signedPoint": "<base64-encoded signed point (Q)>",
"proofChallenge": "<base64-encoded proof challenge (c)>",
"proofResponse": "<base64-encoded proof response (z)"
}
```

The properties of the response object is calculated roughly as follows

```c#
// using AnonymousTokens.Server.Protocol
// request : AnonymousTokenRequest
// _keyStore : IAnonymousTokensKeyStore
// _tokenGenerator = new TokenGenerator()
var signingKeyPair = await _keyStore.GetActiveSigningKeyPair();
var k = signingKeyPair.PrivateKey;
var K = signingKeyPair.PublicKey;
var P = signingKeyPair.EcParameters.Curve.DecodePoint(Hex.Decode(request.PAsHex));

var token = _tokenGenerator.GenerateToken(k, K.Q, signingKeyPair.EcParameters, P);
var Q = token.Q;
var c = token.c;
var z = token.z;

return new AnonymousTokenResponse(signingKeyPair.Kid, Q, c, z);
```

The private key used in this logic (`_keyStore.GetActiveSigningKeyPair()`) must be shared with the backend service that will accept the created tokens for authenticating and authorizing requests.

The second endpoint returns a list of valid anonymous token public keys, used to validate issued anonymous tokens.
This endpoint is inspired by the specification for [JSON Web Key Sets](https://tools.ietf.org/html/rfc7517).

```bash
curl --location --request GET 'https://localhost:5001/api/anonymoustokens/atks'
```

Which should return a response as follows

```json
{
"keys": [
{
"kid": "6215",
"kty": "EC",
"crv": "P-256",
"x": "AMEz4DeB0qSA1OIsSXDoI4bmjwZpOpta7ZrZU7opb8ao",
"y": "ALnIKgbW+0MCTaMOme19AM7c3LCp8uyQj0g9nWK/YkgO"
}
]
}
```

Please note that the "keys" element will contain multiple elements during key rollover,
and the correct element to validate an anonymous token should be retrieved by matching the "kid" property from the anonymous token response above.

### New backend behaviour

### Anonymous authentication scheme

A new authentication option has been added to the TEK upload API on the backend server in addition to the existing `Bearer` scheme using the JWTs.

The new authentication option uses the scheme `Anonymous` and assumes the following structure for the `Authentication` header

```http
Authentication: Anonymous <submitted point>.<token seed>.<kid>
```

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 = <calculated as above>
// X9ECParameters ecParameters = CustomNamedCurves.GetByName("P-256");
ECPoint publicKey = ecParameters.G.Multiply(privateKey).Normalize()
```
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<mxfile host="Electron" modified="2021-01-20T13:01:45.123Z" agent="5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.1.8 Chrome/87.0.4280.88 Electron/11.1.1 Safari/537.36" etag="AK2LA0O-8bwrY_1xJ7AO" version="14.1.8" type="device"><diagram name="Page-1" id="929967ad-93f9-6ef4-fab6-5d389245f69c">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</diagram></mxfile>
3 changes: 3 additions & 0 deletions diagrams/Smittestopp_notify_contacts_anonymous_tokens_en.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions gaen-processes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 7376d67

Please sign in to comment.