Skip to content

Commit

Permalink
Brazil: Create DCR 'happy flow' test
Browse files Browse the repository at this point in the history
Obtains an SSA from the directory then registers at the bank and checks
the client works.

Obtaining the SSA means we need to do discovery and client credentials
grant at the directory, this has been done using the existing test
conditions and mostly mapping the environment keys to avoid the
directory values overwriting the ones for the main part of the test.

The DCR mostly follows RFC7591, but some of the fields in the SSA do
not follow the standard - however we do very little processing SSA.

The same MTLS certificate is used to authenticate to the directory as
a client, to authenticate at the DCR endpoint and for client
authentication/certificate bound access tokens to the system under
test.

This will be a separate column on the certification page I believe so
has been done as a new test plan rather than generally adding DCR as
an option to the FAPI tests. The test plan will eventually have a few
more tests.
  • Loading branch information
jogu committed Jun 13, 2021
1 parent 59823f2 commit a44de98
Show file tree
Hide file tree
Showing 18 changed files with 714 additions and 5 deletions.
5 changes: 4 additions & 1 deletion scripts/run-test-plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -1062,7 +1062,10 @@ def is_unused(obj):
untested_test_modules.remove(m)
continue

if m in ["fapi1-advanced-final-brazil-ensure-encryption-required"]:
if m in [
"fapi1-advanced-final-brazil-ensure-encryption-required",
"fapi1-advanced-final-brazil-dcr-happy-flow"
]:
# Brazil specific tests; we don't have an automated test environment yet
untested_test_modules.remove(m)
continue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
import java.text.ParseException;

