Skip to content

Commit

Permalink
ARC-1233: Basic working version with GesundheitsID
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasrichner-oviva committed Feb 5, 2024
1 parent 6bf89e9 commit 3dd6e77
Show file tree
Hide file tree
Showing 38 changed files with 1,436 additions and 716 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ gesundheitsid/env.properties
gesundheitsid/dependency-reduced-pom.xml
.flattened-pom.xml
*_jwks.json
env.properties
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

1. Setup your test environment, your own issuer **MUST** serve a **VALID** and **TRUSTED** entity
statement. See [Gematik docs](https://wiki.gematik.de/pages/viewpage.action?pageId=544316583)
2. Setup the file `.env.properties` to provide
2. Setup the file `env.properties` to provide
the [X-Authorization header](https://wiki.gematik.de/display/IDPKB/Fachdienste+Test-Umgebungen)
for the Gematik
3. Setup the JWK sets for signing and encryption keys
Expand Down Expand Up @@ -94,6 +94,7 @@ Setting up a proxy with a header filter can get around that limitation though.
## Helpful Links
- [Gematik Sectoral IDP Specifications v2.0.1](https://fachportal.gematik.de/fachportal-import/files/gemSpec_IDP_Sek_V2.0.1.pdf)
- [AppFlow - Authentication flow to implement](https://wiki.gematik.de/display/IDPKB/App-App+Flow#AppAppFlow-0-FederationMaster)
- [Sektoraler IDP - Examples & Reference Implementation](https://wiki.gematik.de/display/IDPKB/Sektoraler+IDP+-+Referenzimplementierung+und+Beispiele)
- [OpenID Federation Spec](https://openid.net/specs/openid-federation-1_0.html)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSObject;
import com.nimbusds.jose.Payload;
import com.nimbusds.jose.crypto.ECDSASigner;
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.JWKSet;
import com.oviva.gesundheitsid.util.JsonCodec;
import java.time.Instant;
import java.util.List;

Expand Down Expand Up @@ -38,6 +47,25 @@ public static Builder create() {
return new Builder();
}

public JWSObject sign(ECKey key) {
try {
var signer = new ECDSASigner(key);

var h =
new JWSHeader.Builder(JWSAlgorithm.ES256)
.type(new JOSEObjectType(EntityStatementJWS.ENTITY_STATEMENT_TYP))
.keyID(key.getKeyID())
.build();

var jwsObject = new JWSObject(h, new Payload(JsonCodec.writeValueAsString(this)));
jwsObject.sign(signer);

return jwsObject;
} catch (JOSEException e) {
throw new IllegalArgumentException("failed to sign entity statement", e);
}
}

@JsonIgnoreProperties(ignoreUnknown = true)
public record OpenidProvider(
@JsonProperty("pushed_authorization_request_endpoint")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

public record EntityStatementJWS(JWSObject jws, EntityStatement body) implements TemporalValid {

private static final String ENTITY_STATEMENT_TYP = "entity-statement+jwt";
public static final String ENTITY_STATEMENT_TYP = "entity-statement+jwt";

public static EntityStatementJWS parse(String wire) {
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.oviva.gesundheitsid.util;

import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
Expand All @@ -13,6 +14,7 @@ public class JsonCodec {
var om = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

om.registerModule(new JoseModule());
om.setSerializationInclusion(Include.NON_NULL);

JsonCodec.om = om;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.oviva.gesundheitsid.util;

import com.nimbusds.jose.jwk.JWKSet;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.ParseException;

public class JwksUtils {

private JwksUtils() {}

public static JWKSet load(Path path) {

try (var fin = Files.newInputStream(path)) {
return JWKSet.load(fin);
} catch (IOException | ParseException e) {
var fullPath = path.toAbsolutePath();
throw new RuntimeException(
"failed to load JWKS from '%s' ('%s')".formatted(path, fullPath), e);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
package com.oviva.gesundheitsid.test;
package com.oviva.gesundheitsid.util;

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSObject;
import com.nimbusds.jose.Payload;
import com.nimbusds.jose.crypto.ECDSASigner;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.ECKey;

public class JwsUtils {

private JwsUtils() {}

public static JWSObject toJws(JWKSet jwks, String payload) {
public static JWSObject toJws(ECKey key, String payload) {
try {
var key = jwks.getKeys().get(0);
var signer = new ECDSASigner(key.toECKey());
var signer = new ECDSASigner(key);

var h = new JWSHeader.Builder(JWSAlgorithm.ES256).keyID(key.getKeyID()).build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import com.oviva.gesundheitsid.fedclient.api.UrlFormBodyBuilder;
import com.oviva.gesundheitsid.test.Environment;
import com.oviva.gesundheitsid.test.GematikHeaderDecoratorHttpClient;
import com.oviva.gesundheitsid.test.JwksUtils;
import com.oviva.gesundheitsid.util.JwksUtils;
import java.io.IOException;
import java.net.URI;
import java.net.URLDecoder;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,36 @@
package com.oviva.gesundheitsid.crypto;

import static com.oviva.gesundheitsid.test.JwksUtils.toJwks;
import static com.oviva.gesundheitsid.test.JwsUtils.*;
import static com.oviva.gesundheitsid.test.JwsUtils.garbageSignature;
import static com.oviva.gesundheitsid.test.JwsUtils.tamperSignature;
import static com.oviva.gesundheitsid.util.JwsUtils.*;
import static com.oviva.gesundheitsid.util.JwsUtils.garbageSignature;
import static com.oviva.gesundheitsid.util.JwsUtils.tamperSignature;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSObject;
import com.nimbusds.jose.Payload;
import com.nimbusds.jose.jwk.Curve;
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.JWKSet;
import com.oviva.gesundheitsid.test.ECKeyPairGenerator;
import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
import java.text.ParseException;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

class JwsVerifierTest {

private static ECKeyPair ECKEY = ECKeyPairGenerator.generate();
private static ECKey ECKEY;
private static JWKSet JWKS;

@BeforeAll
static void beforeAll() throws JOSEException {
ECKEY = new ECKeyGenerator(Curve.P_256).keyIDFromThumbprint(true).generate();
JWKS = new JWKSet(ECKEY);
}

@Test
void verifyEmptyJwks() {
Expand All @@ -38,36 +48,30 @@ void verifyNoJwks() {
@Test
void verify() throws ParseException {

var jwks = toJwks(ECKEY);

var jws = toJws(jwks, "hello world?").serialize();
var jws = toJws(ECKEY, "hello world?").serialize();

var in = JWSObject.parse(jws);

assertTrue(JwsVerifier.verify(jwks, in));
assertTrue(JwsVerifier.verify(JWKS, in));
}

@Test
void verifyBadSignature() throws ParseException {

var jwks = toJwks(ECKEY);

var jws = toJws(jwks, "test").serialize();
var jws = toJws(ECKEY, "test").serialize();

jws = tamperSignature(jws);

var in = JWSObject.parse(jws);

// when & then
assertFalse(JwsVerifier.verify(jwks, in));
assertFalse(JwsVerifier.verify(JWKS, in));
}

@Test
void verifyUnknownKey() throws ParseException {

var trustedJwks = toJwks(ECKEY);
void verifyUnknownKey() throws ParseException, JOSEException {

var signerJwks = toJwks(ECKeyPairGenerator.generate());
var signerJwks = new ECKeyGenerator(Curve.P_256).generate();

var jws = toJws(signerJwks, "test").serialize();

Expand All @@ -76,32 +80,29 @@ void verifyUnknownKey() throws ParseException {
var in = JWSObject.parse(jws);

// when & then
assertFalse(JwsVerifier.verify(trustedJwks, in));
assertFalse(JwsVerifier.verify(JWKS, in));
}

@Test
void verifyGarbageSignature() throws ParseException {
var jwks = toJwks(ECKEY);

var jws = toJws(jwks, "test").serialize();
var jws = toJws(ECKEY, "test").serialize();
jws = garbageSignature(jws);

var in = JWSObject.parse(jws);

// when & then
assertFalse(JwsVerifier.verify(jwks, in));
assertFalse(JwsVerifier.verify(JWKS, in));
}

@Test
void verify_badAlg() {

var jwks = toJwks(ECKEY);

var h = new JWSHeader(JWSAlgorithm.RS256);
var in = new JWSObject(h, new Payload("hello?"));

// when
var e = assertThrows(UnsupportedOperationException.class, () -> JwsVerifier.verify(jwks, in));
var e = assertThrows(UnsupportedOperationException.class, () -> JwsVerifier.verify(JWKS, in));

// then
assertEquals("only supports ES256, found: RS256", e.getMessage());
Expand Down
Loading

0 comments on commit 3dd6e77

Please sign in to comment.