Skip to content

Commit

Permalink
Add support for RSA-OAEP-256 key management algorithm
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin committed May 16, 2022
1 parent bb92434 commit 9054773
Show file tree
Hide file tree
Showing 10 changed files with 326 additions and 23 deletions.
9 changes: 8 additions & 1 deletion spec/src/main/asciidoc/configuration.asciidoc
Expand Up @@ -381,7 +381,7 @@ Note that two types of keys are required to implement a JWE encryption scheme:
* Content encryption key - typically a generated secret key which is used to encrypt a plaintext such as a JSON representation of the token claims.
* Key management key - public RSA key which is used to encrypt a content encryption key. `mp.jwt.decrypt.key.location` must point to a private RSA key matching this key.

Key management key algorithm which must be supported is https://tools.ietf.org/html/rfc7518#section-4.3[RSA-OAEP] (RSAES using Optimal Asymmetric Encryption Padding) with a key length 2048 bits or higher.
Key management key algorithms which must be supported are https://tools.ietf.org/html/rfc7518#section-4.3[RSA-OAEP] (RSAES using Optimal Asymmetric Encryption Padding and SHA-1) and https://tools.ietf.org/html/rfc7518#section-4.3[RSA-OAEP-256] (RSAES using Optimal Asymmetric Encryption Padding and SHA-256) with a public RSA key length 2048 bits or higher.

Content encryption algorithm which must be supported is https://tools.ietf.org/html/rfc7518#section-5.3[A256GCM] (AES in Galois/Counter Mode (GCM)).

Expand Down Expand Up @@ -409,6 +409,13 @@ The `mp.jwt.decrypt.key.location` config property allows for an external or inte
of Private Decryption Key to be specified. The value may be a relative path or a URL.
Please see <<verification-publickey-location, mp.jwt.publickey.location>> for all the information about the supported locations and <<encrypted-jwt-tokens, Encrypted JWT claims and nested tokens>> section for the additional recommendations.

#### `mp.jwt.decrypt.key.algorithm`

The `mp.jwt.decrypt.key.algorithm` configuration property allows for specifying which key management key algorithm
is supported by the MP JWT endpoint. Algorithms which must be supported are either `RSA-OAEP` or `RSA-OAEP-256`. Default value is `RSA-OAEP` but `RSA-OAEP-256` will become a default value in one of the next major releases.

Support for the other key management key algorithm such as `RSA-OAEP-384`, `RSA-OAEP-512` and others is optional.

[[claims-verification]]
## Verification of JWT token claims

Expand Down
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2016-2017 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* You may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.eclipse.microprofile.jwt.tck.util;

