Skip to content

Commit

Permalink
EPA-111: EntityStatement tweaks for compatibility (#76)
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasrichner-oviva committed May 14, 2024
1 parent 08efa73 commit 70b9782
Show file tree
Hide file tree
Showing 17 changed files with 621 additions and 102 deletions.
25 changes: 16 additions & 9 deletions ehealthid-cli/pom.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
Expand All @@ -11,11 +9,9 @@
</parent>

<artifactId>ehealthid-cli</artifactId>
<description>
Generator for entity-statements in the TI federation. This can generate the necessary
keys as well as the registration xml.
</description>
<packaging>jar</packaging>
<description>Generator for entity-statements in the TI federation. This can generate the necessary
keys as well as the registration xml.</description>

<dependencies>
<dependency>
Expand All @@ -37,6 +33,18 @@
<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.spullara.mustache.java</groupId>
Expand Down Expand Up @@ -116,16 +124,15 @@
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<phase>package</phase>
</execution>
</executions>

</plugin>
</plugins>
</build>


</project>
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
package com.oviva.ehealthid.cli;

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.jwk.Curve;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.KeyUse;
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;
import picocli.CommandLine.Command;

@Command(
Expand All @@ -23,6 +32,12 @@ Generator for JSON web keys (JWKS) for use in the TI OpenID federation.
""")
public class KeyGeneratorCommand implements Callable<Integer> {

@CommandLine.Option(
names = {"-i", "--iss", "--issuer-uri"},
description = "the issuer uri of the 'Fachdienst' identiy provider",
required = true)
private URI issuerUri;

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

public Integer call() throws Exception {
Expand All @@ -31,20 +46,57 @@ public Integer call() throws Exception {
var encName = "enc";

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

logger.atInfo().log("generating encryption keys");
var federationIdTokenEncryptionKey = generateJwks(KeyUse.ENCRYPTION);
var federationIdTokenEncryptionKey = generateEncryptionKey();

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

return 0;
}

private JWKSet generateJwks(KeyUse keyUse) throws JOSEException {
var key = new ECKeyGenerator(Curve.P_256).keyUse(keyUse).keyIDFromThumbprint(true).generate();
return new JWKSet(key);
private String deriveName(URI issuer) {
var s = issuer.toString();
s = s.replaceAll("^https://", "");
s = s.replaceAll("(/*)$", "");

s = s.replaceAll("[^_A-Za-z0-9]+", "_");
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();

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;
}

private JWK generateEncryptionKey() throws JOSEException {
return new ECKeyGenerator(Curve.P_256)
.keyUse(KeyUse.ENCRYPTION)
.keyIDFromThumbprint(true)
.generate();
}

private void saveJwks(String name, JWKSet set) throws IOException {
Expand Down
28 changes: 21 additions & 7 deletions ehealthid-rp/pom.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
Expand All @@ -11,8 +9,8 @@
</parent>

<artifactId>ehealthid-rp</artifactId>
<description>Standalone OpenID connect relying party for Germany's eHealthID</description>
<packaging>jar</packaging>
<description>Standalone OpenID connect relying party for Germany's eHealthID</description>

<dependencies>
<dependency>
Expand Down Expand Up @@ -157,6 +155,22 @@
<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 All @@ -166,10 +180,10 @@
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<phase>package</phase>
<configuration>
<includeScope>compile</includeScope>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
Expand All @@ -196,10 +210,10 @@
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<phase>package</phase>
</execution>
</executions>

Expand All @@ -222,10 +236,10 @@
<executions>
<execution>
<id>check</id>
<phase>verify</phase>
<goals>
<goal>check</goal>
</goals>
<phase>verify</phase>
<configuration>
<rules>
<rule>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
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.LoggingHttpClient;
import com.oviva.ehealthid.relyingparty.util.TlsContext;
import com.oviva.ehealthid.relyingparty.ws.App;
import com.oviva.ehealthid.relyingparty.ws.HealthEndpoint;
import com.oviva.ehealthid.relyingparty.ws.MetricsEndpoint;
Expand Down Expand Up @@ -122,7 +124,13 @@ public void start() throws ExecutionException, InterruptedException {
var tokenIssuer = new TokenIssuerImpl(config.baseUri(), keyStore, codeRepo);
var sessionRepo = buildSessionRepo(config.sessionStore(), meterRegistry);

var httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
var sslContext = TlsContext.fromClientCertificate(config.federation().entitySigningKey());

var httpClient =
HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.sslContext(sslContext)
.build();

var authFlow =
buildAuthFlow(
Expand All @@ -131,8 +139,12 @@ public void start() throws ExecutionException, InterruptedException {
config.federation().relyingPartyEncKeys(),
httpClient);

var discoveryHttpClient =
HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();

var jwkSource =
JWKSourceBuilder.create(new DiscoveryJwkSetSource<>(httpClient, config.idpDiscoveryUri()))
JWKSourceBuilder.create(
new DiscoveryJwkSetSource<>(discoveryHttpClient, config.idpDiscoveryUri()))
.refreshAheadCache(true)
.build();

Expand Down Expand Up @@ -170,13 +182,23 @@ public void start() throws ExecutionException, InterruptedException {
logger.atInfo().log("Management Server can be found at port {}", config.managementPort());
}

private com.oviva.ehealthid.fedclient.api.HttpClient instrumentHttpClient(
com.oviva.ehealthid.fedclient.api.HttpClient client) {
if (logger.isDebugEnabled()) {
return new LoggingHttpClient(client);
}
return client;
}

private AuthenticationFlow buildAuthFlow(
URI selfIssuer, URI fedmaster, JWKSet encJwks, HttpClient httpClient) {

// set up the file `.env.properties` to provide the X-Authorization header for the Gematik
var client = instrumentHttpClient(new JavaHttpClient(httpClient));

// setup the file `.env.properties` to provide the X-Authorization header for the Gematik
// test environment
// see: https://wiki.gematik.de/display/IDPKB/Fachdienste+Test-Umgebungen
var fedHttpClient = new GematikHeaderDecoratorHttpClient(new JavaHttpClient(httpClient));
var fedHttpClient = new GematikHeaderDecoratorHttpClient(client);

// setup as needed
var clock = Clock.systemUTC();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ public Response get() {
.idTokenEncryptedResponseEnc("A256GCM")
.scope(String.join(" ", federationConfig.scopes()))
.redirectUris(federationConfig.redirectUris())
.clientRegistrationTypes(List.of("automatic"))
.tokenEndpointAuthMethodsSupported(
List.of("self_signed_tls_client_auth"))

// according to the federation spec this is not required here, some
// sectoral IdPs require it though
.defaultAcrValues(List.of("gematik-ehealth-loa-high"))

// warn: this is a non-standard field, but needed by some sectoral IdPs
.tokenEndpointAuthMethod("self_signed_tls_client_auth")
.build())
.federationEntity(
FederationEntity.create().name(federationConfig.appName()).build())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.oviva.ehealthid.relyingparty.util;

import com.oviva.ehealthid.fedclient.api.HttpClient;
import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LoggingHttpClient implements HttpClient {

private final Logger logger = LoggerFactory.getLogger(LoggingHttpClient.class);
private final HttpClient delegate;

public LoggingHttpClient(HttpClient delegate) {
this.delegate = delegate;
}

@Override
public Response call(Request req) {

if (!logger.isDebugEnabled()) {
return delegate.call(req);
}

logger
.atDebug()
.addKeyValue("url", () -> req.uri().toString())
.addKeyValue(
"headers",
() ->
req.headers().stream()
.map(h -> h.name() + ": " + h.value())
.collect(Collectors.joining("\n")))
.addKeyValue("method", req::method)
.addKeyValue(
"body", () -> req.body() != null ? new String(req.body(), StandardCharsets.UTF_8) : "")
.log("request: %s %s".formatted(req.method(), req.uri()));

var res = delegate.call(req);

logger
.atDebug()
.addKeyValue("url", () -> req.uri().toString())
.addKeyValue("status", () -> Integer.toString(res.status()))
.addKeyValue(
"headers",
() ->
res.headers().stream()
.map(h -> h.name() + ": " + h.value())
.collect(Collectors.joining("\n")))
.addKeyValue("method", req::method)
.addKeyValue(
"body", () -> res.body() != null ? new String(res.body(), StandardCharsets.UTF_8) : "")
.log("response: %s %s %d".formatted(req.method(), req.uri(), res.status()));

return res;
}
}

0 comments on commit 70b9782

Please sign in to comment.