public abstract class AbstractExtractJWKsFromClientConfiguration extends AbstractCondition {
protected void extractJwks(Environment env, String key) {
JsonElement jwks = env.getElementFromObject(key, "jwks");
protected void extractJwks(Environment env, JsonElement jwks) {

if (jwks == null) {
throw error("Couldn't find JWKs in client configuration");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package net.openid.conformance.condition.client;

import com.google.gson.JsonObject;
import net.openid.conformance.condition.AbstractCondition;
import net.openid.conformance.condition.PostEnvironment;
import net.openid.conformance.condition.PreEnvironment;
import net.openid.conformance.testmodule.Environment;

public class AddSoftwareStatementToDynamicRegistrationRequest extends AbstractCondition {

@Override
@PreEnvironment(required = { "dynamic_registration_request", "software_statement_assertion" })
@PostEnvironment(required = "dynamic_registration_request")
public Environment evaluate(Environment env) {

JsonObject dynamicRegistrationRequest = env.getObject("dynamic_registration_request");
String assertion = env.getString("software_statement_assertion", "value");

dynamicRegistrationRequest.addProperty("software_statement", assertion);

log("Added software_statement to dynamic registration request", args("dynamic_registration_request", dynamicRegistrationRequest));

return env;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package net.openid.conformance.condition.client;

import com.google.common.base.Strings;
import com.google.gson.JsonObject;
import net.openid.conformance.condition.AbstractCondition;
import net.openid.conformance.condition.PostEnvironment;
import net.openid.conformance.condition.PreEnvironment;
import net.openid.conformance.testmodule.Environment;

public class AddTlsClientAuthSubjectDnToDynamicRegistrationRequest extends AbstractCondition {

@Override
@PreEnvironment(required = { "dynamic_registration_request", "certificate_subject" })
@PostEnvironment(required = "dynamic_registration_request")
public Environment evaluate(Environment env) {

JsonObject dynamicRegistrationRequest = env.getObject("dynamic_registration_request");

String subjectDn = env.getString("certificate_subject", "subjectdn");
if (Strings.isNullOrEmpty(subjectDn)) {
throw error("'subjectdn' not found in TLS certificate");
}

dynamicRegistrationRequest.addProperty("tls_client_auth_subject_dn", subjectDn);

env.putObject("dynamic_registration_request", dynamicRegistrationRequest);

log("Added tls_client_auth_subject_dn to dynamic registration request",
args("dynamic_registration_request", dynamicRegistrationRequest));

return env;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package net.openid.conformance.condition.client;

import com.google.common.base.Strings;
import com.google.gson.JsonObject;
import com.nimbusds.jose.JOSEException;
import net.openid.conformance.condition.AbstractCondition;
import net.openid.conformance.condition.PostEnvironment;
import net.openid.conformance.condition.PreEnvironment;
import net.openid.conformance.testmodule.Environment;
import net.openid.conformance.util.JWTUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.client.RestTemplate;

import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.spec.InvalidKeySpecException;
import java.text.ParseException;
import java.util.Collections;


public class CallDirectorySoftwareStatementEndpointWithBearerToken extends AbstractCondition {

@Override
@PreEnvironment(required = { "access_token", "config", "certificate_subject" })
@PostEnvironment(required = { "software_statement_assertion" })
public Environment evaluate(Environment env) {

String accessToken = env.getString("access_token", "value");
if (Strings.isNullOrEmpty(accessToken)) {
throw error("Access token not found");
}

String tokenType = env.getString("access_token", "type");
if (Strings.isNullOrEmpty(tokenType)) {
throw error("Token type not found");
} else if (!tokenType.equalsIgnoreCase("Bearer")) {
throw error("Access token is not a bearer token", args("token_type", tokenType));
}

// https://matls-api.sandbox.directory.openbankingbrasil.org.br/organisations/${ORGID}/softwarestatements/${SSID}/assertion
String apibase = env.getString("config", "directory.apibase");
if (Strings.isNullOrEmpty(apibase)) {
throw error("directory.apibase missing from test configuration");
}
String ou = env.getString("certificate_subject", "ou");
if (Strings.isNullOrEmpty(ou)) {
throw error("'ou' not found in TLS certificate subject");
}
String cn = env.getString("certificate_subject", "cn");
if (Strings.isNullOrEmpty(cn)) {
throw error("'cn' not found in TLS certificate subject");
}

String resourceEndpoint = String.format("%sorganisations/%s/softwarestatements/%s/assertion", apibase, ou, cn);

try {
RestTemplate restTemplate = createRestTemplate(env);

HttpHeaders headers = new HttpHeaders();

headers.setAccept(Collections.singletonList(DATAUTILS_MEDIATYPE_APPLICATION_JWT_UTF8));
headers.set("Authorization", "Bearer " + accessToken);

// Stop RestTemplate from overwriting the Accept-Charset header
StringHttpMessageConverter converter = new StringHttpMessageConverter();
converter.setWriteAcceptCharset(false);
restTemplate.setMessageConverters(Collections.singletonList(converter));

HttpEntity<String> request = new HttpEntity<>(null, headers);

ResponseEntity<String> response = restTemplate.exchange(resourceEndpoint, HttpMethod.GET, request, String.class);

String jsonString = response.getBody();

if (Strings.isNullOrEmpty(jsonString)) {
throw error("Empty/missing response from the software statement endpoint");
} else {
log("software statement endpoint response", args("response", jsonString));

JsonObject jwtAsJsonObject;
try {
jwtAsJsonObject = JWTUtil.jwtStringToJsonObjectForEnvironment(jsonString, null, null);
} catch (JOSEException | ParseException e) {
throw error("Couldn't parse software statement as a JWT", args("ssa", jsonString, "error", e.getMessage()));
}
if (jwtAsJsonObject == null) {
throw error("Couldn't parse software statement as a JWT", args("ssa", jsonString));
}


env.putObject("software_statement_assertion", jwtAsJsonObject);

logSuccess("Parsed assertion endpoint response", jwtAsJsonObject);

return env;
}
} catch (RestClientResponseException e) {
throw error("Error from the software statement endpoint", e, args("code", e.getRawStatusCode(), "status", e.getStatusText(), "body", e.getResponseBodyAsString()));
} catch (NoSuchAlgorithmException | KeyManagementException | CertificateException | InvalidKeySpecException | KeyStoreException | IOException | UnrecoverableKeyException e) {
throw error("Error creating HTTP Client", e);
} catch (RestClientException e) {
String msg = "Call to software statement endpoint " + resourceEndpoint + " failed";
if (e.getCause() != null) {
msg += " - " +e.getCause().getMessage();
}
throw error(msg, e);
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package net.openid.conformance.condition.client;

import com.google.common.base.Strings;
import com.google.gson.JsonObject;
import net.openid.conformance.condition.AbstractCondition;
import net.openid.conformance.condition.PostEnvironment;
import net.openid.conformance.condition.PreEnvironment;
import net.openid.conformance.testmodule.Environment;
import org.bouncycastle.asn1.x500.RDN;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x500.style.IETFUtils;
import org.bouncycastle.jce.PrincipalUtil;
import org.bouncycastle.jce.X509Principal;

import java.io.ByteArrayInputStream;
import java.security.NoSuchProviderException;
import java.security.Principal;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Base64;

public class ExtractClientMTLSCertificateSubject extends AbstractCondition {

@Override
@PreEnvironment(required = "mutual_tls_authentication")
@PostEnvironment(required = "certificate_subject")
public Environment evaluate(Environment env) {
String certString = env.getString("mutual_tls_authentication", "cert");

if (Strings.isNullOrEmpty(certString)) {
throw error("Couldn't find TLS client certificate for MTLS");
}

CertificateFactory certFactory = null;
try {
certFactory = CertificateFactory.getInstance("X.509", "BC");
} catch (CertificateException | NoSuchProviderException | IllegalArgumentException e) {
throw error("Couldn't get CertificateFactory", e);
}

X509Certificate certificate = generateCertificateFromMTLSCert(certString, certFactory);
X500Name x500name = X500Name.getInstance(certificate.getSubjectX500Principal().getEncoded());

Principal principal = certificate.getSubjectDN();
String subjectDn = principal.getName();

RDN ou = x500name.getRDNs(BCStyle.OU)[0];
String ouAsString = IETFUtils.valueToString(ou.getFirst().getValue());

RDN cn = x500name.getRDNs(BCStyle.CN)[0];
String cnAsString = IETFUtils.valueToString(cn.getFirst().getValue());

JsonObject o = new JsonObject();
o.addProperty("subjectdn", subjectDn);
o.addProperty("ou", ouAsString);
o.addProperty("cn", cnAsString);

env.putObject("certificate_subject", o);

logSuccess("Extracted subject from MTLS certificate", o);

return env;
}

private X509Certificate generateCertificateFromMTLSCert(String certString, CertificateFactory certFactory) {
byte[] decodedCert;
try {
decodedCert = Base64.getDecoder().decode(certString);
} catch (IllegalArgumentException e) {
throw error("base64 decode of cert failed", e, args("cert", certString));
}

X509Certificate certificate;
try {
certificate = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(decodedCert));
} catch (CertificateException | IllegalArgumentException e) {
throw error("Calling generateCertificate on cert failed", e, args("cert", certString));
}
return certificate;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package net.openid.conformance.condition.client;

import com.google.common.base.Strings;
import com.google.gson.JsonObject;
import net.openid.conformance.condition.AbstractCondition;
import net.openid.conformance.condition.PostEnvironment;
import net.openid.conformance.condition.PreEnvironment;
import net.openid.conformance.testmodule.Environment;

public class ExtractDirectoryConfiguration extends AbstractCondition {

@Override
@PreEnvironment(required = "config")
@PostEnvironment(required = { "directory_client", "directory_config" })
public Environment evaluate(Environment env) {
String discoveryUrl = env.getString("config", "directory.discoveryUrl");
if (Strings.isNullOrEmpty(discoveryUrl)) {
throw error("directory.discoveryUrl missing from test configuration");
}

String clientId = env.getString("config", "directory.client_id");
if (Strings.isNullOrEmpty(clientId)) {
throw error("directory.client_id missing from test configuration");
}

JsonObject server = new JsonObject();
server.addProperty("discoveryUrl", "https://auth.sandbox.directory.openbankingbrasil.org.br/.well-known/openid-configuration");
JsonObject directoryConfig = new JsonObject();
directoryConfig.add("server", server);
env.putObject("directory_config", directoryConfig);


JsonObject directoryClient = new JsonObject();
directoryClient.addProperty("client_id", "NqKz9-A_LivAIOAjHyXpL");
env.putObject("directory_client", directoryClient);

logSuccess("Extracted directory configuration parameters",
args("directory_config", directoryConfig,
"directory_client", directoryClient));

return env;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package net.openid.conformance.condition.client;

import com.google.gson.JsonElement;
import net.openid.conformance.condition.PostEnvironment;
import net.openid.conformance.condition.PreEnvironment;
import net.openid.conformance.testmodule.Environment;

// Extracts the JWKS from the 'client' element within the configuration
public class ExtractJWKSDirectFromClientConfiguration extends AbstractExtractJWKsFromClientConfiguration {

@Override
@PreEnvironment(required = "config")
@PostEnvironment(required = {"client_jwks", "client_public_jwks" })
public Environment evaluate(Environment env) {
// bump the client's internal JWK up to the root
JsonElement jwks = env.getElementFromObject("config", "client.jwks");
extractJwks(env, jwks);

return env;
}

}
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
package net.openid.conformance.condition.client;

import com.google.gson.JsonElement;
import net.openid.conformance.condition.PostEnvironment;
import net.openid.conformance.condition.PreEnvironment;
import net.openid.conformance.testmodule.Environment;

// Extracts the JWKS from the 'client' element that 'GetStaticClientConfiguration' added to the environment root
public class ExtractJWKsFromStaticClientConfiguration extends AbstractExtractJWKsFromClientConfiguration {

@Override
@PreEnvironment(required = "client")
@PostEnvironment(required = {"client_jwks", "client_public_jwks" })
public Environment evaluate(Environment env) {
// bump the client's internal JWK up to the root

extractJwks(env, "client");
JsonElement jwks = env.getElementFromObject("client", "jwks");
extractJwks(env, jwks);

return env;
}
Expand Down
Loading

0 comments on commit a44de98

Please sign in to comment.