Skip to content

Commit 566ef29

Browse files
committed
Add x5c and x5t#S256 to jwks response
1 parent 4deae96 commit 566ef29

File tree

4 files changed

+194
-16
lines changed

4 files changed

+194
-16
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com) All Rights Reserved.
3+
*
4+
* WSO2 LLC. licenses this file to you under the Apache License,
5+
* Version 2.0 (the "License"); you may not use this file except
6+
* in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing,
12+
* software distributed under the License is distributed on an
13+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
* KIND, either express or implied. See the License for the
15+
* specific language governing permissions and limitations
16+
* under the License.
17+
*/
18+
19+
package org.wso2.carbon.apimgt.gateway.dto;
20+
21+
import java.security.cert.Certificate;
22+
23+
/**
24+
* Contains Certificate information
25+
*/
26+
public class CertificateInfo {
27+
28+
private Certificate certificate;
29+
private Certificate[] certificateChain;
30+
private String certificateAlias;
31+
32+
public CertificateInfo(Certificate certificate, String certificateAlias) {
33+
34+
this.certificate = certificate;
35+
this.certificateAlias = certificateAlias;
36+
}
37+
38+
public Certificate getCertificate() {
39+
40+
return certificate;
41+
}
42+
43+
public Certificate[] getCertificateChain() {
44+
45+
return certificateChain;
46+
}
47+
48+
public void setCertificateChain(Certificate[] certificateChain) {
49+
50+
this.certificateChain = certificateChain;
51+
}
52+
53+
public String getCertificateAlias() {
54+
55+
return certificateAlias;
56+
}
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com) All Rights Reserved.
3+
*
4+
* WSO2 LLC. licenses this file to you under the Apache License,
5+
* Version 2.0 (the "License"); you may not use this file except
6+
* in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing,
12+
* software distributed under the License is distributed on an
13+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
* KIND, either express or implied. See the License for the
15+
* specific language governing permissions and limitations
16+
* under the License.
17+
*/
18+
19+
package org.wso2.carbon.apimgt.gateway.exception;
20+
21+
import org.wso2.carbon.apimgt.api.APIManagementException;
22+
23+
/**
24+
* OAuth 2 exception.
25+
*/
26+
public class OAuth2Exception extends APIManagementException {
27+
28+
public OAuth2Exception(String message) {
29+
super(message);
30+
}
31+
32+
public OAuth2Exception(String message, Throwable e) {
33+
super(message, e);
34+
}
35+
36+
}

components/apimgt/org.wso2.carbon.apimgt.gateway/src/main/java/org/wso2/carbon/apimgt/gateway/handlers/common/JwksHandler.java

