Skip to content

Commit

Permalink
EPA-164: Make Keymaterial Configurable
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasrichner-oviva committed Nov 5, 2024
1 parent 5620df3 commit c16371c
Show file tree
Hide file tree
Showing 25 changed files with 479 additions and 266 deletions.
54 changes: 37 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ a good old OpenID Connect Relying Party (OIDC RP).

Identity Providers such as Keycloak can link accounts with OIDC out-of-the-box

## State of Compatibility

### Productive Environment (PU)

| Sectoral IdP | End-to-End | Provider |
|------------------------|------------|----------|
| Techniker Krankenkasse || IBM |
| Gothaer | 🚫 | RISE |

> [!NOTE]
> Most providers can not be independently tested as there are no test accounts available.
## Authentication Flow IDP / Relying Party

```mermaid
Expand Down Expand Up @@ -122,23 +134,31 @@ Use environment variables to configure the relying party server.
(*) required configuration
| 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:versicherter` |
| `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_OPENID_RP_SIG_JWKS_PATH`* | Path to a JWKS with signing keys of our relying party, i.e. for mTLS client certificates | `./openid_rp_sig_jwks.json` |
| `EHEALTHID_RP_OPENID_RP_ENC_JWKS_PATH`* | Path to a JWKS with the keys used for encryption between the federation and the relying party, i.e. to encrypt id_tokens | `./openid_rp_enc_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:versicherter` |
| `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` |
| `EHEALTHID_RP_OPENID_PROVIDER_SIG_JWKS_PATH` | Path to a JWKS with signing keys for our openIdProvider, for example the id_token issued by the relying party will be signed with it. Will be generated if not configured. | `./openid_provider_sig_jwks.json` |
public static final String CONFIG_FEDERATION_SIG_JWKS_PATH = "federation_sig_jwks_path";
public static final String CONFIG_OPENID_RP_SIG_JWKS_PATH = "openid_rp_sig_jwks_path";
public static final String CONFIG_OPENID_RP_ENC_JWKS_PATH = "openid_rp_enc_jwks_path";
public static final String CONFIG_OPENID_PROVIDER_SIG_JWKS_PATH = "openid_provider_sig_jwks_path";
# Generate Keys & Register for Federation
Expand Down
4 changes: 4 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Debugging Entity-Statements

curl https://pta-ehealthid.ovivadiga.com/.well-known/openid-federation | jwt decode -j - | jq -r .payload.metadata.openid_relying_party.jwks.keys[0].x5c[0] | base64 -d | openssl x509 -text


