Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change Default Certificate Hashing Algorithm to SHA-256 #12365

Merged
merged 4 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,9 @@ public class JWTConstants {
public static final String ORGANIZATIONS = "organizations";
public static final String GATEWAY_JWKS_API_CONTEXT = "/jwks";
public static final String GATEWAY_JWKS_API_NAME = "_JwksEndpoint_";

public static final String SHA_256 = "SHA-256";
public static final String SHA_1 = "SHA-1";
public static final String X5T_PARAMETER = "x5t";
public static final String X5T256_PARAMETER = "x5t#S256";
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
private String jwtHeader = "X-JWT-Assertion";
private String consumerDialectUri = "http://wso2.org/claims";
private String signatureAlgorithm = "SHA256withRSA";

private boolean useSHA1Hash = false;
private String jwtDecoding = "base64";
private boolean enableUserClaims;
private String gatewayJWTGeneratorImpl;
Expand All @@ -59,6 +61,7 @@
this.jwtHeader = jwtConfigurationDto.jwtHeader;
this.consumerDialectUri = jwtConfigurationDto.consumerDialectUri;
this.signatureAlgorithm = jwtConfigurationDto.signatureAlgorithm;
this.useSHA1Hash = jwtConfigurationDto.useSHA1Hash;

Check warning on line 64 in components/apimgt/org.wso2.carbon.apimgt.common.gateway/src/main/java/org/wso2/carbon/apimgt/common/gateway/dto/JWTConfigurationDto.java

View check run for this annotation

Codecov / codecov/patch

components/apimgt/org.wso2.carbon.apimgt.common.gateway/src/main/java/org/wso2/carbon/apimgt/common/gateway/dto/JWTConfigurationDto.java#L64

Added line #L64 was not covered by tests
this.jwtDecoding = jwtConfigurationDto.jwtDecoding;
this.enableUserClaims = jwtConfigurationDto.enableUserClaims;
this.gatewayJWTGeneratorImpl = jwtConfigurationDto.gatewayJWTGeneratorImpl;
Expand Down Expand Up @@ -190,4 +193,11 @@
return ttl;
}

public boolean useSHA1Hash() {
return useSHA1Hash;

Check warning on line 197 in components/apimgt/org.wso2.carbon.apimgt.common.gateway/src/main/java/org/wso2/carbon/apimgt/common/gateway/dto/JWTConfigurationDto.java

View check run for this annotation

Codecov / codecov/patch

components/apimgt/org.wso2.carbon.apimgt.common.gateway/src/main/java/org/wso2/carbon/apimgt/common/gateway/dto/JWTConfigurationDto.java#L197

Added line #L197 was not covered by tests
}

public void setUseSHA1Hash(boolean useSHA1Hash) {
this.useSHA1Hash = useSHA1Hash;
}

Check warning on line 202 in components/apimgt/org.wso2.carbon.apimgt.common.gateway/src/main/java/org/wso2/carbon/apimgt/common/gateway/dto/JWTConfigurationDto.java

View check run for this annotation

Codecov / codecov/patch

components/apimgt/org.wso2.carbon.apimgt.common.gateway/src/main/java/org/wso2/carbon/apimgt/common/gateway/dto/JWTConfigurationDto.java#L201-L202

Added lines #L201 - L202 were not covered by tests
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@

private String signatureAlgorithm;

private boolean useSHA1Hash;

public AbstractAPIMgtGatewayJWTGenerator() {
}

Expand All @@ -71,6 +73,7 @@
|| SHA256_WITH_RSA.equals(signatureAlgorithm))) {
signatureAlgorithm = SHA256_WITH_RSA;
}
useSHA1Hash = jwtConfigurationDto.useSHA1Hash();

Check warning on line 76 in components/apimgt/org.wso2.carbon.apimgt.common.gateway/src/main/java/org/wso2/carbon/apimgt/common/gateway/jwtgenerator/AbstractAPIMgtGatewayJWTGenerator.java

View check run for this annotation

Codecov / codecov/patch

components/apimgt/org.wso2.carbon.apimgt.common.gateway/src/main/java/org/wso2/carbon/apimgt/common/gateway/jwtgenerator/AbstractAPIMgtGatewayJWTGenerator.java#L76

Added line #L76 was not covered by tests

}

Expand Down Expand Up @@ -146,7 +149,8 @@

try {
Certificate publicCert = jwtConfigurationDto.getPublicCert();
return JWTUtil.generateHeader(publicCert, signatureAlgorithm, jwtConfigurationDto.useKid());
return JWTUtil.generateHeader(publicCert, signatureAlgorithm, jwtConfigurationDto.useKid(),

Check warning on line 152 in components/apimgt/org.wso2.carbon.apimgt.common.gateway/src/main/java/org/wso2/carbon/apimgt/common/gateway/jwtgenerator/AbstractAPIMgtGatewayJWTGenerator.java

View check run for this annotation

Codecov / codecov/patch

components/apimgt/org.wso2.carbon.apimgt.common.gateway/src/main/java/org/wso2/carbon/apimgt/common/gateway/jwtgenerator/AbstractAPIMgtGatewayJWTGenerator.java#L152

Added line #L152 was not covered by tests
useSHA1Hash);
} catch (Exception e) {
String error = "Error in obtaining keystore";
throw new JWTGeneratorException(error, e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.apache.commons.logging.LogFactory;
import org.json.JSONException;
import org.json.JSONObject;
import org.wso2.carbon.apimgt.common.gateway.constants.JWTConstants;
import org.wso2.carbon.apimgt.common.gateway.exception.JWTGeneratorException;
import org.wso2.carbon.apimgt.common.gateway.jwtgenerator.JWTSignatureAlg;

Expand Down Expand Up @@ -78,7 +79,7 @@

public static String generateHeader(Certificate publicCert, String signatureAlgorithm)
throws JWTGeneratorException {
return generateHeader(publicCert, signatureAlgorithm, false);
return generateHeader(publicCert, signatureAlgorithm, false, false);

Check warning on line 82 in components/apimgt/org.wso2.carbon.apimgt.common.gateway/src/main/java/org/wso2/carbon/apimgt/common/gateway/util/JWTUtil.java

View check run for this annotation

Codecov / codecov/patch

components/apimgt/org.wso2.carbon.apimgt.common.gateway/src/main/java/org/wso2/carbon/apimgt/common/gateway/util/JWTUtil.java#L82

Added line #L82 was not covered by tests
}

/**
Expand All @@ -87,23 +88,26 @@
* @param publicCert The public certificate which needs to include in the header as thumbprint
* @param signatureAlgorithm Signature algorithm which needs to include in the header
* @param useKid Specifies whether the header should include the kid property
* @param useSHA1Hash Specifies whether to use SHA-1 algorithm to generate the certificate thumbprint
* @throws JWTGeneratorException
*/

public static String generateHeader(Certificate publicCert, String signatureAlgorithm, boolean useKid)
public static String generateHeader(Certificate publicCert, String signatureAlgorithm, boolean useKid,
boolean useSHA1Hash)
throws JWTGeneratorException {

/*
* Sample header
* {"typ":"JWT", "alg":"SHA256withRSA", "x5t":"a_jhNus21KVuoFx65LmkW2O_l10",
* {"typ":"JWT", "alg":"SHA256withRSA", "x5t#S256":"a_jhNus21KVuoFx65LmkW2O_l10",
* "kid":"a_jhNus21KVuoFx65LmkW2O_l10"}
* {"typ":"JWT", "alg":"[2]", "x5t":"[1]", "x5t":"[1]"}
* {"typ":"JWT", "alg":"[2]", "x5t#S256":"[1]"}
* */
try {
X509Certificate x509Certificate = (X509Certificate) publicCert;

//generate the SHA-1 thumbprint of the certificate
MessageDigest digestValue = MessageDigest.getInstance("SHA-1");
String hashingAlgorithm = useSHA1Hash ? JWTConstants.SHA_1 : JWTConstants.SHA_256;
//generate the thumbprint of the certificate
MessageDigest digestValue = MessageDigest.getInstance(hashingAlgorithm);
byte[] der = publicCert.getEncoded();
digestValue.update(der);
byte[] digestInBytes = digestValue.digest();
Expand All @@ -115,7 +119,12 @@
JSONObject jwtHeader = new JSONObject();
jwtHeader.put("typ", "JWT");
jwtHeader.put("alg", getJWSCompliantAlgorithmCode(signatureAlgorithm));
jwtHeader.put("x5t", base64UrlEncodedThumbPrint);
if (useSHA1Hash) {
jwtHeader.put(JWTConstants.X5T_PARAMETER, base64UrlEncodedThumbPrint);
} else {
jwtHeader.put(JWTConstants.X5T256_PARAMETER, base64UrlEncodedThumbPrint);
}

if (useKid) {
jwtHeader.put("kid", getKID(x509Certificate));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@

import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;

Expand Down Expand Up @@ -59,13 +61,58 @@ public void testJWTHeader() throws Exception {
X509Certificate cert = (X509Certificate) certificateFactory.generateCertificate(
Files.newInputStream(Paths.get("src/test/resources/cnf/certificate.pem"))
);
String signatureAlgorithm = "SHA256withRSA";

String jwt = JWTUtil.generateHeader(cert, "SHA256withRSA", true);
//Use SHA-256 as the certificate hashing algorithm
String jwt = JWTUtil.generateHeader(cert, signatureAlgorithm, true, false);
Assert.assertNotNull(jwt);
Assert.assertTrue(jwt.contains("kid"));

jwt = JWTUtil.generateHeader(cert, "SHA256withRSA", false);
String encodedThumbprint = generateCertThumbprint(cert, "SHA-256");
//Check if the encoded thumbprint matches with the JWT header's x5t#S256
Assert.assertTrue(jwt.contains(encodedThumbprint));
Assert.assertTrue(jwt.contains("x5t#S256"));

//Use SHA-1 as the certificate hashing algorithm
jwt = JWTUtil.generateHeader(cert, signatureAlgorithm, false, true);
Assert.assertNotNull(jwt);
Assert.assertFalse(jwt.contains("kid"));

encodedThumbprint = generateCertThumbprint(cert, "SHA-1");
//Check if the encoded thumbprint matches with the JWT header's x5t
Assert.assertTrue(jwt.contains(encodedThumbprint));
Assert.assertTrue(jwt.contains("x5t"));
}

private String generateCertThumbprint(Certificate cert, String hashingAlgorithm) throws Exception {
//Get the public certificate's thumbprint and base64url encode it
byte[] der = cert.getEncoded();
MessageDigest digestValue = MessageDigest.getInstance(hashingAlgorithm);
digestValue.update(der);
byte[] digestInBytes = digestValue.digest();
String publicCertThumbprint = hexify(digestInBytes);
return java.util.Base64.getUrlEncoder()
.encodeToString(publicCertThumbprint.getBytes("UTF-8"));
}

/**
* Helper method to hexify a byte array.
*
* @param bytes - The input byte array
* @return hexadecimal representation
*/
private String hexify(byte bytes[]) {

char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};

StringBuilder buf = new StringBuilder(bytes.length * 2);

for (byte aByte : bytes) {
buf.append(hexDigits[(aByte & 0xf0) >> 4]);
buf.append(hexDigits[aByte & 0x0f]);
}

return buf.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,10 @@ public final class APIConstants {
public static final String USE_KID = "UseKidProperty";
public static final String CONSUMER_DIALECT_URI = "ConsumerDialectURI";
public static final String JWT_SIGNATURE_ALGORITHM = "SignatureAlgorithm";
public static final String USE_SHA1_HASH = "UseSHA1Hash";

public static final String X5T_PARAMETER = "x5t";
public static final String X5T256_PARAMETER = "x5t#S256";
public static final String GATEWAY_JWT_GENERATOR = "GatewayJWTGeneration";
public static final String GATEWAY_JWT_GENERATOR_IMPL = "ImplClass";
public static final String TOKEN_ISSUERS = "TokenIssuers";
Expand Down Expand Up @@ -1378,6 +1382,8 @@ public static class AccessTokenConstants {

public static final String SHA_256 = "SHA-256";

public static final String SHA_1 = "SHA-1";

public static final String US_ASCII = "US-ASCII";

public static class DigestAuthConstants {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1630,6 +1630,11 @@
if (signatureElement != null) {
jwtConfigurationDto.setSignatureAlgorithm(signatureElement.getText());
}
OMElement useSHA1HashElement =
omElement.getFirstChildWithName(new QName(APIConstants.USE_SHA1_HASH));

Check warning on line 1634 in components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/APIManagerConfiguration.java

View check run for this annotation

Codecov / codecov/patch

components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/APIManagerConfiguration.java#L1633-L1634

Added lines #L1633 - L1634 were not covered by tests
if (useSHA1HashElement != null) {
jwtConfigurationDto.setUseSHA1Hash(Boolean.parseBoolean(useSHA1HashElement.getText()));

Check warning on line 1636 in components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/APIManagerConfiguration.java

View check run for this annotation

Codecov / codecov/patch

components/apimgt/org.wso2.carbon.apimgt.impl/src/main/java/org/wso2/carbon/apimgt/impl/APIManagerConfiguration.java#L1636

Added line #L1636 was not covered by tests
}
OMElement claimRetrieverImplElement =
omElement.getFirstChildWithName(new QName(APIConstants.CLAIMS_RETRIEVER_CLASS));
if (claimRetrieverImplElement != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,8 @@ protected String buildHeader() throws APIManagementException {
*/
private JSONObject generateHeader(Certificate publicCert, String signatureAlgorithm) throws APIManagementException {
try {
//generate the SHA-1 thumbprint of the certificate
MessageDigest digestValue = MessageDigest.getInstance("SHA-1");
//generate the SHA-256 thumbprint of the certificate
MessageDigest digestValue = MessageDigest.getInstance("SHA-256");
byte[] der = publicCert.getEncoded();
digestValue.update(der);
byte[] digestInBytes = digestValue.digest();
Expand All @@ -146,14 +146,14 @@ private JSONObject generateHeader(Certificate publicCert, String signatureAlgori

/*
* Sample header
* {"typ":"JWT", "alg":"SHA256withRSA", "x5t":"a_jhNus21KVuoFx65LmkW2O_l10",
* {"typ":"JWT", "alg":"SHA256withRSA", "x5t#S256":"a_jhNus21KVuoFx65LmkW2O_l10",
* "kid":"a_jhNus21KVuoFx65LmkW2O_l10_RS256"}
* {"typ":"JWT", "alg":"[2]", "x5t":"[1]", "x5t":"[1]"}
* {"typ":"JWT", "alg":"[2]", "x5t#S256":"[1]", "kid":"gateway_certificate_alias"}
* */
JSONObject jwtHeader = new JSONObject();
jwtHeader.put("typ", "JWT");
jwtHeader.put("alg", APIUtil.getJWSCompliantAlgorithmCode(signatureAlgorithm));
jwtHeader.put("x5t", base64UrlEncodedThumbPrint);
jwtHeader.put("x5t#S256", base64UrlEncodedThumbPrint);
return jwtHeader;

} catch (NoSuchAlgorithmException | CertificateEncodingException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@
private boolean tenantBasedSigningEnabled;
private boolean useKid;

private boolean useSHA1Hash;

public AbstractJWTGenerator() {

ExtendedJWTConfigurationDto jwtConfigurationDto =
Expand Down Expand Up @@ -125,6 +127,7 @@
}
tenantBasedSigningEnabled = jwtConfigurationDto.isTenantBasedSigningEnabled();
useKid = jwtConfigurationDto.useKid();
useSHA1Hash = jwtConfigurationDto.useSHA1Hash();

Check warning on line 130 in components/apimgt/org.wso2.carbon.apimgt.keymgt/src/main/java/org/wso2/carbon/apimgt/keymgt/token/AbstractJWTGenerator.java

View check run for this annotation

Codecov / codecov/patch

components/apimgt/org.wso2.carbon.apimgt.keymgt/src/main/java/org/wso2/carbon/apimgt/keymgt/token/AbstractJWTGenerator.java#L130

Added line #L130 was not covered by tests
}

public String getDialectURI() {
Expand Down Expand Up @@ -344,7 +347,7 @@
KeyStoreManager keyStoreManager = KeyStoreManager.getInstance(MultitenantConstants.SUPER_TENANT_ID);
publicCert = keyStoreManager.getDefaultPrimaryCertificate();
}
return generateHeader(publicCert, signatureAlgorithm, useKid);
return generateHeader(publicCert, signatureAlgorithm, useKid, useSHA1Hash);

Check warning on line 350 in components/apimgt/org.wso2.carbon.apimgt.keymgt/src/main/java/org/wso2/carbon/apimgt/keymgt/token/AbstractJWTGenerator.java

View check run for this annotation

Codecov / codecov/patch

components/apimgt/org.wso2.carbon.apimgt.keymgt/src/main/java/org/wso2/carbon/apimgt/keymgt/token/AbstractJWTGenerator.java#L350

Added line #L350 was not covered by tests
} catch (Exception e) {
String error = "Error in obtaining keystore";
throw new APIManagementException(error, e);
Expand Down Expand Up @@ -388,12 +391,15 @@
* @param publicCert The public certificate which needs to include in the header as thumbprint
* @param signatureAlgorithm Signature algorithm which needs to include in the header
* @param useKid Boolean to indicate whether to include kid property in the header
* @param useSHA1Hash Specifies whether to use SHA-1 algorithm to generate the certificate thumbprint
*/
public static String generateHeader(Certificate publicCert, String signatureAlgorithm, boolean useKid)
public static String generateHeader(Certificate publicCert, String signatureAlgorithm, boolean useKid,
boolean useSHA1Hash)
throws APIManagementException {
try {
//generate the SHA-1 thumbprint of the certificate
MessageDigest digestValue = MessageDigest.getInstance("SHA-1");
String hashingAlgorithm = useSHA1Hash ? APIConstants.SHA_1 : APIConstants.SHA_256;
//generate the thumbprint of the certificate
MessageDigest digestValue = MessageDigest.getInstance(hashingAlgorithm);
byte[] der = publicCert.getEncoded();
digestValue.update(der);
byte[] digestInBytes = digestValue.digest();
Expand All @@ -405,14 +411,18 @@

/*
* Sample header
* {"typ":"JWT", "alg":"SHA256withRSA", "x5t":"a_jhNus21KVuoFx65LmkW2O_l10",
* {"typ":"JWT", "alg":"SHA256withRSA", "x5t#S256":"a_jhNus21KVuoFx65LmkW2O_l10",
* "kid":"a_jhNus21KVuoFx65LmkW2O_l10_RS256"}
* {"typ":"JWT", "alg":"[2]", "x5t":"[1]", "x5t":"[1]"}
* {"typ":"JWT", "alg":"[2]", "x5t#S256":"[1]"}
* */
JSONObject jwtHeader = new JSONObject();
jwtHeader.put("typ", "JWT");
jwtHeader.put("alg", APIUtil.getJWSCompliantAlgorithmCode(signatureAlgorithm));
jwtHeader.put("x5t", base64UrlEncodedThumbPrint);
if (useSHA1Hash) {
jwtHeader.put(APIConstants.X5T_PARAMETER, base64UrlEncodedThumbPrint);
} else {
jwtHeader.put(APIConstants.X5T256_PARAMETER, base64UrlEncodedThumbPrint);
}
if (useKid) {
jwtHeader.put("kid", JWTUtil.getKID(x509Certificate));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
SubscriptionDataHolder.class})
public class TokenGenTest {
private static final Log log = LogFactory.getLog(TokenGenTest.class);
private static final String signatureAlgorithm = "SHA256withRSA";

@Before
public void setUp() throws Exception {
Expand Down Expand Up @@ -232,26 +233,49 @@ public Map<String, String> convertClaimMap(Map<ClaimMapping, String> userAttribu

@Test
public void testJWTx5tEncoding() throws Exception {
//Read public certificat
//Read public certificate
InputStream inputStream = new FileInputStream("src/test/resources/wso2carbon.jks");
KeyStore keystore = KeyStore.getInstance("JKS");
char[] pwd = "wso2carbon".toCharArray();
keystore.load(inputStream, pwd);
Certificate cert = keystore.getCertificate("wso2carbon");

//Generate JWT header using the above certificate
String header = AbstractJWTGenerator.generateHeader(cert, "SHA256withRSA", false);
//Generate JWT header using the above certificate. Use SHA-1 as the certificate hashing algorithm.
String header = AbstractJWTGenerator.generateHeader(cert, signatureAlgorithm, false, true);

String encodedThumbprint = generateCertThumbprint(cert, "SHA-1");
//Check if the encoded thumbprint matches with the JWT header's x5t
Assert.assertTrue(header.contains(encodedThumbprint));
Assert.assertTrue(header.contains("x5t"));
}

@Test
public void testJWTx5tS256Encoding() throws Exception {
//Read public certificate
InputStream inputStream = new FileInputStream("src/test/resources/wso2carbon.jks");
KeyStore keystore = KeyStore.getInstance("JKS");
char[] pwd = "wso2carbon".toCharArray();
keystore.load(inputStream, pwd);
Certificate cert = keystore.getCertificate("wso2carbon");

//Generate JWT header using the above certificate. Use SHA-256 as the certificate hashing algorithm.
String header = AbstractJWTGenerator.generateHeader(cert, signatureAlgorithm, false, false);

String encodedThumbprint = generateCertThumbprint(cert, "SHA-256");
//Check if the encoded thumbprint matches with the JWT header's x5t#S256
Assert.assertTrue(header.contains(encodedThumbprint));
Assert.assertTrue(header.contains("x5t#S256"));
}

private String generateCertThumbprint(Certificate cert, String hashingAlgorithm) throws Exception {
//Get the public certificate's thumbprint and base64url encode it
byte[] der = cert.getEncoded();
MessageDigest digestValue = MessageDigest.getInstance("SHA-1");
MessageDigest digestValue = MessageDigest.getInstance(hashingAlgorithm);
digestValue.update(der);
byte[] digestInBytes = digestValue.digest();
String publicCertThumbprint = hexify(digestInBytes);
String encodedThumbprint = java.util.Base64.getUrlEncoder()
return java.util.Base64.getUrlEncoder()
.encodeToString(publicCertThumbprint.getBytes("UTF-8"));
//Check if the encoded thumbprint get matched with JWT header's x5t
Assert.assertTrue(header.contains(encodedThumbprint));
}

/**
Expand Down
Loading
Loading