+50-16
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
import com.nimbusds.jose.JWSAlgorithm;
2222
import com.nimbusds.jose.jwk.KeyUse;
2323
import com.nimbusds.jose.jwk.RSAKey;
24+
import com.nimbusds.jose.util.Base64;
25+
import com.nimbusds.jose.util.Base64URL;
26+
import net.minidev.json.JSONArray;
27+
import net.minidev.json.JSONObject;
2428
import org.apache.axis2.AxisFault;
2529
import org.apache.axis2.Constants;
2630
import org.apache.commons.logging.Log;
@@ -29,10 +33,10 @@
2933
import org.apache.synapse.commons.json.JsonUtil;
3034
import org.apache.synapse.core.axis2.Axis2MessageContext;
3135
import org.apache.synapse.rest.AbstractHandler;
32-
import org.json.JSONArray;
33-
import org.json.JSONObject;
3436
import org.wso2.carbon.apimgt.api.APIManagementException;
3537
import org.wso2.carbon.apimgt.common.gateway.util.JWTUtil;
38+
import org.wso2.carbon.apimgt.gateway.dto.CertificateInfo;
39+
import org.wso2.carbon.apimgt.gateway.utils.GatewayUtils;
3640
import org.wso2.carbon.apimgt.impl.APIConstants;
3741
import org.wso2.carbon.apimgt.impl.dto.ExtendedJWTConfigurationDto;
3842
import org.wso2.carbon.apimgt.impl.internal.ServiceReferenceHolder;
@@ -44,6 +48,7 @@
4448
import java.security.KeyStore;
4549
import java.security.KeyStoreException;
4650
import java.security.cert.Certificate;
51+
import java.security.cert.CertificateEncodingException;
4752
import java.security.cert.X509Certificate;
4853
import java.security.interfaces.RSAPublicKey;
4954
import java.text.ParseException;
@@ -63,7 +68,7 @@ public class JwksHandler extends AbstractHandler {
6368
private static final Log log = LogFactory.getLog(JwksHandler.class);
6469
private static final String KEY_USE = "sig";
6570
private static final String KEYS = "keys";
66-
private final Map<String, Set<Certificate>> certificateMap = new HashMap<>();
71+
private final Map<String, Set<CertificateInfo>> certificateMap = new HashMap<>();
6772
ExtendedJWTConfigurationDto jwtConfigurationDto;
6873

6974
public boolean handleRequest(MessageContext messageContext) {
@@ -77,7 +82,7 @@ public boolean handleRequest(MessageContext messageContext) {
7782
axis2MsgContext.setProperty(Constants.Configuration.MESSAGE_TYPE, APIConstants.APPLICATION_JSON_MEDIA_TYPE);
7883
axis2MsgContext.setProperty(Constants.Configuration.CONTENT_TYPE, APIConstants.APPLICATION_JSON_MEDIA_TYPE);
7984
axis2MsgContext.removeProperty(APIConstants.NO_ENTITY_BODY);
80-
} catch (ParseException | AxisFault | APIManagementException e) {
85+
} catch (ParseException | AxisFault | APIManagementException | CertificateEncodingException e) {
8186
log.error("Error while generating payload " + axis2MsgContext.getLogIDString(), e);
8287
}
8388
return true;
@@ -92,7 +97,7 @@ public boolean handleResponse(MessageContext messageContext) {
9297
*
9398
* @return JWKS response
9499
*/
95-
public String getJwksEndpointResponse() throws ParseException, APIManagementException {
100+
public String getJwksEndpointResponse() throws ParseException, APIManagementException, CertificateEncodingException {
96101
this.jwtConfigurationDto = org.wso2.carbon.apimgt.gateway.internal.ServiceReferenceHolder.getInstance()
97102
.getAPIManagerConfiguration().getJwtConfigurationDto();
98103
boolean isTenantBasedSigningEnabled = jwtConfigurationDto.isTenantBasedSigningEnabled();
@@ -103,7 +108,7 @@ public String getJwksEndpointResponse() throws ParseException, APIManagementExce
103108
certMapKey = APIConstants.SUPER_TENANT_DOMAIN;
104109
}
105110

106-
Set<Certificate> certificateSet = null;
111+
Set<CertificateInfo> certificateSet = null;
107112
if (certificateMap.containsKey(certMapKey)) {
108113
certificateSet = certificateMap.get(certMapKey);
109114
} else {
@@ -126,24 +131,30 @@ public String getJwksEndpointResponse() throws ParseException, APIManagementExce
126131
* @param certificates Set of certificates
127132
* @return JWKS response as a JSON string
128133
*/
129-
private String buildResponse(Set<Certificate> certificates) throws ParseException, APIManagementException {
134+
private String buildResponse(Set<CertificateInfo> certificates) throws ParseException, APIManagementException,
135+
CertificateEncodingException {
130136

131137
JSONArray jwksArray = new JSONArray();
132138
JSONObject jwksJson = new JSONObject();
133139
JWSAlgorithm accessTokenSignAlgorithm = mapSignatureAlgorithmForJWSAlgorithm(
134140
ServiceReferenceHolder.getInstance().getOauthServerConfiguration().getSignatureAlgorithm());
135141
List<JWSAlgorithm> diffAlgorithms = findDifferentAlgorithms(accessTokenSignAlgorithm);
136142

137-
for (Certificate certificate : certificates) {
143+
for (CertificateInfo certificateInfo : certificates) {
138144
for (JWSAlgorithm algorithm : diffAlgorithms) {
139-
RSAPublicKey publicKey = (RSAPublicKey) certificate.getPublicKey();
145+
String alias = certificateInfo.getCertificateAlias();
146+
X509Certificate x509Certificate = (X509Certificate) certificateInfo.getCertificate();
147+
RSAPublicKey publicKey = (RSAPublicKey) x509Certificate.getPublicKey();
140148
RSAKey.Builder jwk = new RSAKey.Builder(publicKey);
141149

142-
X509Certificate x509Certificate = (X509Certificate) certificate;
150+
Certificate[] certChain = certificateInfo.getCertificateChain();
151+
List<Base64> encodedCertList = generateEncodedCertList(certChain, alias);
143152
jwk.keyID(JWTUtil.getKID(x509Certificate));
144153
jwk.algorithm(algorithm);
145154
jwk.keyUse(KeyUse.parse(KEY_USE));
146-
jwksArray.put(jwk.build().toJSONObject());
155+
jwk.x509CertChain(encodedCertList);
156+
jwk.x509CertSHA256Thumbprint(new Base64URL(GatewayUtils.getThumbPrint(x509Certificate, alias)));
157+
jwksArray.add(jwk.build().toJSONObject());
147158
}
148159
}
149160

@@ -192,8 +203,8 @@ private String generateKSNameFromDomainName(String tenantDomain) {
192203
* @param tenantDomain tenant domain which is used to get the relevant key store and extract certificates
193204
* @return set of certificates
194205
*/
195-
private Set<Certificate> getCertificates(String tenantDomain) {
196-
Set<Certificate> certificates = new HashSet<>();
206+
private Set<CertificateInfo> getCertificates(String tenantDomain) {
207+
Set<CertificateInfo> certificates = new HashSet<>();
197208
KeyStore keyStore;
198209
try {
199210
if (!APIConstants.SUPER_TENANT_DOMAIN.equals(tenantDomain)) {
@@ -232,13 +243,14 @@ private Set<Certificate> getCertificates(String tenantDomain) {
232243
* @param keyStore Key store
233244
* @return Set of certificates from the key store
234245
*/
235-
private Set<Certificate> getCertificatesFromKeyStore(KeyStore keyStore) throws KeyStoreException {
236-
Set<Certificate> certs = new HashSet<>();
246+
private Set<CertificateInfo> getCertificatesFromKeyStore(KeyStore keyStore) throws KeyStoreException {
247+
Set<CertificateInfo> certs = new HashSet<>();
237248
Enumeration<String> enumeration = keyStore.aliases();
238249
while (enumeration.hasMoreElements()) {
239250
String alias = enumeration.nextElement();
240251
if (keyStore.isKeyEntry(alias)) {
241-
Certificate publicCert = keyStore.getCertificate(alias);
252+
CertificateInfo publicCert = new CertificateInfo(keyStore.getCertificate(alias), alias);
253+
publicCert.setCertificateChain(keyStore.getCertificateChain(alias));
242254
certs.add(publicCert);
243255
}
244256
}
@@ -277,4 +289,26 @@ private static JWSAlgorithm mapSignatureAlgorithmForJWSAlgorithm(String signatur
277289
return JWSAlgorithm.PS256;
278290
}
279291
}
292+
293+
/**
294+
* This method generates the base64 encoded certificate list from a Certificate array
295+
*
296+
* @return base64 encoded certificate list
297+
*/
298+
private List<Base64> generateEncodedCertList(Certificate[] certificates, String alias)
299+
throws CertificateEncodingException {
300+
301+
List<Base64> certList = new ArrayList<>();
302+
for (Certificate certificate : certificates) {
303+
try {
304+
certList.add(Base64.encode(certificate.getEncoded()));
305+
} catch (CertificateEncodingException exception) {
306+
String errorMessage = "Unable to encode the public certificate with alias: " + alias +
307+
" in the tenant domain: " + PrivilegedCarbonContext.getThreadLocalCarbonContext().
308+
getTenantDomain();
309+
throw new CertificateEncodingException(errorMessage, exception);
310+
}
311+
}
312+
return certList;
313+
}
280314
}

components/apimgt/org.wso2.carbon.apimgt.gateway/src/main/java/org/wso2/carbon/apimgt/gateway/utils/GatewayUtils.java

+51
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
import org.apache.axis2.clustering.ClusteringAgent;
3939
import org.apache.axis2.context.MessageContext;
4040
import org.apache.axis2.description.AxisService;
41+
import org.apache.commons.codec.binary.Base64;
42+
import org.apache.commons.io.Charsets;
4143
import org.apache.commons.lang3.StringUtils;
4244
import org.apache.commons.logging.Log;
4345
import org.apache.commons.logging.LogFactory;
@@ -58,6 +60,7 @@
5860
import org.wso2.carbon.apimgt.common.gateway.dto.JWTValidationInfo;
5961
import org.wso2.carbon.apimgt.gateway.APIMgtGatewayConstants;
6062
import org.wso2.carbon.apimgt.gateway.dto.IPRange;
63+
import org.wso2.carbon.apimgt.gateway.exception.OAuth2Exception;
6164
import org.wso2.carbon.apimgt.gateway.handlers.security.APIKeyValidator;
6265
import org.wso2.carbon.apimgt.gateway.handlers.security.APISecurityConstants;
6366
import org.wso2.carbon.apimgt.gateway.handlers.security.APISecurityException;
@@ -99,7 +102,10 @@
99102
import java.io.IOException;
100103
import java.io.InputStream;
101104
import java.nio.charset.StandardCharsets;
105+
import java.security.MessageDigest;
106+
import java.security.NoSuchAlgorithmException;
102107
import java.security.cert.Certificate;
108+
import java.security.cert.CertificateEncodingException;
103109
import java.security.interfaces.RSAPublicKey;
104110
import java.text.ParseException;
105111
import java.util.ArrayList;
@@ -1783,4 +1789,49 @@ public static List<String> getTenantsToBeDeployed() throws APIManagementExceptio
17831789
}
17841790
return tenantsToBeDeployed;
17851791
}
1792+
1793+
/**
1794+
* Helper method to add public certificate to JWT_HEADER to signature verification.
1795+
* This creates thumbPrints directly from given certificates
1796+
*
1797+
* @param certificate
1798+
* @param alias
1799+
* @return
1800+
* @throws OAuth2Exception
1801+
*/
1802+
public static String getThumbPrint(Certificate certificate, String alias) throws OAuth2Exception {
1803+
return getThumbPrint(certificate);
1804+
}
1805+
1806+
/**
1807+
* Method to obtain certificate thumbprint.
1808+
*
1809+
* @param certificate java.security.cert type certificate.
1810+
* @return Certificate thumbprint as a String.
1811+
* @throws OAuth2Exception When failed to obtain the thumbprint.
1812+
*/
1813+
public static String getThumbPrint(Certificate certificate) throws OAuth2Exception {
1814+
1815+
try {
1816+
MessageDigest digestValue = MessageDigest.getInstance("SHA-256");
1817+
byte[] der = certificate.getEncoded();
1818+
digestValue.update(der);
1819+
byte[] digestInBytes = digestValue.digest();
1820+
1821+
String publicCertThumbprint = APIUtil.hexify(digestInBytes);
1822+
String thumbprint = new String(new Base64(0, null, true).
1823+
encode(publicCertThumbprint.getBytes(Charsets.UTF_8)), Charsets.UTF_8);
1824+
if (log.isDebugEnabled()) {
1825+
log.debug(String.format("Thumbprint value: %s calculated for Certificate: %s using algorithm: %s",
1826+
thumbprint, certificate, digestValue.getAlgorithm()));
1827+
}
1828+
return thumbprint;
1829+
} catch (CertificateEncodingException e) {
1830+
String error = "Error occurred while encoding thumbPrint from certificate.";
1831+
throw new OAuth2Exception(error, e);
1832+
} catch (NoSuchAlgorithmException e) {
1833+
String error = "Error in obtaining SHA-256 thumbprint from certificate.";
1834+
throw new OAuth2Exception(error, e);
1835+
}
1836+
}
17861837
}

0 commit comments

Comments
 (0)