# Library IntegrationTest flow with Gematik Reference IDP

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public ConfigReader(ConfigProvider configProvider) {

public Config read() {

var federationEntityStatementJwksPath = loadJwks(CONFIG_FEDERATION_ENTITY_STATEMENT_JWKS_PATH);
var federationEntityStatementJwks = loadJwks(CONFIG_FEDERATION_ENTITY_STATEMENT_JWKS_PATH);

var baseUri =
configProvider
Expand Down Expand Up @@ -83,10 +83,6 @@ public Config read() {
.iss(baseUri)
.appName(appName)
.federationMaster(fedmaster)

// safety, remove the private key as we don't need it here
.entitySigningKeys(federationEntityStatementJwksPath.toPublicJWKSet())
.entitySigningKey(federationEntityStatementJwksPath.getKeys().get(0).toECKey())
.ttl(entityStatementTtl)
.scopes(getScopes())
.redirectUris(List.of(baseUri.resolve("/auth/callback").toString()))
Expand Down
103 changes: 42 additions & 61 deletions ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.jwk.*;
import com.nimbusds.jose.jwk.source.JWKSourceBuilder;
import com.oviva.ehealthid.auth.AuthenticationFlow;
import com.oviva.ehealthid.fedclient.FederationMasterClientImpl;
Expand All @@ -16,20 +16,19 @@
import com.oviva.ehealthid.relyingparty.ConfigReader.SessionStoreConfig;
import com.oviva.ehealthid.relyingparty.cfg.ConfigProvider;
import com.oviva.ehealthid.relyingparty.cfg.EnvConfigProvider;
import com.oviva.ehealthid.relyingparty.poc.GematikHeaderDecoratorHttpClient;
import com.oviva.ehealthid.relyingparty.providers.BasicKeystoreProvider;
import com.oviva.ehealthid.relyingparty.svc.AfterCreatedExpiry;
import com.oviva.ehealthid.relyingparty.svc.AuthService;
import com.oviva.ehealthid.relyingparty.svc.CaffeineCodeRepo;
import com.oviva.ehealthid.relyingparty.svc.CaffeineSessionRepo;
import com.oviva.ehealthid.relyingparty.svc.ClientAuthenticator;
import com.oviva.ehealthid.relyingparty.svc.CodeRepo;
import com.oviva.ehealthid.relyingparty.svc.KeyStore;
import com.oviva.ehealthid.relyingparty.svc.SessionRepo;
import com.oviva.ehealthid.relyingparty.svc.SessionRepo.Session;
import com.oviva.ehealthid.relyingparty.svc.TokenIssuer.Code;
import com.oviva.ehealthid.relyingparty.svc.TokenIssuerImpl;
import com.oviva.ehealthid.relyingparty.testenv.GematikHeaderDecoratorHttpClient;
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,13 +45,16 @@
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.security.KeyPair;
import java.security.interfaces.ECPublicKey;
import java.time.Clock;
import java.time.Duration;
import java.util.List;
import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -124,23 +126,34 @@ public void start() throws ExecutionException, InterruptedException {

var config = configReader.read();

// generate fresh keys for the relying-party
config = replaceRelyingPartyKeys(config);
var keyStores = BasicKeystoreProvider.load(configProvider);

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 tokenIssuer =
new TokenIssuerImpl(
config.baseUri(),
keyStores.openIdProviderJwksKeystore().keys().get(0)::toECKey,
codeRepo);
var sessionRepo = buildSessionRepo(config.sessionStore(), meterRegistry);

// the relying party signing key is for mTLS
var mTlsClientCertificate = config.federation().relyingPartySigningKey();
var mTlsClientCertificate =
keyStores.relyingPartySigJwksKeystore(config.federation().sub()).keys().get(0);

var relyingPartyKeys =
keyStores.relyingPartyEncJwksKeystore().keys().stream()
.map(k -> (JWK) k)
.collect(Collectors.toCollection(ArrayList::new));
relyingPartyKeys.add(mTlsClientCertificate);

var relyingPartyJwks = new JWKSet(relyingPartyKeys);

var authFlow =
buildAuthFlow(
config.baseUri(),
config.federation().federationMaster(),
config.federation().relyingPartyKeys(),
relyingPartyJwks,
mTlsClientCertificate);

var discoveryHttpClient =
Expand All @@ -165,14 +178,20 @@ public void start() throws ExecutionException, InterruptedException {

server =
SeBootstrap.start(
new App(config, keyStore, tokenIssuer, clientAuthenticator, authService),
new App(config, keyStores, tokenIssuer, clientAuthenticator, authService),
Configuration.builder().host(config.host()).port(config.port()).build())
.toCompletableFuture()
.get();

var localUri = server.configuration().baseUri();
logger.atInfo().log("Magic at {} ({})", config.baseUri(), localUri);

bootManagementServer(config, meterRegistry);
logger.atInfo().log("Management Server can be found at port {}", config.managementPort());
}

private void bootManagementServer(
ConfigReader.Config config, PrometheusMeterRegistry meterRegistry) {
managementServer =
Undertow.builder()
.addHttpListener(config.managementPort(), config.host())
Expand All @@ -182,56 +201,18 @@ public void start() throws ExecutionException, InterruptedException {
.addExactPath(MetricsEndpoint.PATH, new MetricsEndpoint(meterRegistry)))
.build();
managementServer.start();

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()
.addKeyValue("kid", signingKey.getKeyID())
.addKeyValue("jwk", signingKey.toJSONString())
.log(
"openid_relying_party signing key, kid={} jwk={}",
signingKey.getKeyID(),
signingKey.toJSONString());

logger
.atDebug()
.addKeyValue("kid", encKey.getKeyID())
.addKeyValue("jwk", encKey.toJSONString())
.log(
"openid_relying_party encryption key, kid={} jwk={}",
encKey.getKeyID(),
encKey.toJSONString());

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 ECKey toEcKey(KeyPair kp) {
try {
return new ECKey.Builder(Curve.P_256, (ECPublicKey) kp.getPublic())
.keyIDFromThumbprint()
.privateKey(kp.getPrivate())
.keyUse(KeyUse.SIGNATURE)
.build();
} catch (JOSEException e) {
throw new IllegalStateException("cannot create EC JWK from keypair", e);
}
}

private com.oviva.ehealthid.fedclient.api.HttpClient instrumentHttpClient(
Expand Down
Loading

0 comments on commit c16371c

Please sign in to comment.