public enum KeyManagementAlgorithm {
/**
* RSAES using Optimal Asymmetric Encryption Padding with SHA-1
*/
RSA_OAEP,
/**
* RSAES using Optimal Asymmetric Encryption Padding with SHA-256
*/
RSA_OAEP_256;

public String getAlgorithm() {
return name().replace("_", "-");
}
}
Expand Up @@ -24,7 +24,7 @@
* loading the META-INF/MPJWTTESTVERSION resource from the test war and converting it to the MpJwtTestVersion value.
*/
public enum MpJwtTestVersion {
MPJWT_V_1_0, MPJWT_V_1_1, MPJWT_V_1_2;
MPJWT_V_1_0, MPJWT_V_1_1, MPJWT_V_1_2, MPJWT_V_2_1;

public static final String VERSION_LOCATION = "META-INF/MPJWTTESTVERSION";
public static final String MANIFEST_NAME = "MPJWTTESTVERSION";
Expand Down
Expand Up @@ -393,6 +393,32 @@ public static String encryptClaims(PublicKey pk, String kid, String jsonResName)
*/
public static String encryptClaims(PublicKey pk, String kid, String jsonResName, Set<InvalidClaims> invalidClaims,
Map<String, Long> timeClaims) throws Exception {
return encryptClaims(pk, null, kid, jsonResName, invalidClaims, timeClaims);
}

/**
* Utility method to generate a JWT string from a JSON resource file that is encrypted by the public key, possibly
* with invalid fields.
*
* @param pk
* - the public key to encrypt the token with
* @param keyAlgorithm
* - the key encryption algorithm
* @param kid
* - the kid header to assign to the token
* @param jsonResName
* - name of test resources file
* @param invalidClaims
* - the set of claims that should be added with invalid values to test failure modes
* @param timeClaims
* - used to return the exp, iat, auth_time claims
* @return the JWT string
* @throws Exception
* on parse failure
*/
public static String encryptClaims(PublicKey pk, KeyManagementAlgorithm keyAlgorithm, String kid,
String jsonResName, Set<InvalidClaims> invalidClaims,
Map<String, Long> timeClaims) throws Exception {
if (invalidClaims == null) {
invalidClaims = Collections.emptySet();
}
Expand All @@ -409,7 +435,7 @@ public static String encryptClaims(PublicKey pk, String kid, String jsonResName,
key = pk;
}

return encryptString(key, kid, claims.toJson(), false);
return encryptString(key, keyAlgorithm, kid, claims.toJson(), false);
}

/**
Expand Down Expand Up @@ -525,11 +551,47 @@ public static String signEncryptClaims(PrivateKey signingKey,
String jsonResName,
boolean setContentType) throws Exception {

return signEncryptClaims(signingKey, signingKid, encryptionKey, null, encryptionKid, jsonResName,
setContentType);
}

/**
* Utility method to generate a JWT string from a JSON resource file by signing it first with the private key using
* RS256 algorithm and encrypting next with the public key with an option to skip setting a content-type 'cty'
* parameter.
*
* @param signingKey
* - the private key to sign the token with
* @param signingKid
* - the signing key identifier
* @param encryptionKey
* - the public key to encrypt the token with
* @param keyEncryptionAlgorithm
* - the key encryption algorithm
* @param encryptionKid
* - the encryption key identifier
* @param jsonResName
* - name of test resources file
* @param setContentType
* - set a content-type 'cty' parameter if true
* @return the JWT string
* @throws Exception
* on parse failure
*/
public static String signEncryptClaims(PrivateKey signingKey,
String signingKid,
PublicKey encryptionKey,
KeyManagementAlgorithm keyAlgorithm,
String encryptionKid,
String jsonResName,
boolean setContentType) throws Exception {

String nestedJwt = signClaims(signingKey, signingKid, jsonResName, null, null);
return encryptString(encryptionKey, encryptionKid, nestedJwt, setContentType);
return encryptString(encryptionKey, keyAlgorithm, encryptionKid, nestedJwt, setContentType);
}

private static String encryptString(Key key, String kid, String plainText, boolean setContentType)
private static String encryptString(Key key, KeyManagementAlgorithm keyAlgorithm, String kid, String plainText,
boolean setContentType)
throws Exception {

JsonWebEncryption jwe = new JsonWebEncryption();
Expand All @@ -543,10 +605,14 @@ private static String encryptString(Key key, String kid, String plainText, boole
}
jwe.setEncryptionMethodHeaderParameter("A256GCM");

if (key instanceof SecretKey) {
jwe.setAlgorithmHeaderValue("A128KW");
if (keyAlgorithm != null) {
jwe.setAlgorithmHeaderValue(keyAlgorithm.getAlgorithm());
} else {
jwe.setAlgorithmHeaderValue("RSA-OAEP");
if (key instanceof SecretKey) {
jwe.setAlgorithmHeaderValue("A128KW");
} else {
jwe.setAlgorithmHeaderValue("RSA-OAEP");
}
}
jwe.setKey(key);
return jwe.getCompactSerialization();
Expand Down
@@ -0,0 +1,127 @@
/*
* Copyright (c) 2020 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* You may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.eclipse.microprofile.jwt.tck.container.jaxrs.jwe;

import static jakarta.ws.rs.core.MediaType.TEXT_PLAIN;
import static org.eclipse.microprofile.jwt.tck.TCKConstants.TEST_GROUP_JAXRS;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.PrivateKey;
import java.security.PublicKey;

import org.eclipse.microprofile.jwt.tck.container.jaxrs.RolesEndpoint;
import org.eclipse.microprofile.jwt.tck.container.jaxrs.TCKApplication;
import org.eclipse.microprofile.jwt.tck.util.KeyManagementAlgorithm;
import org.eclipse.microprofile.jwt.tck.util.MpJwtTestVersion;
import org.eclipse.microprofile.jwt.tck.util.TokenUtils;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.container.test.api.RunAsClient;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.arquillian.testng.Arquillian;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.testng.Assert;
import org.testng.Reporter;
import org.testng.annotations.Test;

import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;

/**
* Tests of the MP-JWT auth method authorization behavior as expected by the MP-JWT RBAC 1.0 spec
*/
public class RolesAllowedSignEncryptRsaOaep256Test extends Arquillian {

/**
* The base URL for the container under test
*/
@ArquillianResource
private URL baseURL;

/**
* Create a CDI aware base web application archive
*
* @return the base base web application archive
* @throws IOException
* - on resource failure
*/
@Deployment(testable = true)
public static WebArchive createDeployment() throws IOException {
URL config = RolesAllowedSignEncryptRsaOaep256Test.class
.getResource("/META-INF/microprofile-config-verify-decrypt-rsa-oaep-256.properties");
URL verifyKey = RolesAllowedSignEncryptRsaOaep256Test.class.getResource("/publicKey4k.pem");
URL decryptKey = RolesAllowedSignEncryptRsaOaep256Test.class.getResource("/privateKey.pem");
WebArchive webArchive = ShrinkWrap
.create(WebArchive.class, "RolesAllowedSignEncryptRsaOaep256Test.war")
.addAsManifestResource(new StringAsset(MpJwtTestVersion.MPJWT_V_2_1.name()),
MpJwtTestVersion.MANIFEST_NAME)
.addAsResource(decryptKey, "/privateKey.pem")
.addAsResource(verifyKey, "/publicKey4k.pem")
.addClass(RolesEndpoint.class)
.addClass(TCKApplication.class)
.addAsWebInfResource("beans.xml", "beans.xml")
.addAsManifestResource(config, "microprofile-config.properties");
return webArchive;
}

@RunAsClient
@Test(groups = TEST_GROUP_JAXRS, description = "Validate a request with RSA-OAEP-256 encrypted token succeeds")
public void callEchoRsaOaep256() throws Exception {
Reporter.log("callEcho with RSA-OAEP-256 encrypted token, expect HTTP_OK");

PrivateKey signingKey = TokenUtils.readPrivateKey("/privateKey4k.pem");
PublicKey encryptionKey = TokenUtils.readPublicKey("/publicKey.pem");
String token =
TokenUtils.signEncryptClaims(signingKey, null, encryptionKey, KeyManagementAlgorithm.RSA_OAEP_256, null,
"/Token1.json", true);

String uri = baseURL.toExternalForm() + "endp/echo";
WebTarget echoEndpointTarget = ClientBuilder.newClient()
.target(uri)
.queryParam("input", "hello");
Response response =
echoEndpointTarget.request(TEXT_PLAIN).header(HttpHeaders.AUTHORIZATION, "Bearer " + token).get();
Assert.assertEquals(response.getStatus(), HttpURLConnection.HTTP_OK);
String reply = response.readEntity(String.class);
// Must return hello, user={token upn claim}
Assert.assertEquals(reply, "hello, user=jdoe@example.com");
}

@RunAsClient
@Test(groups = TEST_GROUP_JAXRS, description = "Validate a request with RSA-OAEP encrypted token fails with HTTP_UNAUTHORIZED")
public void callEchoRsaOaep() throws Exception {
Reporter.log("callEcho with RSA-OAEP encrypted token, expect HTTP_UNAUTHORIZED");

String token = TokenUtils.signEncryptClaims("/Token1.json");
String uri = baseURL.toExternalForm() + "endp/echo";
WebTarget echoEndpointTarget = ClientBuilder.newClient()
.target(uri)
.queryParam("input", "hello");
Response response =
echoEndpointTarget.request(TEXT_PLAIN).header(HttpHeaders.AUTHORIZATION, "Bearer " + token).get();
Assert.assertEquals(response.getStatus(), HttpURLConnection.HTTP_UNAUTHORIZED);
}

}
Expand Up @@ -34,6 +34,7 @@
import org.eclipse.microprofile.jwt.tck.TCKConstants;
import org.eclipse.microprofile.jwt.tck.container.jaxrs.RolesEndpoint;
import org.eclipse.microprofile.jwt.tck.container.jaxrs.TCKApplication;
import org.eclipse.microprofile.jwt.tck.util.KeyManagementAlgorithm;
import org.eclipse.microprofile.jwt.tck.util.MpJwtTestVersion;
import org.eclipse.microprofile.jwt.tck.util.TokenUtils;
import org.jboss.arquillian.container.test.api.Deployment;
Expand Down Expand Up @@ -139,9 +140,9 @@ public void callEchoBASIC() {
}

@RunAsClient
@Test(groups = TEST_GROUP_JAXRS, description = "Validate a request with MP-JWT succeeds with HTTP_OK, and replies with hello, user={token upn claim}")
public void callEcho() {
Reporter.log("callEcho, expect HTTP_OK");
@Test(groups = TEST_GROUP_JAXRS, description = "Validate a request with RSA-OAEP encrypted token succeeds")
public void callEchoRsaOaep() {
Reporter.log("callEcho with RSA-OAEP encrypted token, expect HTTP_OK");

String uri = baseURL.toExternalForm() + "endp/echo";
WebTarget echoEndpointTarget = ClientBuilder.newClient()
Expand All @@ -155,6 +156,25 @@ public void callEcho() {
Assert.assertEquals(reply, "hello, user=jdoe@example.com");
}

@RunAsClient
@Test(groups = TEST_GROUP_JAXRS, description = "Validate a request with RSA-OAEP-256 encrypted token fails with HTTP_UNAUTHORIZED")
public void callEchoRsaOaep256() throws Exception {
Reporter.log("callEcho with RSA-OAEP-356 encrypted token, expect HTTP_UNAUTHORIZED");

PrivateKey signingKey = TokenUtils.readPrivateKey("/privateKey4k.pem");
PublicKey encryptionKey = TokenUtils.readPublicKey("/publicKey.pem");
String token =
TokenUtils.signEncryptClaims(signingKey, null, encryptionKey, KeyManagementAlgorithm.RSA_OAEP_256, null,
"/Token1.json", true);
String uri = baseURL.toExternalForm() + "endp/echo";
WebTarget echoEndpointTarget = ClientBuilder.newClient()
.target(uri)
.queryParam("input", "hello");
Response response =
echoEndpointTarget.request(TEXT_PLAIN).header(HttpHeaders.AUTHORIZATION, "Bearer " + token).get();
Assert.assertEquals(response.getStatus(), HttpURLConnection.HTTP_UNAUTHORIZED);
}

@RunAsClient
@Test(groups = TEST_GROUP_JAXRS, description = "Validate a request with MP-JWT fail with HTTP_UNAUTHORIZED if no 'cty' header is set")
public void callEchoWithoutCty() throws Exception {
Expand Down

0 comments on commit 9054773

Please sign in to comment.