Skip to content

Commit

Permalink
EPA-133: A_23183 - Veröffentlichen der TLS Authentisierungsschlüssel (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasrichner-oviva authored Jul 10, 2024
1 parent 0580512 commit 03d4c5a
Show file tree
Hide file tree
Showing 23 changed files with 300 additions and 138 deletions.
41 changes: 18 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,10 @@ export ISSUER_URI=https://mydiga.example.com

#---- 2. deploy the relying party
docker run --rm \
-v "$(pwd)"/enc_mydiga_example_com_jwks.json:/secrets/enc_jwks.json:ro \
-v "$(pwd)"/sig_mydiga_example_com_jwks.json:/secrets/sig_jwks.json:ro \
-e "EHEALTHID_RP_APP_NAME=Awesome DiGA" \
-e "EHEALTHID_RP_BASE_URI=$ISSUER_URI" \
-e 'EHEALTHID_RP_FEDERATION_ENC_JWKS_PATH=/secrets/enc_jwks.json' \
-e 'EHEALTHID_RP_FEDERATION_SIG_JWKS_PATH=/secrets/sig_jwks.json' \
-e 'EHEALTHID_RP_FEDERATION_ES_JWKS_PATH=/secrets/sig_jwks.json' \
-e 'EHEALTHID_RP_FEDERATION_MASTER=https://app-test.federationmaster.de' \
-e 'EHEALTHID_RP_REDIRECT_URIS=https://sso-mydiga.example.com/auth/callback' \
-e 'EHEALTHID_RP_ES_TTL=PT5M' \
Expand Down Expand Up @@ -124,24 +122,23 @@ Use environment variables to configure the relying party server.
(*) required configuration
| Name | Description | Example |
|------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------|
| `EHEALTHID_RP_FEDERATION_ENC_JWKS_PATH`* | Path to a JWKS with at least one keypair for encryption of ID tokens. | `./enc_jwks.json` |
| `EHEALTHID_RP_FEDERATION_SIG_JWKS_PATH`* | Path to a JWKS with at least one keypair for signature withing the federation. All these keys __MUST__ be registered with the federation master. | `./sig_jwks.json` |
| `EHEALTHID_RP_REDIRECT_URIS`* | Valid redirection URIs for OpenID connect. | `https://sso-mydiga.example.com/auth/callback` |
| `EHEALTHID_RP_BASE_URI`* | The external base URI of the relying party. This is also the `issuer` towards the OpenID federation. Additional paths are unsupported for now. | `https://mydiga-rp.example.com` |
| `EHEALTHID_RP_IDP_DISCOVERY_URI`* | The URI of the discovery document of your identity provider. Used to fetch public keys for client authentication. | `https://sso-mydiga.example.com/.well-known/openid-configuration` |
| `EHEALTHID_RP_FEDERATION_MASTER`* | The URI of the federation master. | `https://app-test.federationmaster.de` |
| `EHEALTHID_RP_APP_NAME`* | The application name within the federation. | `Awesome DiGA` |
| `EHEALTHID_RP_HOST` | Host to bind to. | `0.0.0.0` |
| `EHEALTHID_RP_PORT` | Port to bind to. | `1234` |
| `EHEALTHID_RP_ES_TTL` | The time to live for the entity statement. In ISO8601 format. | `PT12H` |
| `EHEALTHID_RP_SCOPES` | The comma separated list of scopes requested in the federation. This __MUST__ match what was registered with the federation master. | `openid,urn:telematik:email,urn:telematik:display_name` |
| `EHEALTHID_RP_SESSION_STORE_TTL` | The time to live for sessions. In ISO8601 format. | `PT20M` |
| `EHEALTHID_RP_SESSION_STORE_MAX_ENTRIES` | The maximum number of sessions to store. Keeps memory bounded. | `1000` |
| `EHEALTHID_RP_CODE_STORE_TTL` | The time to live for codes, i.e. successful logins where the code is not redeemed yet. In ISO8601 format. | `PT5M` |
| `EHEALTHID_RP_CODE_STORE_MAX_ENTRIES` | The maximum number of codes to store. Keeps memory bounded. | `1000` |
| `EHEALTHID_RP_LOG_LEVEL` | The log level. | `INFO` |
| Name | Description | Example |
|------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------|
| `EHEALTHID_RP_FEDERATION_ES_JWKS_PATH`* | Path to a JWKS with at least one keypair for signature of the entity statement. All these keys __MUST__ be registered with the federation master. | `./sig_jwks.json` |
| `EHEALTHID_RP_REDIRECT_URIS`* | Valid redirection URIs for OpenID connect. | `https://sso-mydiga.example.com/auth/callback` |
| `EHEALTHID_RP_BASE_URI`* | The external base URI of the relying party. This is also the `issuer` towards the OpenID federation. Additional paths are unsupported for now. | `https://mydiga-rp.example.com` |
| `EHEALTHID_RP_IDP_DISCOVERY_URI`* | The URI of the discovery document of your identity provider. Used to fetch public keys for client authentication. | `https://sso-mydiga.example.com/.well-known/openid-configuration` |
| `EHEALTHID_RP_FEDERATION_MASTER`* | The URI of the federation master. | `https://app-test.federationmaster.de` |
| `EHEALTHID_RP_APP_NAME`* | The application name within the federation. | `Awesome DiGA` |
| `EHEALTHID_RP_HOST` | Host to bind to. | `0.0.0.0` |
| `EHEALTHID_RP_PORT` | Port to bind to. | `1234` |
| `EHEALTHID_RP_ES_TTL` | The time to live for the entity statement. In ISO8601 format. | `PT12H` |
| `EHEALTHID_RP_SCOPES` | The comma separated list of scopes requested in the federation. This __MUST__ match what was registered with the federation master. | `openid,urn:telematik:email,urn:telematik:display_name` |
| `EHEALTHID_RP_SESSION_STORE_TTL` | The time to live for sessions. In ISO8601 format. | `PT20M` |
| `EHEALTHID_RP_SESSION_STORE_MAX_ENTRIES` | The maximum number of sessions to store. Keeps memory bounded. | `1000` |
| `EHEALTHID_RP_CODE_STORE_TTL` | The time to live for codes, i.e. successful logins where the code is not redeemed yet. In ISO8601 format. | `PT5M` |
| `EHEALTHID_RP_CODE_STORE_MAX_ENTRIES` | The maximum number of codes to store. Keeps memory bounded. | `1000` |
| `EHEALTHID_RP_LOG_LEVEL` | The log level. | `INFO` |
# Generate Keys & Register for Federation
Expand Down Expand Up @@ -220,8 +217,6 @@ sequenceDiagram
# Open Points
- end-to-end tests with Verimi, Gematik, RISE and IBM IDPs, most lack options to test currently
- [A_23183 - Veröffentlichen der TLS Authentisierungsschlüssel](https://gemspec.gematik.de/docs/gemSpec/gemSpec_IDP_FD/gemSpec_IDP_FD_V1.7.0/#A_23183) -
no option to test currently, though implemented
# Wishlist
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,16 @@
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.jwk.*;
import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
import com.nimbusds.jose.util.Base64;
import com.nimbusds.oauth2.sdk.id.Issuer;
import com.nimbusds.oauth2.sdk.util.X509CertificateUtils;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.security.cert.CertificateEncodingException;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Callable;
import org.bouncycastle.operator.OperatorCreationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import picocli.CommandLine;
Expand All @@ -35,24 +30,20 @@ public class KeyGeneratorCommand implements Callable<Integer> {
@CommandLine.Option(
names = {"-i", "--iss", "--issuer-uri"},
description = "the issuer uri of the 'Fachdienst' identiy provider",
required = true)
required = true,
defaultValue = "default")
private URI issuerUri;

private static final Logger logger = LoggerFactory.getLogger(KeyGeneratorCommand.class);

public Integer call() throws Exception {

var sigName = "sig";
var encName = "enc";

logger.atInfo().log("generating signing keys");
var federationSigningKeys = generateSigningKey(issuerUri);

logger.atInfo().log("generating encryption keys");
var federationIdTokenEncryptionKey = generateEncryptionKey();
var federationSigningKeys = generateSigningKey();

saveJwks(sigName + "_" + deriveName(issuerUri), new JWKSet(List.of(federationSigningKeys)));
saveJwks(encName + "_" + deriveName(issuerUri), new JWKSet(federationIdTokenEncryptionKey));

return 0;
}
Expand All @@ -66,36 +57,18 @@ private String deriveName(URI issuer) {
return s;
}

private JWK generateSigningKey(URI issuer)
throws JOSEException, IOException, CertificateEncodingException, OperatorCreationException {

var key =
new ECKeyGenerator(Curve.P_256)
.keyUse(KeyUse.SIGNATURE)
.keyIDFromThumbprint(true)
.generate();
private JWK generateSigningKey() throws JOSEException {

// https://gemspec.gematik.de/docs/gemSpec/gemSpec_IDP_FD/gemSpec_IDP_FD_V1.7.2/#A_23185-01
var now = Instant.now();
var nbf = now.minus(Duration.ofHours(24));
var exp = now.plus(Duration.ofDays(180));

var cert =
X509CertificateUtils.generateSelfSigned(
new Issuer(issuer),
Date.from(nbf),
Date.from(exp),
key.toPublicKey(),
key.toPrivateKey());

key = new ECKey.Builder(key).x509CertChain(List.of(Base64.encode(cert.getEncoded()))).build();

return key;
}
var exp = now.plus(Duration.ofDays(366)); // < 398d

private JWK generateEncryptionKey() throws JOSEException {
return new ECKeyGenerator(Curve.P_256)
.keyUse(KeyUse.ENCRYPTION)
.keyUse(KeyUse.SIGNATURE)
.keyIDFromThumbprint(true)
.notBeforeTime(Date.from(nbf))
.expirationTime(Date.from(exp))
.generate();
}

Expand Down
32 changes: 16 additions & 16 deletions ehealthid-rp/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,30 @@
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-annotations</artifactId>
</dependency>

<!-- BEGIN crypto & jwt handling -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>oauth2-oidc-sdk</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
</dependency>

<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!-- END crypto & jwt handling -->

<!-- BEGIN jackson -->
<dependency>
Expand Down Expand Up @@ -155,22 +171,6 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>oauth2-oidc-sdk</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<scope>test</scope>
</dependency>

</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@

public class ConfigReader {

public static final String CONFIG_FEDERATION_ENC_JWKS_PATH = "federation_enc_jwks_path";
public static final String CONFIG_FEDERATION_SIG_JWKS_PATH = "federation_sig_jwks_path";
public static final String CONFIG_FEDERATION_ENTITY_STATEMENT_JWKS_PATH =
"federation_es_jwks_path";
public static final String CONFIG_BASE_URI = "base_uri";
public static final String CONFIG_HOST = "host";
public static final String CONFIG_PORT = "port";
Expand All @@ -42,8 +42,7 @@ public ConfigReader(ConfigProvider configProvider) {

public Config read() {

var federationEncJwksPath = loadJwks(CONFIG_FEDERATION_ENC_JWKS_PATH);
var federationSigJwksPath = loadJwks(CONFIG_FEDERATION_SIG_JWKS_PATH);
var federationEntityStatementJwksPath = loadJwks(CONFIG_FEDERATION_ENTITY_STATEMENT_JWKS_PATH);

var baseUri =
configProvider
Expand Down Expand Up @@ -84,13 +83,10 @@ public Config read() {
.iss(baseUri)
.appName(appName)
.federationMaster(fedmaster)
.entitySigningKey(federationSigJwksPath.getKeys().get(0).toECKey())

// safety, remove the private key as we don't need it here
.entitySigningKeys(federationSigJwksPath.toPublicJWKSet())

// _MUST NOT_ be public. We need it for decryption.
.relyingPartyEncKeys(federationEncJwksPath)
.entitySigningKeys(federationEntityStatementJwksPath.toPublicJWKSet())
.entitySigningKey(federationEntityStatementJwksPath.getKeys().get(0).toECKey())
.ttl(entityStatementTtl)
.scopes(getScopes())
.redirectUris(List.of(baseUri.resolve("/auth/callback").toString()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import com.oviva.ehealthid.relyingparty.svc.TokenIssuer.Code;
import com.oviva.ehealthid.relyingparty.svc.TokenIssuerImpl;
import com.oviva.ehealthid.relyingparty.util.DiscoveryJwkSetSource;
import com.oviva.ehealthid.relyingparty.util.KeyGenerator;
import com.oviva.ehealthid.relyingparty.util.LoggingHttpClient;
import com.oviva.ehealthid.relyingparty.ws.App;
import com.oviva.ehealthid.relyingparty.ws.HealthEndpoint;
Expand All @@ -46,6 +47,7 @@
import java.net.http.HttpClient;
import java.time.Clock;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -118,13 +120,17 @@ public void start() throws ExecutionException, InterruptedException {

var config = configReader.read();

// generate fresh keys for the relying-party
config = replaceRelyingPartyKeys(config);

var keyStore = new KeyStore();
var meterRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
var codeRepo = buildCodeRepo(config.codeStoreConfig(), meterRegistry);
var tokenIssuer = new TokenIssuerImpl(config.baseUri(), keyStore, codeRepo);
var sessionRepo = buildSessionRepo(config.sessionStore(), meterRegistry);

var sslContext = TlsContext.fromClientCertificate(config.federation().entitySigningKey());
// the relying party signing key is for mTLS
var sslContext = TlsContext.fromClientCertificate(config.federation().relyingPartySigningKey());

var httpClient =
HttpClient.newBuilder()
Expand All @@ -136,7 +142,7 @@ public void start() throws ExecutionException, InterruptedException {
buildAuthFlow(
config.baseUri(),
config.federation().federationMaster(),
config.federation().relyingPartyEncKeys(),
config.federation().relyingPartyKeys(),
httpClient);

var discoveryHttpClient =
Expand Down Expand Up @@ -182,6 +188,39 @@ public void start() throws ExecutionException, InterruptedException {
logger.atInfo().log("Management Server can be found at port {}", config.managementPort());
}

private ConfigReader.Config replaceRelyingPartyKeys(ConfigReader.Config config) {

logger.atInfo().log(
"Generating fresh 'openid_relying_party' keys for mTLS and id_token encryption.");

var signingKey = KeyGenerator.generateSigningKeyWithCertificate(config.federation().sub());
var encKey = KeyGenerator.generateEncryptionKey();

var keys = new JWKSet(List.of(signingKey, encKey));

logger.atDebug().log("openid_relying_party signing key, kid={}", signingKey.getKeyID());
logger.atDebug().log("openid_relying_party encryption key, kid={}", encKey.getKeyID());

var fedConfig =
config
.federation()
.builder()
.relyingPartySigningKey(signingKey.toECKey())
.relyingPartyKeys(keys)
.build();

return new ConfigReader.Config(
config.relyingParty(),
fedConfig,
config.host(),
config.port(),
config.managementPort(),
config.baseUri(),
config.idpDiscoveryUri(),
config.sessionStore(),
config.codeStoreConfig());
}

private com.oviva.ehealthid.fedclient.api.HttpClient instrumentHttpClient(
com.oviva.ehealthid.fedclient.api.HttpClient client) {
if (logger.isDebugEnabled()) {
Expand Down
Loading

0 comments on commit 03d4c5a

Please sign in to comment.