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 1811c12
Show file tree
Hide file tree
Showing 9 changed files with 429 additions and 70 deletions.
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 (!idpEntitytStatement.isValidAt(clock.instant())) {
throw FederationExceptions.entityStatementTimeNotValid(
trustedFederationStatement.body().sub());
if (!trustedEntityConfiguration.verifySignature(trustStore)) {
throw FederationExceptions.untrustedFederationStatement(sub.toString());
}

return idpEntitytStatement;
if (!trustStore.equals(trustedEntityConfiguration.body().jwks())
&& !trustedEntityConfiguration.verifySelfSigned()) {
throw FederationExceptions.entityStatementBadSignature(sub.toString());
}

return trustedEntityConfiguration;
}

private EntityStatementJWS fetchTrustedFederationStatement(URI issuer) {
Expand All @@ -74,6 +79,10 @@ private EntityStatementJWS fetchTrustedFederationStatement(URI issuer) {
throw FederationExceptions.entityStatementTimeNotValid(federationStatement.body().sub());
}

if (!federationStatement.verifySignature(masterEntityConfiguration.body().jwks())) {
throw FederationExceptions.entityStatementBadSignature(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 1811c12

Please sign in to comment.