Skip to content

Commit

Permalink
ARC-1215: Moar coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasrichner-oviva committed Jan 31, 2024
1 parent 50f5e81 commit b77c4e0
Show file tree
Hide file tree
Showing 10 changed files with 589 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ public static RuntimeException entityStatementBadSignature(String sub) {
return new RuntimeException("entity statement of '%s' has a bad signature".formatted(sub));
}

public static RuntimeException federationStatementTimeNotValid(String sub) {
return new RuntimeException(
"federation statement of '%s' expired or not yet valid".formatted(sub));
}

public static RuntimeException federationStatementBadSignature(String sub) {
return new RuntimeException("federation statement of '%s' has a bad signature".formatted(sub));
}

public static RuntimeException untrustedFederationStatement(String sub) {
return new RuntimeException("federation statement untrusted: sub=%s".formatted(sub));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.oviva.gesundheitsid.fedclient;

import com.nimbusds.jose.jwk.JWKSet;
import com.oviva.gesundheitsid.fedclient.api.EntityStatement;
import com.oviva.gesundheitsid.fedclient.api.EntityStatementJWS;
import com.oviva.gesundheitsid.fedclient.api.FederationApiClient;
import com.oviva.gesundheitsid.fedclient.api.IdpList.IdpEntity;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.net.URI;
import java.time.Clock;
import java.util.List;
Expand Down Expand Up @@ -36,28 +38,31 @@ public EntityStatementJWS establishIdpTrust(URI issuer) {

var trustedFederationStatement = fetchTrustedFederationStatement(issuer);

if (!trustedFederationStatement.isValidAt(clock.instant())) {
throw FederationExceptions.entityStatementTimeNotValid(
trustedFederationStatement.body().sub());
}

// the federation statement from the master will establish trust in the JWKS and the issuer URL
// of the idp,
// we still need to fetch the entity configuration directly afterward to get the full
// entity statement

var idpEntitytStatement = apiClient.fetchEntityConfiguration(issuer);
if (!idpEntitytStatement.verifySignature(trustedFederationStatement.body().jwks())) {
throw FederationExceptions.untrustedFederationStatement(
trustedFederationStatement.body().sub());
return fetchTrustedEntityConfiguration(issuer, trustedFederationStatement.body().jwks());
}

private EntityStatementJWS fetchTrustedEntityConfiguration(@NonNull URI sub, JWKSet trustStore) {

var trustedEntityConfiguration = apiClient.fetchEntityConfiguration(sub);
if (!trustedEntityConfiguration.isValidAt(clock.instant())) {
throw FederationExceptions.entityStatementTimeNotValid(sub.toString());
}

if (!trustedEntityConfiguration.verifySignature(trustStore)) {
throw FederationExceptions.untrustedFederationStatement(sub.toString());
}

if (!idpEntitytStatement.isValidAt(clock.instant())) {
throw FederationExceptions.entityStatementTimeNotValid(
trustedFederationStatement.body().sub());
if (!trustStore.equals(trustedEntityConfiguration.body().jwks())
&& !trustedEntityConfiguration.verifySelfSigned()) {
throw FederationExceptions.entityStatementBadSignature(sub.toString());
}

return idpEntitytStatement;
return trustedEntityConfiguration;
}

private EntityStatementJWS fetchTrustedFederationStatement(URI issuer) {
Expand All @@ -66,12 +71,23 @@ private EntityStatementJWS fetchTrustedFederationStatement(URI issuer) {

var federationFetchEndpoint = getFederationFetchEndpoint(masterEntityConfiguration.body());

return fetchTrustedFederationStatement(
federationFetchEndpoint, masterEntityConfiguration.body().jwks(), issuer);
}

private EntityStatementJWS fetchTrustedFederationStatement(
URI federationFetchEndpoint, JWKSet fedmasterTrustStore, URI issuer) {

var federationStatement =
apiClient.fetchFederationStatement(
federationFetchEndpoint, fedMasterUri.toString(), issuer.toString());

if (!federationStatement.isValidAt(clock.instant())) {
throw FederationExceptions.entityStatementTimeNotValid(federationStatement.body().sub());
throw FederationExceptions.federationStatementTimeNotValid(federationStatement.body().sub());
}

if (!federationStatement.verifySignature(fedmasterTrustStore)) {
throw FederationExceptions.federationStatementBadSignature(issuer.toString());
}

return federationStatement;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.oviva.gesundheitsid.fedclient.api;

import edu.umd.cs.findbugs.annotations.NonNull;
import java.net.URI;

/** very primitive cached client, there is no cache eviction here */
Expand All @@ -24,6 +25,7 @@ public CachedFederationApiClient(
this.idpListCache = idpListCache;
}

@NonNull
@Override
public EntityStatementJWS fetchFederationStatement(
URI federationFetchUrl, String issuer, String subject) {
Expand All @@ -32,14 +34,15 @@ public EntityStatementJWS fetchFederationStatement(
key, k -> delegate.fetchFederationStatement(federationFetchUrl, issuer, subject));
}

@NonNull
@Override
public IdpListJWS fetchIdpList(URI idpListUrl) {
return idpListCache.computeIfAbsent(
idpListUrl.toString(), k -> delegate.fetchIdpList(idpListUrl));
}

@Override
public EntityStatementJWS fetchEntityConfiguration(URI entityUrl) {
public @NonNull EntityStatementJWS fetchEntityConfiguration(URI entityUrl) {
return entityStatementCache.computeIfAbsent(
entityUrl.toString(), k -> delegate.fetchEntityConfiguration(entityUrl));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ public static final class Builder {
private String contacts;
private String homepageUri;

private String federationFetchEndpoint;
private String federationListEndpoint;
private String idpListEndpoint;

private Builder() {}

public Builder name(String name) {
Expand All @@ -130,8 +134,29 @@ public Builder homepageUri(String homepageUri) {
return this;
}

public Builder federationFetchEndpoint(String federationFetchEndpoint) {
this.federationFetchEndpoint = federationFetchEndpoint;
return this;
}

public Builder federationListEndpoint(String federationListEndpoint) {
this.federationListEndpoint = federationListEndpoint;
return this;
}

public Builder idpListEndpoint(String idpListEndpoint) {
this.idpListEndpoint = idpListEndpoint;
return this;
}

public FederationEntity build() {
return new FederationEntity(name, contacts, homepageUri, null, null, null);
return new FederationEntity(
name,
contacts,
homepageUri,
federationFetchEndpoint,
federationListEndpoint,
idpListEndpoint);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package com.oviva.gesundheitsid.fedclient.api;

import edu.umd.cs.findbugs.annotations.NonNull;
import java.net.URI;

public interface FederationApiClient {

@NonNull
EntityStatementJWS fetchFederationStatement(
URI federationFetchUrl, String issuer, String subject);

@NonNull
IdpListJWS fetchIdpList(URI idpListUrl);

@NonNull
EntityStatementJWS fetchEntityConfiguration(URI entityUrl);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.oviva.gesundheitsid.fedclient.api.HttpClient.Header;
import com.oviva.gesundheitsid.fedclient.api.HttpClient.Request;
import edu.umd.cs.findbugs.annotations.NonNull;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.UriBuilder;
Expand All @@ -22,6 +23,7 @@ public FederationApiClientImpl(HttpClient client) {
this.httpClient = client;
}

@NonNull
@Override
public EntityStatementJWS fetchFederationStatement(
URI federationFetchUrl, String issuer, String subject) {
Expand All @@ -32,6 +34,7 @@ public EntityStatementJWS fetchFederationStatement(
return EntityStatementJWS.parse(body);
}

@NonNull
@Override
public IdpListJWS fetchIdpList(URI idpListUrl) {

Expand All @@ -40,7 +43,7 @@ public IdpListJWS fetchIdpList(URI idpListUrl) {
}

@Override
public EntityStatementJWS fetchEntityConfiguration(URI entityUrl) {
public @NonNull EntityStatementJWS fetchEntityConfiguration(URI entityUrl) {

var uri =
UriBuilder.fromUri(entityUrl)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
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 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.crypto.ECDSASigner;
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.oviva.gesundheitsid.test.ECKeyPairGenerator.ECKeyPair;
import java.io.IOException;
import java.text.ParseException;
import java.util.List;
import org.junit.jupiter.api.Test;

class JwsVerifierTest {
Expand All @@ -41,23 +37,23 @@ void verifyNoJwks() {
}

@Test
void verify() throws IOException, JOSEException, ParseException {
void verify() throws ParseException {

var jwks = toJwks(ECKEY);

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

var in = JWSObject.parse(jws);

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

@Test
void verifyBadSignature() throws JOSEException, ParseException {
void verifyBadSignature() throws ParseException {

var jwks = toJwks(ECKEY);

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

jws = tamperSignature(jws);

Expand All @@ -68,13 +64,13 @@ void verifyBadSignature() throws JOSEException, ParseException {
}

@Test
void verifyUnknownKey() throws JOSEException, ParseException {
void verifyUnknownKey() throws ParseException {

var trustedJwks = toJwks(ECKEY);

var signerJwks = toJwks(ECKeyPairGenerator.generate());

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

jws = tamperSignature(jws);

Expand All @@ -85,10 +81,10 @@ void verifyUnknownKey() throws JOSEException, ParseException {
}

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

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

var in = JWSObject.parse(jws);
Expand All @@ -97,32 +93,8 @@ void verifyGarbageSignature() throws JOSEException, ParseException {
assertFalse(JwsVerifier.verify(jwks, in));
}

private String toJws(JWKSet jwks, String payload) throws JOSEException {
var key = jwks.getKeys().get(0);
var signer = new ECDSASigner(key.toECKey());

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

var jwsObject = new JWSObject(h, new Payload(payload));
jwsObject.sign(signer);

return jwsObject.serialize();
}

private JWKSet toJwks(ECKeyPair pair) throws JOSEException {

var jwk =
new ECKey.Builder(Curve.P_256, pair.pub())
.privateKey(pair.priv())
.keyIDFromThumbprint()
.build();

// JWK with extra steps, otherwise Keycloak can't deal with the parsed key
return new JWKSet(List.of(jwk));
}

@Test
void verify_badAlg() throws JOSEException {
void verify_badAlg() {

var jwks = toJwks(ECKEY);

Expand Down
Loading

0 comments on commit b77c4e0

Please sign in to comment.