-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added anonymous tokens documentation
- Loading branch information
Showing
4 changed files
with
257 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
|
||
 | ||
|
||
### 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() | ||
``` |
1 change: 1 addition & 0 deletions
1
diagrams/Smittestopp_notify_contacts_anonymous_tokens_en.drawio
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters