diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a08ae2b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM ubuntu + +# Get required dependencies to install sdkman +RUN apt-get clean +RUN apt-get update +RUN rm /bin/sh && ln -s /bin/bash /bin/sh +RUN apt-get -qq -y install curl wget unzip zip + +# Install required versions of java, maven and kotlin +RUN curl -s "https://get.sdkman.io" | bash +RUN source "$HOME/.sdkman/bin/sdkman-init.sh" && \ + yes | sdk install java 11.0.10-open && \ + yes | sdk install maven 3.8.1 && \ + yes | sdk install kotlin 1.4.31 + +# Copy testing folder and required keys for testing +COPY tests/ tests/ +COPY keys/ keys/ + +# Pre-install java and kotlin libraries required +RUN source "$HOME/.sdkman/bin/sdkman-init.sh" && \ + cd tests && mvn package -Dmaven.test.skip + +# Run tests +CMD source "$HOME/.sdkman/bin/sdkman-init.sh" && cd tests && mvn clean test && mvn clean \ No newline at end of file diff --git a/README.md b/README.md index 35aa7b5..a4ece9e 100644 --- a/README.md +++ b/README.md @@ -121,3 +121,58 @@ Using the API requires an access token with the special scope `omejdn:admin` (to The Omejdn Config API is documented [here](https://github.com/Fraunhofer-AISEC/omejdn-server/blob/master/docs/). +## Testing the DAPS + + You can test the DAPS implementation with the provided Dockerfile, however, previous configuration is required. Before creating the image with the Dockerfile, the certificates and keys for 4 clients, and the DAPS signing key should be placed in the `keys` directory. A configuration file should be placed in `tests/test_config.txt`. The configuration file contains information about the clients in order to correctly request DAT tokens. An example configuration file is as follows: + ``` +iss=7D:50:61:67:B9:6E:A5:99:A9:58:30:1A:81:C7:78:8E:19:4E:20:C4:keyid:7D:50:61:67:B9:6E:A5:99:A9:58:30:1A:81:C7:78:8E:19:4E:20:C4 +aud=idsc:IDS_CONNECTORS_ALL +iss_daps=http://omejdn:4567 +securityProfile=idsc:BASE_SECURITY_PROFILE +referringConnector=http://test1.demo +@type=ids:DatPayload +@context=https://w3id.org/idsa/contexts/context.jsonld +scope=idsc:IDS_CONNECTOR_ATTRIBUTES_ALL +transportCertsSha256=0c07ba5e4c305e9d1bd3d14c6e6e6b8166864e57c5b0c43b46b39d77994880b6 +keyPath=../keys/test1.key +keyPath2=../keys/test2.key +iss2=30:C1:05:0A:2E:00:41:BB:6C:7B:B6:78:A1:F2:67:C7:B8:B1:02:34:keyid:30:C1:05:0A:2E:00:41:BB:6C:7B:B6:78:A1:F2:67:C7:B8:B1:02:34 +url=http://localhost:4567/ +iss_256=E6:60:A2:C2:C5:97:F1:76:21:DE:C4:08:26:85:E9:74:DE:0E:49:FB:keyid:E6:60:A2:C2:C5:97:F1:76:21:DE:C4:08:26:85:E9:74:DE:0E:49:FB +securityProfile_256=idsc:BASE_SECURITY_PROFILE +referringConnector_256=http://ec256.demo +scope_256=idsc:IDS_CONNECTOR_ATTRIBUTES_ALL +transportCertsSha256_256=9f106ca3c67d4c5f997ae48fefe1107f583ff5d58a6445572944fda901916863 +keyPath3=../keys/ec256.key +iss_512=2C:9E:A1:D0:CF:4B:9A:37:38:FD:32:3F:1A:49:CE:25:98:73:B3:0F:keyid:2C:9E:A1:D0:CF:4B:9A:37:38:FD:32:3F:1A:49:CE:25:98:73:B3:0F +securityProfile_512=idsc:BASE_SECURITY_PROFILE +referringConnector_512=http://ec521.demo +scope_512=idsc:IDS_CONNECTOR_ATTRIBUTES_ALL +transportCertsSha256_512=7c5b1aba8484fc8721ac75c02fddfa6b3ccd9da414cb44177a65fd96d65abf53 +keyPath4=../keys/ec521.key + ``` + Each line in the configuration file is an attribute required in that specific order and to be separated with an equal sign without spaces. The attributes refer to: + - iss: `client_id` for the first client. + - aud: Audience for the first client. + - iss_daps: DAPS issuer for DAT tokens. + - securityProfile: Expected security profile in DAT. + - referringConnector: URI of the first client. + - @type: Type of the DAT token. + - @context: Context containing the IDS classes. + - scope: List of scopes in the DAT. + - transporteCertSha256: The public transportation key from the first client used to request a DAT token. + - keyPath: Path to the first client's key. + - keyPath2: Path to the second client's key. + - iss2: `client_id` for the second client. + - url: Address at which the DAPS server can be contacted. + - iss_256, securityProfile_256, referingConnector_256, scope_256, transportCertSha256_256, keyPath3: These refer to the third client, which is an ES256 certificate, see above for a detailed explanation on each attribute. + - iss_512, securityProfile_512, referingConnector_512, scope_512, transportCertSha256_512, keyPath4: These refer to the fourth client, which is an ES512 certificate, see above for a detailed explanation on each attribute. + + Once all the required material for the testing is ready, we can start testing a DAPS instance by creating a docker image and the running a container. In order to create the image execute: + ``` + $ docker build . -t daps-test + ``` +And then to run the container with the tests simply execute: + ``` + $ docker run -v $PWD/tests:/tests -v $PWD/keys:/keys --name=test daps-test + ``` diff --git a/omejdn-server b/omejdn-server index 06a009d..52aabc6 160000 --- a/omejdn-server +++ b/omejdn-server @@ -1 +1 @@ -Subproject commit 06a009da26d2543648c327fb826218ca410c0e27 +Subproject commit 52aabc631395f9bc9048ccf376d1fea40b648939 diff --git a/scripts/register_connector.sh b/scripts/register_connector.sh index 553b10e..ab12e1b 100755 --- a/scripts/register_connector.sh +++ b/scripts/register_connector.sh @@ -10,14 +10,14 @@ CLIENT_NAME=$1 CLIENT_SECURITY_PROFILE=$2 [ -z "$CLIENT_SECURITY_PROFILE" ] && CLIENT_SECURITY_PROFILE="idsc:BASE_SECURITY_PROFILE" -CLIENT_CERT="keys/$CLIENT_NAME.cert" +CLIENT_CERT="keys/clients/$CLIENT_NAME.cert" if [ -n "$3" ]; then [ ! -f "$3" ] && (echo "Cert not found"; exit 1) cert_format="DER" openssl x509 -noout -in "$3" 2>/dev/null && cert_format="PEM" openssl x509 -inform "$cert_format" -in "$3" -text > "$CLIENT_CERT" else - openssl req -newkey rsa:2048 -new -batch -nodes -x509 -days 3650 -text -keyout "keys/${CLIENT_NAME}.key" -out "$CLIENT_CERT" + openssl req -newkey rsa:2048 -new -batch -nodes -x509 -days 3650 -text -keyout "keys/clients/${CLIENT_NAME}.key" -out "$CLIENT_CERT" fi SKI="$(grep -A1 "Subject Key Identifier" "$CLIENT_CERT" | tail -n 1 | tr -d ' ')" diff --git a/scripts/register_ec_connector.sh b/scripts/register_ec_connector.sh new file mode 100644 index 0000000..d6f9bf3 --- /dev/null +++ b/scripts/register_ec_connector.sh @@ -0,0 +1,50 @@ +#!/bin/sh + +if [ ! $# -ge 2 ] || [ ! $# -le 3 ]; then + echo "Usage: $0 NAME EC_BITS (SECURITY_PROFILE)" + exit 1 +fi + +CLIENT_NAME=$1 +CLIENT_CERT="keys/clients/$CLIENT_NAME.cert" + +if [ $2 = "521" ]; then + openssl genpkey -genparam -algorithm ec -pkeyopt ec_paramgen_curve:P-521 -out EC521PARAM.key + openssl req -newkey ec:EC521PARAM.key -new -batch -nodes -x509 -days 3650 -text -keyout "keys/clients/${CLIENT_NAME}.key" -out "$CLIENT_CERT" + rm EC521PARAM.key +else + openssl genpkey -genparam -algorithm ec -pkeyopt ec_paramgen_curve:P-256 -out EC256PARAM.key + openssl req -newkey ec:EC256PARAM.key -new -batch -nodes -x509 -days 3650 -text -keyout "keys/clients/${CLIENT_NAME}.key" -out "$CLIENT_CERT" + rm EC256PARAM.key +fi + +CLIENT_SECURITY_PROFILE=$3 +[ -z "$CLIENT_SECURITY_PROFILE" ] && CLIENT_SECURITY_PROFILE="idsc:BASE_SECURITY_PROFILE" + +SKI="$(grep -A1 "Subject Key Identifier" "$CLIENT_CERT" | tail -n 1 | tr -d ' ')" +AKI="$(grep -A1 "Authority Key Identifier" "$CLIENT_CERT" | tail -n 1 | tr -d ' ')" +CLIENT_ID="$SKI:$AKI" + +CLIENT_CERT_SHA="$(openssl x509 -in "$CLIENT_CERT" -noout -sha256 -fingerprint | tr '[:upper:]' '[:lower:]' | tr -d : | sed 's/.*=//')" + +cat >> config/clients.yml < config/clients.yml && \ +mv omejdn-server/config/clients.yml.orig omejdn-server/config/clients.yml && \ +mv omejdn-server/config/omejdn.yml.orig omejdn-server/config/omejdn.yml && \ +mv omejdn-server/config/scope_mapping.yml.orig omejdn-server/config/scope_mapping.yml && \ +echo "${GR}Restored existing DAPS configuration${NC}" + +# Remove configuration file for testing +rm tests/test_config.txt && \ +echo "${GR}Deleted configuration file for testing${NC}" \ No newline at end of file diff --git a/tests/pom.xml b/tests/pom.xml new file mode 100644 index 0000000..ad0af49 --- /dev/null +++ b/tests/pom.xml @@ -0,0 +1,107 @@ + + + 4.0.0 + de.fraunhofer.aisec + daps-test + 1.0-SNAPSHOT + + 1.4.31 + 5.4.2 + UTF-8 + UTF-8 + + + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + org.assertj + assertj-core + 3.12.2 + test + + + org.junit.jupiter + junit-jupiter-api + ${junit-jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter.version} + test + + + io.jsonwebtoken + jjwt-api + 0.11.2 + + + io.jsonwebtoken + jjwt-impl + 0.11.2 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.2 + runtime + + + org.json + json + 20210307 + + + org.bitbucket.b_c + jose4j + 0.7.8 + + + com.nimbusds + nimbus-jose-jwt + 9.13 + + + org.bouncycastle + bcpkix-jdk15on + 1.69 + + + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin + + + kotlin-maven-plugin + org.jetbrains.kotlin + ${kotlin.version} + + + compile + + compile + + + + test-compile + + test-compile + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.1 + + + + \ No newline at end of file diff --git a/tests/setup.sh b/tests/setup.sh new file mode 100644 index 0000000..da1b2b2 --- /dev/null +++ b/tests/setup.sh @@ -0,0 +1,62 @@ +#!/bin/bash +GR=`tput setaf 2` +NC=`tput sgr0` + +echo "${GR}Setting up the environment for testing the DAPS${NC}" + +# Register four connectors for testing purposes +cd .. && sh scripts/register_connector.sh test1 && \ +sh scripts/register_connector.sh test2 && \ +sh scripts/register_ec_connector.sh ec256 256 && \ +sh scripts/register_ec_connector.sh ec521 521 && \ +echo "${GR}Connectors added successfully${NC}" + +# Backup certs from the server and load testing certs and private key +mv omejdn-server/keys omejdn-server/keys-backup && mkdir omejdn-server/keys && mkdir omejdn-server/keys/omejdn && mkdir omejdn-server/keys/clients && \ +cd keys/omejdn && openssl req -newkey rsa:2048 -new -batch -nodes -x509 -days 3650 -text -keyout omejdn.key -out omejdn.cert && cd ../../ && \ +cp keys/clients/*.cert omejdn-server/keys/clients/ && cp keys/omejdn/omejdn.key omejdn-server/keys/omejdn/ && \ +echo "${GR}Original certs backed up and testing material added successfully${NC}" + +# Setup DAPS configuration and backup original files +cp omejdn-server/config/clients.yml omejdn-server/config/clients.yml.orig && \ +cp config/clients.yml omejdn-server/config/clients.yml && \ +cp omejdn-server/config/omejdn.yml omejdn-server/config/omejdn.yml.orig && \ +cp config/omejdn.yml omejdn-server/config/omejdn.yml && \ +echo "\nissuer: https://localhost:4567" >> omejdn-server/config/omejdn.yml && \ +echo "accept_audience: idsc:IDS_CONNECTORS_ALL" >> omejdn-server/config/omejdn.yml && \ +echo "default_audience: idsc:IDS_CONNECTORS_ALL" >> omejdn-server/config/omejdn.yml && \ +cp omejdn-server/config/scope_mapping.yml omejdn-server/config/scope_mapping.yml.orig && \ +cp config/scope_mapping.yml omejdn-server/config/scope_mapping.yml && \ +echo "${GR}DAPS configuration setup and original files backuped successfully${NC}" + +# Create config file for testing +ISS="iss=$(awk 'NR==18{ print; exit }' config/clients.yml | cut -c 14-)" +AUD="aud=$(awk 'NR==19{ print; exit }' omejdn-server/config/omejdn.yml | cut -c 18-)" +ISS_DAPS="iss_daps=$(awk 'NR==18{ print; exit }' omejdn-server/config/omejdn.yml | cut -c 9-)" +SEC="securityProfile=$(awk 'NR==27{ print; exit }' config/clients.yml | cut -c 12-)" +CONN="referringConnector=$(awk 'NR==29{ print; exit }' config/clients.yml | cut -c 12-)" +TYPE="@type=$(awk 'NR==31{ print; exit }' config/clients.yml | cut -c 12-)" +CONT="@context=$(awk 'NR==33{ print; exit }' config/clients.yml | cut -c 12-)" +SCOPE="scope=$(awk 'NR==22{ print; exit }' config/clients.yml | cut -c 10-)" +TRANS="transportCertsSha256=$(awk 'NR==35{ print; exit }' config/clients.yml | cut -c 12-)" +KEY1="keyPath=../keys/clients/test1.key" +KEY2="keyPath2=../keys/clients/test2.key" +ISS2="iss2=$(awk 'NR==37{ print; exit }' config/clients.yml | cut -c 14-)" +URL="url=http://localhost:4567/" +ISS_256="iss_256=$(awk 'NR==56{ print; exit }' config/clients.yml | cut -c 14-)" +SEC_256="securityProfile_256=$(awk 'NR==65{ print; exit }' config/clients.yml | cut -c 12-)" +CONN_256="referringConnector_256=$(awk 'NR==67{ print; exit }' config/clients.yml | cut -c 12-)" +SCOPE_256="scope_256=$(awk 'NR==60{ print; exit }' config/clients.yml | cut -c 10-)" +TRANS_256="transportCertsSha256_256=$(awk 'NR==73{ print; exit }' config/clients.yml | cut -c 12-)" +KEY3="keyPath3=../keys/clients/ec256.key" +ISS_512="iss_512=$(awk 'NR==75{ print; exit }' config/clients.yml | cut -c 14-)" +SEC_512="securityProfile_512=$(awk 'NR==84{ print; exit }' config/clients.yml | cut -c 12-)" +CONN_512="referringConnector_512=$(awk 'NR==86{ print; exit }' config/clients.yml | cut -c 12-)" +SCOPE_512="scope_512=$(awk 'NR==79{ print; exit }' config/clients.yml | cut -c 10-)" +TRANS_512="transportCertsSha256_512=$(awk 'NR==92{ print; exit }' config/clients.yml | cut -c 12-)" +KEY4="keyPath4=../keys/clients/ec521.key" +EC256="${ISS_256}\n${SEC_256}\n${CONN_256}\n${SCOPE_256}\n${TRANS_256}\n${KEY3}" +EC512="${ISS_512}\n${SEC_512}\n${CONN_512}\n${SCOPE_512}\n${TRANS_512}\n${KEY4}" +echo "${ISS}\n${AUD}\n${ISS_DAPS}\n${SEC}\n${CONN}\n${TYPE}\n${CONT}\n${SCOPE}\n${TRANS}\n${KEY1}\n${KEY2}\n${ISS2}\n${URL}\n${EC256}\n${EC512}" > tests/test_config.txt && \ +echo "${GR}Configuration file for testing contains:${NC}" && \ +cat tests/test_config.txt \ No newline at end of file diff --git a/tests/src/main/kotlin/TokenGenerator.kt b/tests/src/main/kotlin/TokenGenerator.kt new file mode 100644 index 0000000..371baa5 --- /dev/null +++ b/tests/src/main/kotlin/TokenGenerator.kt @@ -0,0 +1,308 @@ +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import java.util.HashMap; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Base64.Decoder; +import java.util.Base64.Encoder; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.security.KeyFactory; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.PrivateKey; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import javax.crypto.spec.SecretKeySpec; +import org.json.JSONObject; +import org.jose4j.jwk.HttpsJwks; +import org.jose4j.keys.resolvers.HttpsJwksVerificationKeyResolver; +import org.jose4j.jwt.consumer.JwtConsumerBuilder; +import org.jose4j.jwt.JwtClaims; +import org.jose4j.keys.HmacKey; +import org.jose4j.jws.JsonWebSignature; +import org.jose4j.jws.AlgorithmIdentifiers; +import org.jose4j.jwt.consumer.JwtConsumer; +import com.nimbusds.jose.jwk.JWKSet; +import java.net.URL; +import java.security.PublicKey; +import java.security.Key; +import java.io.StringWriter; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo + +class TokenGenerator { + + val daps_url : String = File("test_config.txt").readLines().get(12).split("=")[1]; + + fun getPrivKey(pemFile : String, alg : String) : PrivateKey{ + val privateKeyString : String = File(pemFile).readText(Charsets.UTF_8); + val keyString : String = privateKeyString.replace("-----BEGIN PRIVATE KEY-----", "") + .replace(System.lineSeparator(),"") + .replace("-----END PRIVATE KEY-----", ""); + val decoder : Decoder = Base64.getDecoder(); + val decoded : ByteArray = decoder.decode(keyString); + var keyFactory : KeyFactory; + if(alg.equals("ES256") || alg.equals("ES512")){ + keyFactory = KeyFactory.getInstance("EC"); + }else{ + keyFactory = KeyFactory.getInstance("RSA"); + } + val keySpec : PKCS8EncodedKeySpec = PKCS8EncodedKeySpec(decoded); + val privKey : PrivateKey = keyFactory.generatePrivate(keySpec); + return privKey; + } + + fun getToken(iss : String, aud : String, sub : String, context : String, type : String, + iat : Long, nbf : Long, exp : Long, pemFile : String, alg : String): String { + //Get private key from pem file in PKCS8 format + var privKey : PrivateKey = getPrivKey(pemFile, alg); + + + try { + //Set claims for jwt + val claims : HashMap = HashMap(); + claims.put("iss", iss); + claims.put("aud", aud); + claims.put("sub", sub); + claims.put("@context", context); + claims.put("@type", type); + claims.put("iat", iat); + claims.put("exp", exp); + claims.put("nbf", nbf); + + //Use library to build jwt + var jwt : String; + when(alg){ + "RS256" -> jwt = Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.RS256, privKey).compact(); + "RS512" -> jwt = Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.RS512, privKey).compact(); + "PS256" -> jwt = Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.PS256, privKey).compact(); + "PS512" -> jwt = Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.PS512, privKey).compact(); + "ES256" -> jwt = Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.ES256, privKey).compact(); + "ES512" -> jwt = Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.ES512, privKey).compact(); + else -> jwt = ""; + + } + println("JWT: "+jwt); + + //Build request string + val req : String = "grant_type=client_credentials&"+ + "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&"+ + "client_assertion="+jwt+"&"+ + "scope=idsc:IDS_CONNECTOR_ATTRIBUTES_ALL"; + println("\nRequesting access_token from DAPS\n"); + + //Send request to daps server and get response + val client : HttpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); + val request : HttpRequest = HttpRequest.newBuilder() + .uri(URI.create(daps_url+"token")) + .POST(HttpRequest.BodyPublishers.ofString(req)) + .build(); + val response : HttpResponse = client.send(request, HttpResponse.BodyHandlers.ofString()); + println("Response: "+response.body()) + println("Code: "+response.statusCode()+"\n") + return response.body(); + } + catch(ex : Exception) { + println(ex.message); + } + + return "Error"; + } + + fun getTokenAlgNone(iss : String, aud : String, sub : String, context : String, type : String, + iat : Long, nbf : Long, exp : Long): String { + try { + //Set claims for jwt + val claims : HashMap = HashMap(); + claims.put("iss", iss); + claims.put("aud", aud); + claims.put("sub", sub); + claims.put("@context", context); + claims.put("@type", type); + claims.put("iat", iat); + claims.put("exp", exp); + claims.put("nbf", nbf); + + //Use library to build jwt + val jwt : String = Jwts.builder().setClaims(claims).compact(); + println("JWT: "+jwt); + + //Build request string + val req : String = "grant_type=client_credentials&"+ + "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&"+ + "client_assertion="+jwt+"&"+ + "scope=idsc:IDS_CONNECTOR_ATTRIBUTES_ALL"; + println("\nRequesting access_token from DAPS\n"); + + //Send request to daps server and get response + val client : HttpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); + val request : HttpRequest = HttpRequest.newBuilder() + .uri(URI.create(daps_url+"token")) + .POST(HttpRequest.BodyPublishers.ofString(req)) + .build(); + val response : HttpResponse = client.send(request, HttpResponse.BodyHandlers.ofString()); + println("Response: "+response.body()) + println("Code: "+response.statusCode()+"\n") + return response.body(); + } + catch(ex : Exception) { + println(ex.message); + } + + return "Error"; + } + +fun getTokenNaN(): String { + try { + //Create payload + val header : String = "eyJhbGciOiJub25lIn0"; // alg = none + // {"value":NaN} + val bytes : ByteArray= byteArrayOf(123, 34, 118, 97, 108, 117, 101, 34, 58, 78, 97, 78, 125); + val encoder : Encoder = Base64.getEncoder().withoutPadding(); + val body : String = encoder.encodeToString(bytes); + val jwt = header+"."+body; + println("JWT: "+jwt); + + //Build request string + val req : String = "grant_type=client_credentials&"+ + "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&"+ + "client_assertion="+jwt+"&"+ + "scope=idsc:IDS_CONNECTOR_ATTRIBUTES_ALL"; + println("\nRequesting access_token from DAPS\n"); + + //Send request to daps server and get response + val client : HttpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); + val request : HttpRequest = HttpRequest.newBuilder() + .uri(URI.create(daps_url+"token")) + .POST(HttpRequest.BodyPublishers.ofString(req)) + .build(); + val response : HttpResponse = client.send(request, HttpResponse.BodyHandlers.ofString()); + println("Response: "+response.body()) + println("Code: "+response.statusCode()+"\n") + return response.body(); + } + catch(ex : Exception) { + println(ex.message); + } + + return "Error"; + } + + fun getTokenNested(): String { + try { + // Create big nested jwt of form '{"a":' * 1000 + val header : String = "eyJhbGciOiJub25lIn0"; // alg = none + val bytes : ByteArray = byteArrayOf(123, 34, 97, 34, 58); + // {"a": + var mil : ByteArray = byteArrayOf(); + for(i in 1..1000){ + mil += bytes; + } + val encoder : Encoder = Base64.getEncoder().withoutPadding(); + val body : String = encoder.encodeToString(mil); + + //Use library to build jwt + val jwt = header+"."+body; + println("JWT: "+jwt); + + //Build request string + val req : String = "grant_type=client_credentials&"+ + "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&"+ + "client_assertion="+jwt+"&"+ + "scope=idsc:IDS_CONNECTOR_ATTRIBUTES_ALL"; + println("\nRequesting access_token from DAPS\n"); + + //Send request to daps server and get response + val client : HttpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); + val request : HttpRequest = HttpRequest.newBuilder() + .uri(URI.create(daps_url+"token")) + .POST(HttpRequest.BodyPublishers.ofString(req)) + .build(); + val response : HttpResponse = client.send(request, HttpResponse.BodyHandlers.ofString()); + println("Response: "+response.body()) + println("Code: "+response.statusCode()+"\n") + return response.body(); + } + catch(ex : Exception) { + println(ex.message); + } + + return "Error"; + } + + fun verifyTokenRequest(responseBody : String, iss : String, aud : String, sub : String, + context : String, type : String, secProf : String, + connector : String, scope : String, transCert : String): Boolean{ + + //Retrieve access_token from the http response + val json : JSONObject = JSONObject(responseBody); + val access_token : String = json.getString("access_token"); + println("Verifying access_token\n"); + + // Get JWKS from well-known keys stored in the DAPS + val httpsJwks : HttpsJwks = HttpsJwks(daps_url+"jwks.json"); + + // Create new JWKS key resolver, selects JWK based on key ID in JWT header + val jwksKeyResolver : HttpsJwksVerificationKeyResolver = HttpsJwksVerificationKeyResolver(httpsJwks); + + // Create validation requirements according to the DAPS specification + val jwtConsumer = JwtConsumerBuilder() + .setExpectedAudience(true, aud) + .setExpectedIssuer(iss) + .setAllowedClockSkewInSeconds(30) // If machines are not synchronized it could lead to errors so we allow 30 seconds of difference + .setRequireNotBefore() + .setRequireIssuedAt() + .setRequireJwtId() + .setRequireExpirationTime() + .setRequireSubject() + .setVerificationKeyResolver(jwksKeyResolver) // Get decryption key from JWKS + .build(); + + try { + val claims : JwtClaims = jwtConsumer.processToClaims(access_token); + //Process claims accordingly + if (!claims.getClaimValue("@type").equals(type)){ + throw Exception("\nFailed @type verification\nGot @type: " + +claims.getClaimValue("@type").toString()+"\n"); + } + if (!claims.getClaimValue("@context").equals(context)){ + throw Exception("\nFailed @context verification\nGot @context: " + +claims.getClaimValue("@context").toString()+"\n"); + } + if (!claims.getSubject().equals(sub)){ + throw Exception("\nFailed subject verification\nGot subject: " + +claims.getSubject()+"\n"); + } + if (!claims.getClaimValue("securityProfile").equals(secProf)){ + throw Exception("\nFailed securityProfile verification\nGot securityProfile: " + +claims.getClaimValue("securityProfile").toString()+"\n"); + } + if (!claims.getClaimValue("referringConnector").equals(connector)){ + throw Exception("\nFailed referringConnector verification\nGot referringConnector: " + +claims.getClaimValue("referringConnector").toString()+"\n"); + } + if (!claims.getClaimValue("transportCertsSha256").equals(transCert)){ + throw Exception("\nFailed transportCertsSha256 verification\nGot transportCertsSha256: " + +claims.getClaimValue("transportCertsSha256").toString()+"\n"); + } + if (!claims.getClaimValue("scope").equals(scope)){ + throw Exception("\nFailed scope verification\nGot scope: " + +claims.getClaimValue("scope").toString()+"\n"); + } + + println("Verification successful\n"); + return true; + } catch (ex : Exception) { + println(ex.message); + } + + return false; + } +} \ No newline at end of file diff --git a/tests/src/test/kotlin/TokenGeneratorTest.kt b/tests/src/test/kotlin/TokenGeneratorTest.kt new file mode 100644 index 0000000..5572162 --- /dev/null +++ b/tests/src/test/kotlin/TokenGeneratorTest.kt @@ -0,0 +1,510 @@ +import org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import java.util.Date; +import java.io.File; +import kotlin.system.exitProcess; +import java.lang.System; + +@TestInstance(Lifecycle.PER_CLASS) +class TokenGeneratorTest { + + private lateinit var generator: TokenGenerator + private val prop : List = File("test_config.txt").readLines() + private var keyPath : String = ""; + private var keyPath2 : String = ""; + private var aud : String = ""; + private var iss : String = ""; + private var sub : String = ""; + private var now : Long = 0; + private var iat : Long = 0; + private var nbf : Long = 0; + private var exp : Long = 0; + private var iss2 : String = ""; + private var aud_daps : String = ""; + private var iss_daps : String = ""; + private var securityProfile : String = ""; + private var referringConnector : String = ""; + private var type : String = ""; + private var context : String = ""; + private var scope : String = ""; + private var transCert : String = ""; + + @BeforeAll + fun setup(){ + // Get required properties for tests to work + iss = prop.get(0).split("=")[1]; + if(iss.isEmpty()){ + println("Issuer argument is empty"); + System.exit(-1); + } + sub = iss; + + aud = prop.get(1).split("=")[1]; + if(aud.isEmpty()){ + println("Audience argument is empty"); + System.exit(-1); + } + aud_daps = aud; + + iss_daps = prop.get(2).split("=")[1]; + if(iss_daps.isEmpty()){ + println("Audience2 argument is empty"); + System.exit(-1); + } + + securityProfile = prop.get(3).split("=")[1]; + if(securityProfile.isEmpty()){ + println("securityProfile argument is empty"); + System.exit(-1); + } + + referringConnector = prop.get(4).split("=")[1]; + if(referringConnector.isEmpty()){ + println("referringConnector argument is empty"); + System.exit(-1); + } + + context = prop.get(6).split("=")[1]; + if(context.isEmpty()){ + println("Context argument is empty"); + System.exit(-1); + } + + scope = prop.get(7).split("=")[1]; + if(scope.isEmpty()){ + println("Scope argument is empty"); + System.exit(-1); + } + + transCert = prop.get(8).split("=")[1]; + if(transCert.isEmpty()){ + println("transportCertsSha256 argument is empty"); + System.exit(-1); + } + + iss2 = prop.get(11).split("=")[1]; + if(iss2.isEmpty()){ + println("Issuer2 argument is empty"); + System.exit(-1); + } + + // Get path for keys to use for jwts + keyPath = prop.get(9).split("=")[1]; + if(keyPath.isEmpty()){ + println("Key argument is empty"); + System.exit(-1); + } + + keyPath2 = prop.get(10).split("=")[1]; + if(keyPath2.isEmpty()){ + println("Key2 argument is empty"); + System.exit(-1); + } + + } + + @BeforeEach + fun configureSystemUnderTest() { + + type = prop.get(5).split("=")[1]; + if(type.isEmpty()){ + println("Type argument is empty"); + System.exit(-1); + } + + generator = TokenGenerator(); + now = Date().getTime() / 1000; // Divide by 1000 to get seconds + iat = now; + nbf = now; + exp = now + 3600; + } + + private fun print_test(text : String){ + println("\u001B[44m\u001B[30mTest Case: "+text+"\u001B[0m\n"); + } + + @Test + @DisplayName("Requests and verifies an access_token") + /* Base case, this is the norm and expected on how the server correctly gives + back an access_token that the user can verify and utilize */ + fun getAccessTokenAndVerify() { + print_test("Requests and verifies an access_token"); + + // Try to get access_token with the given arguments + val response : String = generator.getToken(iss, aud, sub, context, type, iat, nbf, exp, keyPath, "RS256"); + assertTrue("access_token" in response); + + // Variables to verify the expected token + type = "ids:DatPayload"; + assertTrue(generator.verifyTokenRequest(response, iss_daps, aud_daps, sub, context, type, + securityProfile, referringConnector, scope, transCert)); + } + + @Test + @DisplayName("Requests and verifies an access_token with the RSA512 algorithm") + /* Base case, this is the norm and expected on how the server correctly gives + back an access_token that the user can verify and utilize */ + fun getAccessTokenAndVerifyRSA512() { + print_test("Requests and verifies an access_token with the RSA512 algorithm"); + + // Try to get access_token with the given arguments + val response : String = generator.getToken(iss, aud, sub, context, type, iat, nbf, exp, keyPath, "RS512"); + assertTrue("access_token" in response); + + // Variables to verify the expected token + type = "ids:DatPayload"; + assertTrue(generator.verifyTokenRequest(response, iss_daps, aud_daps, sub, context, type, + securityProfile, referringConnector, scope, transCert)); + } + + @Test + @DisplayName("Requests and verifies an access_token with the ES256 algorithm") + /* Base case, this is the norm and expected on how the server correctly gives + back an access_token that the user can verify and utilize */ + fun getAccessTokenAndVerifyES256() { + print_test("Requests and verifies an access_token with the ES256 algorithm"); + //Variables to feed to the token + val iss : String = prop.get(13).split("=")[1]; + if(iss.isEmpty()){ + println("Issuer3 argument is empty"); + System.exit(-1); + } + val sub : String = iss; + val keyPathEC256 : String = prop.get(18).split("=")[1]; + if(keyPathEC256.isEmpty()){ + println("Key3 argument is empty"); + System.exit(-1); + } + + val response : String = generator.getToken(iss, aud, sub, context, type, iat, nbf, exp, keyPathEC256, "ES256"); + assertTrue("access_token" in response); + + //Variables to verify the expected token + type = "ids:DatPayload"; + val securityProfile : String = prop.get(14).split("=")[1]; + if(securityProfile.isEmpty()){ + println("securityProfile argument is empty"); + System.exit(-1); + } + val referringConnector : String = prop.get(15).split("=")[1]; + if(referringConnector.isEmpty()){ + println("referringConnector argument is empty"); + System.exit(-1); + } + + val scope : String = prop.get(16).split("=")[1]; + if(scope.isEmpty()){ + println("Scope argument is empty"); + System.exit(-1); + } + + val transCert : String = prop.get(17).split("=")[1]; + if(transCert.isEmpty()){ + println("transportCertsSha256 argument is empty"); + System.exit(-1); + } + assertTrue(generator.verifyTokenRequest(response, iss_daps, aud_daps, sub, context, type, + securityProfile, referringConnector, scope, transCert)); + } + + @Test + @DisplayName("Requests and verifies an access_token with the ES512 algorithm") + /* Base case, this is the norm and expected on how the server correctly gives + back an access_token that the user can verify and utilize */ + fun getAccessTokenAndVerifyES512() { + print_test("Requests and verifies an access_token with the ES512 algorithm"); + //Variables to feed to the token + val iss : String = prop.get(19).split("=")[1]; + if(iss.isEmpty()){ + println("Issuer4 argument is empty"); + System.exit(-1); + } + val sub : String = iss; + val keyPathEC512 : String = prop.get(24).split("=")[1]; + if(keyPathEC512.isEmpty()){ + println("Key4 argument is empty"); + System.exit(-1); + } + + val response : String = generator.getToken(iss, aud, sub, context, type, iat, nbf, exp, keyPathEC512, "ES512"); + assertTrue("access_token" in response); + + //Variables to verify the expected token + type = "ids:DatPayload"; + val securityProfile : String = prop.get(20).split("=")[1]; + if(securityProfile.isEmpty()){ + println("securityProfile argument is empty"); + System.exit(-1); + } + val referringConnector : String = prop.get(21).split("=")[1]; + if(referringConnector.isEmpty()){ + println("referringConnector argument is empty"); + System.exit(-1); + } + + val scope : String = prop.get(22).split("=")[1]; + if(scope.isEmpty()){ + println("Scope argument is empty"); + System.exit(-1); + } + + val transCert : String = prop.get(23).split("=")[1]; + if(transCert.isEmpty()){ + println("transportCertsSha256 argument is empty"); + System.exit(-1); + } + assertTrue(generator.verifyTokenRequest(response, iss_daps, aud_daps, sub, context, type, + securityProfile, referringConnector, scope, transCert)); + } + + @Test + @DisplayName("Requests and verifies an access_token with the PS256 algorithm") + /* This test should fail because in the DAPS PS256 is not a supported algorithm */ + fun getAccessTokenAndVerifyPS256() { + print_test("Requests and verifies an access_token with the PS256 algorithm"); + + // Try to get access_token with the given arguments + val response : String = generator.getToken(iss, aud, sub, context, type, iat, nbf, exp, keyPath, "PS256"); + assertFalse("access_token" in response); + } + + @Test + @DisplayName("Requests and verifies an access_token with the PS512 algorithm") + /* This test should fail because in the DAPS PS512 is not a supported algorithm */ + fun getAccessTokenAndVerifyPS512() { + print_test("Requests and verifies an access_token with the PS512 algorithm"); + + // Try to get access_token with the given arguments + val response : String = generator.getToken(iss, aud, sub, context, type, iat, nbf, exp, keyPath, "PS512"); + assertFalse("access_token" in response); + } + + @Test + @DisplayName("NBF set to current time, IAT one hour before and EXP one hour in the future") + /* This test successfully creates an access_token. In theory this should not affect the security + of the token because this behaviour can't really be abused. */ + fun futureNBFToken() { + print_test("NBF set to one hour in the future after IAT but before EXP"); + + // Variables to feed to the token + val now : Long = Date().getTime() / 1000; // Divide by 1000 to get seconds + val iat : Long = now - 3600; + val nbf : Long = now; + val exp : Long = now + 3600; + val response : String = generator.getToken(iss, aud, sub, context, type, iat, nbf, exp, keyPath, "RS256"); + assertTrue("access_token" in response); + + // Variables to verify the expected token + type = "ids:DatPayload"; + assertTrue(generator.verifyTokenRequest(response, iss_daps, aud_daps, sub, context, type, + securityProfile, referringConnector, scope, transCert)); + } + + @Test + @DisplayName("IAT set to current time, NBF set one hour in the past, EXP one hour in the future") + /* This test successfully creates an access_token. In theory this should not affect the security + of the token because this behaviour can't really be abused. */ + fun pastNBFToken() { + print_test("IAT set to current time, NBF set one hour in the past, EXP one hour in the future"); + + // Variables to feed to the token + val now : Long = Date().getTime() / 1000; // Divide by 1000 to get seconds + val iat : Long = now; + val nbf : Long = now - 3600; + val exp : Long = now + 3600; + val response : String = generator.getToken(iss, aud, sub, context, type, iat, nbf, exp, keyPath, "RS256"); + assertTrue("access_token" in response); + + // Variables to verify the expected token + type = "ids:DatPayload"; + assertTrue(generator.verifyTokenRequest(response, iss_daps, aud_daps, sub, context, type, + securityProfile, referringConnector, scope, transCert)); + } + + @Test + @DisplayName("Wrong context") + /* The permission for an access_token does not depend on the context property, thus a + DAT token is created with the default context if the authentication succeeds. */ + fun wrongContext() { + print_test("Wrong context"); + + // Variables to feed to the token + var context : String = "invalid_context"; + val response : String = generator.getToken(iss, aud, sub, context, type, iat, nbf, exp, keyPath, "RS256"); + assertTrue("access_token" in response); + + // Variables to verify the expected token + type = "ids:DatPayload"; + context = "https://w3id.org/idsa/contexts/context.jsonld"; + assertTrue(generator.verifyTokenRequest(response, iss_daps, aud_daps, sub, context, type, + securityProfile, referringConnector, scope, transCert)); + } + + @Test + @DisplayName("Wrong type") + /* The permission for an access_token does not depend on the type property, thus a + DAT token is created with the default context if the authentication succeeds. */ + fun wrongType() { + print_test("Wrong type"); + + // Variables to feed to the token + type = "invalid_type"; + val response : String = generator.getToken(iss, aud, sub, context, type, iat, nbf, exp, keyPath, "RS256"); + assertTrue("access_token" in response); + + // Variables to verify the expected token + type = "ids:DatPayload"; + assertTrue(generator.verifyTokenRequest(response, iss_daps, aud_daps, sub, context, type, + securityProfile, referringConnector, scope, transCert)); + } + + @Test + @DisplayName("Wrong subject") + /* This test should fail. Would be a big vulnerability if a valid access_token + would be granted for this JWT request token*/ + fun wrongSubject() { + print_test("Wrong subject"); + + // Variables to feed to the token + val sub : String = "invalid_subject"; + + val response : String = generator.getToken(iss, aud, sub, context, type, iat, nbf, exp, keyPath, "RS256"); + assertFalse("access_token" in response); + } + + @Test + @DisplayName("Wrong audience") + /* This test should fail. Would be a big vulnerability if a valid access_token + would be granted for this JWT request token*/ + fun wrongAudience() { + print_test("Wrong audience"); + + // Variables to feed to the token + val aud : String = "invalid_audience"; + + val response : String = generator.getToken(iss, aud, sub, context, type, iat, nbf, exp, keyPath, "RS256"); + assertFalse("access_token" in response); + } + + @Test + @DisplayName("Wrong issuer") + /* This test should fail. Would be a big vulnerability if a valid access_token + would be granted for this JWT request token*/ + fun wrongIssuer() { + print_test("Wrong issuer"); + // Variables to feed to the token + val iss : String = "invalid_issuer"; + + val response : String = generator.getToken(iss, aud, sub, context, type, iat, nbf, exp, keyPath, "RS256"); + assertFalse("access_token" in response); + } + + @Test + @DisplayName("'alg = none' attack") + /* https://www.chosenplaintext.ca/2015/03/31/jwt-algorithm-confusion.html This test should fail. + The code in Client.extract_jwt_cid is responsible for this as it always checks + for the algorithm with which the JWT has been signed and only allows the algorithms RS256, + RS512, ES256a and ES512. This prevents the "alg = none" attack. Changing to an HMAC algorithm + would also result in the same behaviour as HMAC algorithms are not acccepted by the DAPS. */ + fun getAlgNone() { + print_test("Attempts to get an access_token with no valid signature algorithm"); + + // Try to get access_token with the given arguments + val response : String = generator.getTokenAlgNone(iss, aud, sub, context, type, iat, nbf, exp); + assertFalse("access_token" in response); + } + + @Test + @DisplayName("JSON injection attack - expiration change") + /* https://www.acunetix.com/blog/web-security-zone/what-are-json-injections/ Input is sanitized. + The only interesting field to influence is the "sub" field. All the other fields cannot influence the + access_token that is created from the server because the DAPS server recalculates these fields or fills + them in with different information. There is a check for the "iss" and "sub" to be equal in the server, thus + in this case we set both to the same string that tries to perform the injection. %22,%22exp%22:1893456000 + is the appended string to the "sub", this attempts to create a new field with the following appended form: '"exp":1893456000' + being 1893456000 an epoch time that we want to set for the exp in the access_token that we might receive. + ERROR: Client cid%22,%22exp%22:1893456000 does not exist + is the ERROR displayed in the server, indicating that it could not find the client as it is correctly sanitized + and there are no parsing vulnerabilities*/ + fun getJSONInjectionAttackExpiration() { + print_test("Attempts to get an access_token with an specific expiration date"); //1893456000 - Tuesday, 1 January 2030 0:00:00 + + // Variables to feed to the token + val subInjExp : String = sub+"%22,%22exp%22:1893456000"; + val issInjExp: String = subInjExp; + + val response : String = generator.getToken(issInjExp, aud, subInjExp, context, type, iat, nbf, exp, keyPath, "RS256"); + assertFalse("access_token" in response); + } + + @Test + @DisplayName("JSON injection attack - audience change") + /* %22,%22aud%22:%22desiredAudience%22 is the appended string to the "sub", this attempts to create a new field with the following appended form: '"aud":"desiredAudience"' + being desiredAudience an audience that we might want access to but we might not be able to access it. + ERROR: Client cid%22,%22aud%22:%22desiredAudience%22 does not exist + is the ERROR displayed in the server, indicating that it could not find the client as it is correctly sanitized + and there are no parsing vulnerabilities. */ + fun getJSONInjectionAttackAudience() { + print_test("Attempts to get an access_token with an specific user-controlled audience"); + + // Variables to feed to the token + val subInjAud : String = sub+"%22,%22aud%22:%22desiredAudience%22"; + val issInjAud : String = subInjAud; + + val response : String = generator.getToken(issInjAud, aud, subInjAud, context, type, iat, nbf, exp, keyPath, "RS256"); + assertFalse("access_token" in response); + } + + @Test + @DisplayName("Different subject for the first signing key") + /* This test should fail because the key used to sign the JWT does not match the signing key + for the given subject in the JWT. The DAPS server chooses the key based on the subject of the + requesting jwt. In this case, no (malicious) client can create JWTs for other subjects */ + fun getDiffSubKey1() { + print_test("Attempts to get an access_token with another subject for the first client's signing key"); + + // Try to get access_token with the given arguments + val response : String = generator.getToken(iss2, aud, iss2, context, type, iat, nbf, exp, keyPath, "RS256"); + assertFalse("access_token" in response); + } + + @Test + @DisplayName("Different subject for the second signing key") + /* Check the description from the test before*/ + fun getDiffSubKey2() { + print_test("Attempts to get an access_token with another subject for the second client's signing key"); + + // Try to get access_token with the given arguments + val response : String = generator.getToken(iss, aud, sub, context, type, iat, nbf, exp, keyPath2, "RS256"); + assertFalse("access_token" in response); + } + + @Test + @DisplayName("NaN") + /* Checks if NaN is accepted. It should cause an error when processing NaN */ + fun getNaN() { + print_test("Requests and verifies an access_token but the algorithm creates a jwt with a NaN field"); + + // Try to get access_token with the given arguments + val response : String = generator.getTokenNaN(); + assertFalse("access_token" in response); + } + + @Test + @DisplayName("Nested JSON") + /* Checks if nested JSON accepted. It should cause an error when processing the JSON because the max_nesting is 100 */ + fun getNested() { + print_test("Requests and verifies an access_token but the algorithm creates a jwt with a deeply nested JSON"); + + // Try to get access_token with the given arguments + val response : String = generator.getTokenNested(); + assertFalse("access_token" in response); + } + +} \ No newline at end of file