diff --git a/components/identity-core/org.wso2.carbon.identity.core/pom.xml b/components/identity-core/org.wso2.carbon.identity.core/pom.xml index 00237270da07..4f6279e5230e 100644 --- a/components/identity-core/org.wso2.carbon.identity.core/pom.xml +++ b/components/identity-core/org.wso2.carbon.identity.core/pom.xml @@ -208,6 +208,7 @@ org.apache.commons.collections; version="${commons-collections.wso2.osgi.version.range}", org.apache.commons.collections4; version = "${commons-collections4.wso2.osgi.version.range}", ua_parser; version="${ua_parser.version.range}", + org.wso2.carbon.utils.security;version="${carbon.kernel.package.import.version.range}", !org.wso2.carbon.identity.core.internal, diff --git a/components/identity-core/org.wso2.carbon.identity.core/src/main/java/org/wso2/carbon/identity/core/IdentityKeyStoreResolver.java b/components/identity-core/org.wso2.carbon.identity.core/src/main/java/org/wso2/carbon/identity/core/IdentityKeyStoreResolver.java index 1c05a79b5182..a0829c01c927 100644 --- a/components/identity-core/org.wso2.carbon.identity.core/src/main/java/org/wso2/carbon/identity/core/IdentityKeyStoreResolver.java +++ b/components/identity-core/org.wso2.carbon.identity.core/src/main/java/org/wso2/carbon/identity/core/IdentityKeyStoreResolver.java @@ -246,6 +246,78 @@ public Key getPrivateKey(String tenantDomain, InboundProtocol inboundProtocol) return getPrivateKey(tenantDomain); } + /** + * Retrieves the public certificate for a given tenant domain and context. + *

+ * This method fetches the public certificate associated with a specific tenant domain and context. + * If the context is blank, it delegates the call to the overloaded + * {@code getCertificate(String tenantDomain)} method. + * The method first checks if the certificate is cached; if not, it retrieves the certificate from + * the KeyStoreManager, caches it, and then returns it. + *

+ * + * @param tenantDomain the tenant domain for which the certificate is requested. + * @param context the specific context for the tenant's certificate. If blank, the default certificate for the tenant is fetched. + * @return the public certificate for the specified tenant domain and context. + * @throws IdentityKeyStoreResolverException if there is an error while retrieving the certificate. + */ + + private Certificate getCertificate(String tenantDomain, String context) throws IdentityKeyStoreResolverException { + + if (StringUtils.isBlank(context)) { + getCertificate(tenantDomain); + } + int tenantId = IdentityTenantUtil.getTenantId(tenantDomain); + + if (publicCerts.containsKey(buildDomainWithContext(tenantId, context))) { + return publicCerts.get(buildDomainWithContext(tenantId, context)); + } + + KeyStoreManager keyStoreManager = KeyStoreManager.getInstance(tenantId); + Certificate publicCert; + String tenantKeyStoreName = IdentityKeyStoreResolverUtil.buildTenantKeyStoreName(tenantDomain, context); + try { + publicCert = keyStoreManager.getCertificate(tenantKeyStoreName, tenantDomain + + IdentityKeyStoreResolverConstants.KEY_STORE_CONTEXT_SEPARATOR + context); + + } catch (SecurityException e) { + if (e.getMessage() != null && e.getMessage().contains("Key Store with a name: " + tenantKeyStoreName + + " does not exist.")) { + + throw new IdentityKeyStoreResolverException( + ErrorMessages.ERROR_RETRIEVING_TENANT_CONTEXT_PUBLIC_CERTIFICATE_KEYSTORE_NOT_EXIST.getCode(), + String.format( + ErrorMessages.ERROR_RETRIEVING_TENANT_CONTEXT_PUBLIC_CERTIFICATE_KEYSTORE_NOT_EXIST + .getDescription(), tenantDomain), e); + } else { + throw new IdentityKeyStoreResolverException( + ErrorMessages.ERROR_CODE_ERROR_RETRIEVING_TENANT_PUBLIC_CERTIFICATE.getCode(), + String.format(ErrorMessages.ERROR_CODE_ERROR_RETRIEVING_TENANT_PUBLIC_CERTIFICATE.getDescription(), + tenantDomain), e); + } + } catch (Exception e) { + throw new IdentityKeyStoreResolverException( + ErrorMessages.ERROR_CODE_ERROR_RETRIEVING_TENANT_PUBLIC_CERTIFICATE.getCode(), + String.format(ErrorMessages.ERROR_CODE_ERROR_RETRIEVING_TENANT_PUBLIC_CERTIFICATE.getDescription(), + tenantDomain), e); + } + + publicCerts.put(buildDomainWithContext(tenantId, context), publicCert); + return publicCert; + } + + /** + * Concatenates tenantId and context with the separator. + * + * @param tenantId the key store name + * @param context the context + * @return a concatenated string in the format tenantDomain:context + */ + private String buildDomainWithContext(int tenantId, String context) { + + return tenantId + IdentityKeyStoreResolverConstants.KEY_STORE_CONTEXT_SEPARATOR + context; + } + /** * Return Public Certificate of the Primary or tenant keystore according to given tenant domain. * @@ -285,17 +357,22 @@ private Certificate getCertificate(String tenantDomain) throws IdentityKeyStoreR * * @param tenantDomain Tenant domain. * @param inboundProtocol Inbound authentication protocol of the application. + * @param context Context of the keystore. * @return Public Certificate of the Primary, tenant or custom keystore. * @throws IdentityKeyStoreResolverException the exception in the IdentityKeyStoreResolver class. */ - public Certificate getCertificate(String tenantDomain, InboundProtocol inboundProtocol) + public Certificate getCertificate(String tenantDomain, InboundProtocol inboundProtocol, String context) throws IdentityKeyStoreResolverException { + if (StringUtils.isEmpty(tenantDomain)) { throw new IdentityKeyStoreResolverException( ErrorMessages.ERROR_CODE_INVALID_ARGUMENT.getCode(), String.format(ErrorMessages.ERROR_CODE_INVALID_ARGUMENT.getDescription(), "Tenant domain")); } + if (context != null) { + return getCertificate(tenantDomain, context); + } if (inboundProtocol == null) { return getCertificate(tenantDomain); } @@ -326,13 +403,27 @@ public Certificate getCertificate(String tenantDomain, InboundProtocol inboundPr throw new IdentityKeyStoreResolverException( ErrorMessages.ERROR_CODE_ERROR_RETRIEVING_CUSTOM_PUBLIC_CERTIFICATE.getCode(), String.format(ErrorMessages.ERROR_CODE_ERROR_RETRIEVING_CUSTOM_PUBLIC_CERTIFICATE - .getDescription(), keyStoreName), e); + .getDescription(), keyStoreName), e); } } } return getCertificate(tenantDomain); } + /** + * Return Public Certificate of the Primary, tenant or custom keystore. + * + * @param tenantDomain Tenant domain. + * @param inboundProtocol Inbound authentication protocol of the application. + * @return Public Certificate of the Primary, tenant or custom keystore. + * @throws IdentityKeyStoreResolverException the exception in the IdentityKeyStoreResolver class. + */ + public Certificate getCertificate(String tenantDomain, InboundProtocol inboundProtocol) + throws IdentityKeyStoreResolverException { + + return getCertificate(tenantDomain, inboundProtocol, null); + } + /** * Return Public Key of the Primary or tenant keystore according to given tenant domain. * diff --git a/components/identity-core/org.wso2.carbon.identity.core/src/main/java/org/wso2/carbon/identity/core/util/IdentityKeyStoreResolverConstants.java b/components/identity-core/org.wso2.carbon.identity.core/src/main/java/org/wso2/carbon/identity/core/util/IdentityKeyStoreResolverConstants.java index def4bdd725d6..795ed078dd6a 100644 --- a/components/identity-core/org.wso2.carbon.identity.core/src/main/java/org/wso2/carbon/identity/core/util/IdentityKeyStoreResolverConstants.java +++ b/components/identity-core/org.wso2.carbon.identity.core/src/main/java/org/wso2/carbon/identity/core/util/IdentityKeyStoreResolverConstants.java @@ -41,6 +41,7 @@ public class IdentityKeyStoreResolverConstants { // KeyStore Constants. public static final String KEY_STORE_EXTENSION = ".jks"; + public static final String KEY_STORE_CONTEXT_SEPARATOR = "--"; // Inbound Protocols. public static final String INBOUND_PROTOCOL_OAUTH = "oauth"; @@ -119,6 +120,10 @@ public enum ErrorMessages { ERROR_CODE_ERROR_RETRIEVING_CUSTOM_KEYSTORE_CONFIGURATION( "IKSR-10009", "Error retrieving custom keystore configuration.", "Error occurred when retrieving custom keystore configuration for: %s."), + ERROR_RETRIEVING_TENANT_CONTEXT_PUBLIC_CERTIFICATE_KEYSTORE_NOT_EXIST( + "IKSR-10010", "Error retrieving context public certificate. Keystore doesn't exist.", + "Error occurred when retrieving context certificate for tenant: %s. " + + "Context Keystore doesn't exist."), // Errors occurred within the IdentityKeyStoreResolver ERROR_CODE_INVALID_ARGUMENT( diff --git a/components/identity-core/org.wso2.carbon.identity.core/src/main/java/org/wso2/carbon/identity/core/util/IdentityKeyStoreResolverUtil.java b/components/identity-core/org.wso2.carbon.identity.core/src/main/java/org/wso2/carbon/identity/core/util/IdentityKeyStoreResolverUtil.java index 98295af64c9b..da0b99e69bfd 100644 --- a/components/identity-core/org.wso2.carbon.identity.core/src/main/java/org/wso2/carbon/identity/core/util/IdentityKeyStoreResolverUtil.java +++ b/components/identity-core/org.wso2.carbon.identity.core/src/main/java/org/wso2/carbon/identity/core/util/IdentityKeyStoreResolverUtil.java @@ -21,6 +21,7 @@ import org.apache.commons.lang.StringUtils; import org.wso2.carbon.core.RegistryResources; import org.wso2.carbon.identity.core.util.IdentityKeyStoreResolverConstants.ErrorMessages; +import org.wso2.carbon.utils.security.KeystoreUtils; import javax.xml.namespace.QName; @@ -38,13 +39,41 @@ public class IdentityKeyStoreResolverUtil { */ public static String buildTenantKeyStoreName(String tenantDomain) throws IdentityKeyStoreResolverException { + return buildTenantKeyStoreName(tenantDomain, null); + } + + /** + * Builds the keystore name for a given tenant domain and context. + * The tenant domain is sanitized by replacing dots (.) with hyphens (-) to ensure compatibility + * with keystore naming conventions. If a context is provided, it is appended to the sanitized + * tenant domain with an underscore (_). The method also appends the standard keystore file + * extension as defined in {@link IdentityKeyStoreResolverConstants}. + * + * @param tenantDomain The domain name of the tenant (e.g., "example.com"). + * @param context The optional context to append to the tenant keystore name. + * @return A sanitized and formatted keystore name for the tenant. + * @throws IdentityKeyStoreResolverException If the tenant domain is null, empty, or invalid. + */ + public static String buildTenantKeyStoreName(String tenantDomain, String context) + throws IdentityKeyStoreResolverException { + + // Validate tenantDomain argument if (StringUtils.isEmpty(tenantDomain)) { throw new IdentityKeyStoreResolverException( ErrorMessages.ERROR_CODE_INVALID_ARGUMENT.getCode(), String.format(ErrorMessages.ERROR_CODE_INVALID_ARGUMENT.getDescription(), "Tenant domain")); } + + // Sanitize tenant domain: replace '.' with '-' String ksName = tenantDomain.trim().replace(".", "-"); - return ksName + IdentityKeyStoreResolverConstants.KEY_STORE_EXTENSION; + + // Append context if provided + if (StringUtils.isNotBlank(context)) { + ksName = buildDomainWithContext(ksName, context); + } + + // Add the keystore extension + return ksName + KeystoreUtils.getKeyStoreFileExtension(tenantDomain); } /** @@ -74,4 +103,16 @@ public static QName getQNameWithIdentityNameSpace(String localPart) { return new QName(IdentityCoreConstants.IDENTITY_DEFAULT_NAMESPACE, localPart); } + + /** + * Concatenates tenantDomain and context with the separator. + * + * @param tenantDomain the key store name + * @param context the context + * @return a concatenated string in the format tenantDomain:context + */ + public static String buildDomainWithContext(String tenantDomain, String context) { + + return tenantDomain + IdentityKeyStoreResolverConstants.KEY_STORE_CONTEXT_SEPARATOR + context; + } } diff --git a/components/identity-core/org.wso2.carbon.identity.core/src/main/java/org/wso2/carbon/identity/core/util/IdentityUtil.java b/components/identity-core/org.wso2.carbon.identity.core/src/main/java/org/wso2/carbon/identity/core/util/IdentityUtil.java index 7d9a51f7e6cd..bf9203020df3 100644 --- a/components/identity-core/org.wso2.carbon.identity.core/src/main/java/org/wso2/carbon/identity/core/util/IdentityUtil.java +++ b/components/identity-core/org.wso2.carbon.identity.core/src/main/java/org/wso2/carbon/identity/core/util/IdentityUtil.java @@ -114,6 +114,7 @@ import static org.wso2.carbon.identity.core.util.IdentityCoreConstants.ENCODED_ZERO; import static org.wso2.carbon.identity.core.util.IdentityCoreConstants.INDEXES; import static org.wso2.carbon.identity.core.util.IdentityCoreConstants.USERS_LIST_PER_ROLE_LOWER_BOUND; +import static org.wso2.carbon.identity.core.util.IdentityKeyStoreResolverConstants.ErrorMessages.ERROR_RETRIEVING_TENANT_CONTEXT_PUBLIC_CERTIFICATE_KEYSTORE_NOT_EXIST; public class IdentityUtil { @@ -1969,6 +1970,61 @@ public static boolean isSCIM2UserMaxItemsPerPageEnabled() { return Boolean.parseBoolean(scim2UserMaxItemsPerPageEnabledProperty); } + /** + * Validates the provided signature for the given data using the public key of a specified tenant. + * + * The method retrieves the public key for the tenant from the certificate stored in the tenant's keystore. + * If a context is provided, the method attempts to retrieve the certificate within that context. + * + * @param data The data to validate the signature against. + * @param signature The signature to be validated. + * @param tenantDomain The domain name of the tenant whose public key should be used for validation. + * @param context The optional context for retrieving the tenant's certificate (can be null or blank). + * @return True if the signature is valid; false otherwise. + * @throws SignatureException If an error occurs while validating the signature or accessing tenant data. + */ + public static boolean validateSignatureFromTenant(String data, byte[] signature, String tenantDomain, + String context) throws SignatureException { + + // Retrieve tenant ID based on the tenant domain + int tenantId = IdentityTenantUtil.getTenantId(tenantDomain); + try { + // Initialize the tenant's registry + IdentityTenantUtil.initializeRegistry(tenantId); + + // Retrieve the tenant's public key + PublicKey publicKey; + if (StringUtils.isBlank(context)) { + // Fetch certificate without context if context is null or blank + publicKey = IdentityKeyStoreResolver.getInstance() + .getCertificate(tenantDomain, null) + .getPublicKey(); + } else { + try { + // Fetch certificate within the provided context + Certificate certificate = IdentityKeyStoreResolver.getInstance() + .getCertificate(tenantDomain, null, context); + publicKey = certificate.getPublicKey(); + } catch (IdentityKeyStoreResolverException e) { + if (ERROR_RETRIEVING_TENANT_CONTEXT_PUBLIC_CERTIFICATE_KEYSTORE_NOT_EXIST.getCode() + .equals(e.getErrorCode())) { + // Context keystore not exits, hence return validation as false. + return false; + } else { + throw new SignatureException("Error while validating the signature for tenant: " + + tenantDomain, e); + } + } + } + + // Validate the signature using the retrieved public key + return SignatureUtil.validateSignature(data, signature, publicKey); + } catch (IdentityException e) { + // Log and throw an exception if an error occurs + throw new SignatureException("Error while validating the signature for tenant: " + tenantDomain, e); + } + } + /** * Validates the signature of the given data for the specified tenant domain. * @@ -1981,48 +2037,83 @@ public static boolean isSCIM2UserMaxItemsPerPageEnabled() { public static boolean validateSignatureFromTenant(String data, byte[] signature, String tenantDomain) throws SignatureException { - int tenantId = IdentityTenantUtil.getTenantId(tenantDomain); - try { - IdentityTenantUtil.initializeRegistry(tenantId); - PublicKey publicKey = IdentityKeyStoreResolver.getInstance().getCertificate(tenantDomain, null) - .getPublicKey(); - return SignatureUtil.validateSignature(data, signature, publicKey); - } catch (IdentityException e) { - throw new SignatureException("Error while validating the signature from tenant: " + tenantDomain, e); - } + return validateSignatureFromTenant(data, signature, tenantDomain, null); } /** - * Sign the given data for the specified tenant domain. + * Signs the given data using the private key of the specified tenant. + * + * For super tenant domains, the default private key is used. For other tenants, the method retrieves the private + * key from the tenant's keystore. If a context is provided, it will attempt to retrieve the private key associated + * with that context. * * @param data The data to be signed. - * @param tenantDomain The tenant domain to which the data belongs. - * @return The signature of the data. - * @throws SignatureException If an error occurs during the signature generation process. + * @param tenantDomain The domain name of the tenant whose private key will be used for signing. + * @param context The optional context for retrieving the tenant's private key (can be null or blank). + * @return A byte array containing the signature for the provided data. + * @throws SignatureException If an error occurs while retrieving the private key or signing the data. */ - public static byte[] signWithTenantKey(String data, String tenantDomain) throws SignatureException { + public static byte[] signWithTenantKey(String data, String tenantDomain, String context) throws SignatureException { + // Get tenant ID from tenant domain int tenantId = IdentityTenantUtil.getTenantId(tenantDomain); KeyStoreManager keyStoreManager = KeyStoreManager.getInstance(tenantId); PrivateKey privateKey; if (MultitenantConstants.SUPER_TENANT_DOMAIN_NAME.equals(tenantDomain)) { try { - privateKey = keyStoreManager.getDefaultPrivateKey(); + String tenantKeyStoreName = IdentityKeyStoreResolverUtil.buildTenantKeyStoreName(tenantDomain, context); + // Retrieve private key from the tenant's keystore + if (StringUtils.isBlank(context)) { + // Retrieve default private key for the super tenant + privateKey = keyStoreManager.getDefaultPrivateKey(); + } else { + privateKey = (PrivateKey) keyStoreManager.getPrivateKey(tenantKeyStoreName, + tenantDomain + + IdentityKeyStoreResolverConstants.KEY_STORE_CONTEXT_SEPARATOR + context); + } + } catch (Exception e) { - throw new SignatureException(String.format(IdentityKeyStoreResolverConstants.ErrorMessages - .ERROR_CODE_ERROR_RETRIEVING_TENANT_PRIVATE_KEY.getDescription(), tenantDomain), - e); + throw new SignatureException(String.format( + IdentityKeyStoreResolverConstants.ErrorMessages.ERROR_CODE_ERROR_RETRIEVING_TENANT_PRIVATE_KEY + .getDescription(), + tenantDomain), e); } } else { try { - String tenantKeyStoreName = IdentityKeyStoreResolverUtil.buildTenantKeyStoreName(tenantDomain); + // Build tenant keystore name + String tenantKeyStoreName = IdentityKeyStoreResolverUtil.buildTenantKeyStoreName(tenantDomain, context); + + // Initialize the tenant's registry IdentityTenantUtil.initializeRegistry(tenantId); - privateKey = (PrivateKey) keyStoreManager.getPrivateKey(tenantKeyStoreName, tenantDomain); + + // Retrieve private key from the tenant's keystore + if (StringUtils.isBlank(context)) { + privateKey = (PrivateKey) keyStoreManager.getPrivateKey(tenantKeyStoreName, tenantDomain); + } else { + privateKey = (PrivateKey) keyStoreManager.getPrivateKey(tenantKeyStoreName, + tenantDomain + + IdentityKeyStoreResolverConstants.KEY_STORE_CONTEXT_SEPARATOR + context); + } } catch (IdentityException e) { - throw new SignatureException("Error while signing from private key of tenant: " + tenantDomain, e); + throw new SignatureException("Error while retrieving the private key for tenant: " + tenantDomain, e); } } + + // Sign the data with the retrieved private key return SignatureUtil.doSignature(data, privateKey); } + + /** + * Sign the given data for the specified tenant domain. + * + * @param data The data to be signed. + * @param tenantDomain The tenant domain to which the data belongs. + * @return The signature of the data. + * @throws SignatureException If an error occurs during the signature generation process. + */ + public static byte[] signWithTenantKey(String data, String tenantDomain) throws SignatureException { + + return signWithTenantKey(data, tenantDomain, null); + } } diff --git a/components/identity-core/org.wso2.carbon.identity.core/src/test/java/org/wso2/carbon/identity/core/IdentityKeyStoreResolverTest.java b/components/identity-core/org.wso2.carbon.identity.core/src/test/java/org/wso2/carbon/identity/core/IdentityKeyStoreResolverTest.java index 52abb6a18591..4246e1eaaf14 100644 --- a/components/identity-core/org.wso2.carbon.identity.core/src/test/java/org/wso2/carbon/identity/core/IdentityKeyStoreResolverTest.java +++ b/components/identity-core/org.wso2.carbon.identity.core/src/test/java/org/wso2/carbon/identity/core/IdentityKeyStoreResolverTest.java @@ -30,6 +30,7 @@ import org.wso2.carbon.identity.core.util.IdentityConfigParser; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; import org.wso2.carbon.utils.multitenancy.MultitenantConstants; +import org.wso2.carbon.utils.security.KeystoreUtils; import java.io.FileInputStream; import java.lang.reflect.Field; @@ -84,6 +85,7 @@ public class IdentityKeyStoreResolverTest extends TestCase { private MockedStatic identityConfigParser; private MockedStatic identityTenantUtil; + private MockedStatic keystoreUtils; private IdentityKeyStoreResolver identityKeyStoreResolver; @@ -143,6 +145,7 @@ public void setUp() throws Exception { when(keyStoreManager.getCertificate("CUSTOM/" + CUSTOM_KEY_STORE, null)).thenReturn(customCertificate); identityKeyStoreResolver = IdentityKeyStoreResolver.getInstance(); + keystoreUtils = mockStatic(KeystoreUtils.class); } @AfterClass @@ -150,6 +153,7 @@ public void close() { identityConfigParser.close(); identityTenantUtil.close(); + keystoreUtils.close(); } @Test @@ -210,6 +214,7 @@ public Object[][] keyStoreDataProvider() { @Test(dataProvider = "KeyStoreDataProvider") public void testGetKeyStore(String tenantDomain, InboundProtocol inboundProtocol, KeyStore expectedKeyStore) throws Exception { + keystoreUtils.when(() -> KeystoreUtils.getKeyStoreFileExtension(tenantDomain)).thenReturn(".jks"); assertEquals(expectedKeyStore, identityKeyStoreResolver.getKeyStore(tenantDomain, inboundProtocol)); } @@ -229,6 +234,7 @@ public Object[][] privateKeyDataProvider() { @Test(dataProvider = "PrivateKeyDataProvider") public void testGetPrivateKey(String tenantDomain, InboundProtocol inboundProtocol, PrivateKey expectedKey) throws Exception { + keystoreUtils.when(() -> KeystoreUtils.getKeyStoreFileExtension(tenantDomain)).thenReturn(".jks"); assertEquals(expectedKey, identityKeyStoreResolver.getPrivateKey(tenantDomain, inboundProtocol)); } @@ -248,6 +254,7 @@ public Object[][] publicCertificateDataProvider() { @Test(dataProvider = "PublicCertificateDataProvider") public void testGetCertificate(String tenantDomain, InboundProtocol inboundProtocol, X509Certificate expectedCert) throws Exception { + keystoreUtils.when(() -> KeystoreUtils.getKeyStoreFileExtension(tenantDomain)).thenReturn(".jks"); assertEquals(expectedCert, identityKeyStoreResolver.getCertificate(tenantDomain, inboundProtocol)); } diff --git a/components/identity-core/org.wso2.carbon.identity.core/src/test/java/org/wso2/carbon/identity/core/util/IdentityKeyStoreResolverUtilTest.java b/components/identity-core/org.wso2.carbon.identity.core/src/test/java/org/wso2/carbon/identity/core/util/IdentityKeyStoreResolverUtilTest.java index 0a3d7eb3faf1..98d094f26871 100644 --- a/components/identity-core/org.wso2.carbon.identity.core/src/test/java/org/wso2/carbon/identity/core/util/IdentityKeyStoreResolverUtilTest.java +++ b/components/identity-core/org.wso2.carbon.identity.core/src/test/java/org/wso2/carbon/identity/core/util/IdentityKeyStoreResolverUtilTest.java @@ -18,9 +18,14 @@ package org.wso2.carbon.identity.core.util; +import org.mockito.MockedStatic; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import org.wso2.carbon.utils.security.KeystoreUtils; +import static org.mockito.Mockito.mockStatic; import static org.testng.Assert.assertEquals; import static org.wso2.carbon.identity.core.util.IdentityKeyStoreResolverUtil.buildCustomKeyStoreName; @@ -34,6 +39,14 @@ */ public class IdentityKeyStoreResolverUtilTest { + private MockedStatic keystoreUtils; + + @BeforeClass + public void setUp() throws Exception { + + keystoreUtils = mockStatic(KeystoreUtils.class); + } + @DataProvider(name = "CorrectTenantKeyStoreNameDataProvider") public Object[][] correctTenantKeyStoreNameDataProvider() { @@ -43,10 +56,17 @@ public Object[][] correctTenantKeyStoreNameDataProvider() { }; } + @AfterClass + public void close() { + + keystoreUtils.close(); + } + @Test(dataProvider = "CorrectTenantKeyStoreNameDataProvider") public void testCorrectBuildTenantKeyStoreName(String tenantDomain, String expectedResult) throws IdentityKeyStoreResolverException { - assertEquals(expectedResult, buildTenantKeyStoreName(tenantDomain)); + keystoreUtils.when(() -> KeystoreUtils.getKeyStoreFileExtension(tenantDomain)).thenReturn(".jks"); + assertEquals(buildTenantKeyStoreName(tenantDomain), expectedResult); } @DataProvider(name = "IncorrectTenantKeyStoreNameDataProvider") diff --git a/components/identity-core/org.wso2.carbon.identity.core/src/test/java/org/wso2/carbon/identity/core/util/IdentityUtilTest.java b/components/identity-core/org.wso2.carbon.identity.core/src/test/java/org/wso2/carbon/identity/core/util/IdentityUtilTest.java index 4aeb8af2cf83..cd833299e972 100644 --- a/components/identity-core/org.wso2.carbon.identity.core/src/test/java/org/wso2/carbon/identity/core/util/IdentityUtilTest.java +++ b/components/identity-core/org.wso2.carbon.identity.core/src/test/java/org/wso2/carbon/identity/core/util/IdentityUtilTest.java @@ -56,6 +56,7 @@ import org.wso2.carbon.utils.CarbonUtils; import org.wso2.carbon.utils.ConfigurationContextService; import org.wso2.carbon.utils.NetworkUtils; +import org.wso2.carbon.utils.security.KeystoreUtils; import java.io.FileInputStream; import java.io.IOException; @@ -96,6 +97,8 @@ import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; +import static org.wso2.carbon.identity.core.util.IdentityKeyStoreResolverConstants.ErrorMessages.ERROR_CODE_ERROR_RETRIEVING_TENANT_PUBLIC_CERTIFICATE; +import static org.wso2.carbon.identity.core.util.IdentityKeyStoreResolverConstants.ErrorMessages.ERROR_RETRIEVING_TENANT_CONTEXT_PUBLIC_CERTIFICATE_KEYSTORE_NOT_EXIST; @Listeners(MockitoTestNGListener.class) public class IdentityUtilTest { @@ -150,6 +153,7 @@ public class IdentityUtilTest { MockedStatic signatureUtil; MockedStatic identityKeyStoreResolver; MockedStatic keyStoreManager; + private MockedStatic keystoreUtils; @BeforeMethod @@ -164,6 +168,7 @@ public void setUp() throws Exception { signatureUtil = mockStatic(SignatureUtil.class); identityKeyStoreResolver = mockStatic(IdentityKeyStoreResolver.class); keyStoreManager = mockStatic(KeyStoreManager.class); + keystoreUtils = mockStatic(KeystoreUtils.class); serverConfiguration.when(ServerConfiguration::getInstance).thenReturn(mockServerConfiguration); identityCoreServiceComponent.when( @@ -203,6 +208,7 @@ public void tearDown() throws Exception { signatureUtil.close(); identityKeyStoreResolver.close(); keyStoreManager.close(); + keystoreUtils.close(); } @Test(description = "Test converting a certificate to PEM format") @@ -1110,12 +1116,69 @@ public void testValidateSignatureFromTenant() throws Exception { assertTrue(result); } + @Test + public void testValidateSignatureFromContextKeystore() throws Exception { + + String data = "testData"; + byte[] signature = new byte[]{1, 2, 3}; + String tenantDomain = "carbon.super"; + String context = "cookie"; + + when(mockCertificate.getPublicKey()).thenReturn(mockPublicKey); + identityKeyStoreResolver.when(IdentityKeyStoreResolver::getInstance).thenReturn(mockIdentityKeyStoreResolver); + when(mockIdentityKeyStoreResolver.getCertificate(tenantDomain, null, context)).thenReturn(mockCertificate); + signatureUtil.when(() -> SignatureUtil.validateSignature(data, signature, mockPublicKey)).thenReturn(true); + + boolean result = IdentityUtil.validateSignatureFromTenant(data, signature, tenantDomain, context); + assertTrue(result); + } + + @Test(description = "Validate signature when the context keystore does not exist. " + + "Expect the method to return false without throwing an exception.") + public void testValidateSignatureFromContextKeystoreIfNotExists() throws Exception { + + String data = "testData"; + byte[] signature = new byte[]{1, 2, 3}; + String tenantDomain = "carbon.super"; + String context = "cookie"; + + identityKeyStoreResolver.when(IdentityKeyStoreResolver::getInstance).thenReturn(mockIdentityKeyStoreResolver); + when(mockIdentityKeyStoreResolver.getCertificate(tenantDomain, null, context)) + .thenThrow(new IdentityKeyStoreResolverException + (ERROR_RETRIEVING_TENANT_CONTEXT_PUBLIC_CERTIFICATE_KEYSTORE_NOT_EXIST.getCode(), + ERROR_RETRIEVING_TENANT_CONTEXT_PUBLIC_CERTIFICATE_KEYSTORE_NOT_EXIST.getDescription())); + signatureUtil.when(() -> SignatureUtil.validateSignature(data, signature, mockPublicKey)).thenReturn(true); + + boolean result = IdentityUtil.validateSignatureFromTenant(data, signature, tenantDomain, context); + assertFalse(result); + } + + @Test(description = "Validate signature when an unexpected exception occurs while retrieving the " + + "tenant's public certificate. Expect a SignatureException to be thrown.", + expectedExceptions = SignatureException.class) + public void testValidateSignatureFromContextKeystoreNegative() throws Exception { + + String data = "testData"; + byte[] signature = new byte[]{1, 2, 3}; + String tenantDomain = "carbon.super"; + String context = "cookie"; + + identityKeyStoreResolver.when(IdentityKeyStoreResolver::getInstance).thenReturn(mockIdentityKeyStoreResolver); + when(mockIdentityKeyStoreResolver.getCertificate(tenantDomain, null, context)) + .thenThrow(new IdentityKeyStoreResolverException + (ERROR_CODE_ERROR_RETRIEVING_TENANT_PUBLIC_CERTIFICATE.getCode(), + ERROR_CODE_ERROR_RETRIEVING_TENANT_PUBLIC_CERTIFICATE.getDescription())); + + IdentityUtil.validateSignatureFromTenant(data, signature, tenantDomain, context); + } + @Test public void testSignWithTenantKey() throws Exception { String data = "testData"; String superTenantDomain = "carbon.super"; keyStoreManager.when(() -> KeyStoreManager.getInstance(anyInt())).thenReturn(mockKeyStoreManager); + keystoreUtils.when(() -> KeystoreUtils.getKeyStoreFileExtension(superTenantDomain)).thenReturn(".jks"); when(mockKeyStoreManager.getDefaultPrivateKey()).thenReturn(mockPrivateKey); when(mockKeyStoreManager.getPrivateKey(anyString(), anyString())).thenReturn(mockPrivateKey); diff --git a/components/security-mgt/org.wso2.carbon.security.mgt/pom.xml b/components/security-mgt/org.wso2.carbon.security.mgt/pom.xml index 2933841a8f9e..45674b4f0bfe 100644 --- a/components/security-mgt/org.wso2.carbon.security.mgt/pom.xml +++ b/components/security-mgt/org.wso2.carbon.security.mgt/pom.xml @@ -106,6 +106,14 @@ org.wso2.orbit.javax.xml.bind jaxb-api + + org.wso2.orbit.org.bouncycastle + bcpkix-jdk18on + + + org.wso2.orbit.org.bouncycastle + bcprov-jdk18on + @@ -156,7 +164,8 @@ org.wso2.carbon.registry.core.*;version="${carbon.kernel.registry.imp.pkg.version}", org.wso2.carbon.registry.api;version="${carbon.kernel.registry.imp.pkg.version}", org.wso2.carbon.identity.core.*; version="${carbon.identity.package.import.version.range}", - org.wso2.carbon.identity.base; version="${carbon.identity.package.import.version.range}" + org.wso2.carbon.identity.base; version="${carbon.identity.package.import.version.range}", + org.bouncycastle.*;version="${org.bouncycastle.imp.pkg.version.range}", !org.wso2.carbon.security.internal, diff --git a/components/security-mgt/org.wso2.carbon.security.mgt/src/main/java/org/wso2/carbon/security/SecurityConstants.java b/components/security-mgt/org.wso2.carbon.security.mgt/src/main/java/org/wso2/carbon/security/SecurityConstants.java index 4f50c249076c..a5da81df780a 100644 --- a/components/security-mgt/org.wso2.carbon.security.mgt/src/main/java/org/wso2/carbon/security/SecurityConstants.java +++ b/components/security-mgt/org.wso2.carbon.security.mgt/src/main/java/org/wso2/carbon/security/SecurityConstants.java @@ -131,7 +131,7 @@ public static class KeyStoreMgtConstants { public static final String FILTER_OPERATION_CONTAINS = "co"; public static final String SERVER_TRUSTSTORE_FILE = "Security.TrustStore.Location"; - + public static final String KEY_STORE_CONTEXT_SEPARATOR = "--"; /** * Enum for Keystore management service related errors. */ diff --git a/components/security-mgt/org.wso2.carbon.security.mgt/src/main/java/org/wso2/carbon/security/internal/SecurityMgtServiceComponent.java b/components/security-mgt/org.wso2.carbon.security.mgt/src/main/java/org/wso2/carbon/security/internal/SecurityMgtServiceComponent.java index fe1cea36fcaa..638d41d6d4af 100644 --- a/components/security-mgt/org.wso2.carbon.security.mgt/src/main/java/org/wso2/carbon/security/internal/SecurityMgtServiceComponent.java +++ b/components/security-mgt/org.wso2.carbon.security.mgt/src/main/java/org/wso2/carbon/security/internal/SecurityMgtServiceComponent.java @@ -42,6 +42,8 @@ import org.wso2.carbon.security.SecurityServiceHolder; import org.wso2.carbon.security.keystore.KeyStoreManagementService; import org.wso2.carbon.security.keystore.KeyStoreManagementServiceImpl; +import org.wso2.carbon.security.keystore.service.IdentityKeyStoreGenerator; +import org.wso2.carbon.security.keystore.service.IdentityKeyStoreGeneratorImpl; import org.wso2.carbon.user.core.service.RealmService; import org.wso2.carbon.utils.ConfigurationContextService; @@ -66,6 +68,8 @@ protected void activate(ComponentContext ctxt) { BundleContext bundleCtx = ctxt.getBundleContext(); bundleCtx.registerService(KeyStoreManagementService.class.getName(), new KeyStoreManagementServiceImpl(), null); + bundleCtx.registerService(IdentityKeyStoreGenerator.class.getName(), new IdentityKeyStoreGeneratorImpl(), + null); try { addKeystores(); } catch (Exception e) { diff --git a/components/security-mgt/org.wso2.carbon.security.mgt/src/main/java/org/wso2/carbon/security/keystore/service/IdentityKeyStoreGenerator.java b/components/security-mgt/org.wso2.carbon.security.mgt/src/main/java/org/wso2/carbon/security/keystore/service/IdentityKeyStoreGenerator.java new file mode 100644 index 000000000000..4cc380c1462e --- /dev/null +++ b/components/security-mgt/org.wso2.carbon.security.mgt/src/main/java/org/wso2/carbon/security/keystore/service/IdentityKeyStoreGenerator.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you 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.wso2.carbon.security.keystore.service; + +import org.wso2.carbon.security.keystore.KeyStoreManagementException; + +/** + * Interface for generating and managing context-specific tenant key stores. + */ +public interface IdentityKeyStoreGenerator { + + /** + * Generates a context-specific KeyStore for a given tenant domain. + *

+ * This method creates a new KeyStore for the specified tenant domain and context if it does not already exist. + *

+ * + * @param tenantDomain the tenant domain for which the KeyStore is to be created. + * @param context the context for which the KeyStore is to be generated. + * @throws KeyStoreManagementException if an error occurs during KeyStore creation or initialization. + */ + void generateKeyStore(String tenantDomain, String context) throws KeyStoreManagementException; +} diff --git a/components/security-mgt/org.wso2.carbon.security.mgt/src/main/java/org/wso2/carbon/security/keystore/service/IdentityKeyStoreGeneratorImpl.java b/components/security-mgt/org.wso2.carbon.security.mgt/src/main/java/org/wso2/carbon/security/keystore/service/IdentityKeyStoreGeneratorImpl.java new file mode 100644 index 000000000000..22f75c7e6905 --- /dev/null +++ b/components/security-mgt/org.wso2.carbon.security.mgt/src/main/java/org/wso2/carbon/security/keystore/service/IdentityKeyStoreGeneratorImpl.java @@ -0,0 +1,314 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you 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.wso2.carbon.security.keystore.service; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.wso2.carbon.base.ServerConfiguration; +import org.wso2.carbon.core.util.CryptoUtil; +import org.wso2.carbon.core.util.KeyStoreManager; +import org.wso2.carbon.identity.core.util.IdentityKeyStoreResolverConstants; +import org.wso2.carbon.identity.core.util.IdentityKeyStoreResolverException; +import org.wso2.carbon.identity.core.util.IdentityTenantUtil; +import org.wso2.carbon.security.keystore.KeyStoreManagementException; +import org.wso2.carbon.utils.ServerConstants; +import org.wso2.carbon.utils.security.KeystoreUtils; + +import java.io.ByteArrayOutputStream; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.util.Date; + +import static org.wso2.carbon.security.SecurityConstants.KeyStoreMgtConstants.KEY_STORE_CONTEXT_SEPARATOR; + +/** + * Implementation of the {@link IdentityKeyStoreGenerator} interface for generating and managing + * context-specific tenant key stores. This class provides functionality to create, manage, and store + * keystores in a multi-tenant environment, adhering to specific cryptographic requirements. + * + *

Key Features:

+ *
    + *
  • Generates keystores for tenants dynamically.
  • + *
  • Supports various cryptographic algorithms and key generation techniques.
  • + *
  • Handles secure persistence of keystores using {@link KeyStoreManager}.
  • + *
  • Provides explainable methods for certificate creation, storage, and retrieval.
  • + *
+ * + *

Usage:

+ * This class is intended to be used in environments where context-specific cryptographic needs + * must be met dynamically. + * + *

Exceptions:

+ * The methods in this class throw {@link KeyStoreManagementException} for errors encountered during + * keystore creation, management, or persistence. + */ +public class IdentityKeyStoreGeneratorImpl implements IdentityKeyStoreGenerator { + + private static final Log LOG = LogFactory.getLog(IdentityKeyStoreGeneratorImpl.class); + private static final String SIGNING_ALG = "Tenant.SigningAlgorithm"; + + // Supported signature algorithms for public certificate generation. + private static final String RSA_MD5 = "MD5withRSA"; + private static final String RSA_SHA1 = "SHA1withRSA"; + private static final String RSA_SHA256 = "SHA256withRSA"; + private static final String RSA_SHA384 = "SHA384withRSA"; + private static final String RSA_SHA512 = "SHA512withRSA"; + private static final String[] signatureAlgorithms = new String[]{ + RSA_MD5, RSA_SHA1, RSA_SHA256, RSA_SHA384, RSA_SHA512 + }; + private static final long CERT_NOT_BEFORE_TIME = 1000L * 60 * 60 * 24 * 30; // 30 days in milliseconds + private static final long CERT_NOT_AFTER_TIME = 1000L * 60 * 60 * 24 * 365 * 10; // 10 years in milliseconds + + /** + * Generates a context-specific KeyStore for a given tenant domain if it does not already exist. + *

+ * This method checks whether a KeyStore exists for the specified tenant domain and context. + * If the KeyStore does not exist, it creates a new one, initializes it, generates the necessary + * key pairs, and persists it. + *

+ * + * @param tenantDomain the tenant domain for which the KeyStore is to be generated. + * @param context the specific context for which the KeyStore is to be generated. + * @throws KeyStoreManagementException if an error occurs during the KeyStore creation or initialization. + */ + public void generateKeyStore(String tenantDomain, String context) throws KeyStoreManagementException { + + int tenantId = IdentityTenantUtil.getTenantId(tenantDomain); + KeyStoreManager keyStoreManager = KeyStoreManager.getInstance(tenantId); + + try { + IdentityTenantUtil.initializeRegistry(tenantId); + if (isContextKeyStoreExists(context, tenantDomain, keyStoreManager)) { + return; // KeyStore already exists, no need to create again + } + // Create the KeyStore + String password = generatePassword(); + KeyStore keyStore = KeystoreUtils.getKeystoreInstance( + KeystoreUtils.getKeyStoreFileType(tenantDomain)); + keyStore.load(null, password.toCharArray()); + generateContextKeyPair(keyStore, context, tenantDomain, password); + persistContextKeyStore(keyStore, context, tenantDomain, password, keyStoreManager); + } catch (Exception e) { + String msg = "Error while instantiating a keystore"; + throw new KeyStoreManagementException(msg, e); + } + } + + private boolean isContextKeyStoreExists(String context, String tenantDomain, KeyStoreManager keyStoreManager) + throws KeyStoreManagementException { + + String keyStoreName = KeystoreUtils.getKeyStoreFileLocation(buildDomainWithContext(tenantDomain, context)); + boolean isKeyStoreExists = false; + try { + keyStoreManager.getKeyStore(keyStoreName); + isKeyStoreExists = true; + } catch (SecurityException e) { + if (e.getMessage() != null && e.getMessage().contains("Key Store with a name: " + keyStoreName + + " does not exist.")) { + + String msg = "Key store not exits. Proceeding to create keystore : " + keyStoreName; + LOG.debug(msg + e.getMessage()); + } else { + String msg = "Error while checking the existence of keystore."; + throw new KeyStoreManagementException(msg, e); + } + } catch (Exception e) { + String msg = "Error while checking the existence of keystore."; + throw new KeyStoreManagementException(msg, e); + } + return isKeyStoreExists; + } + + /** + * This method is used to generate a random password for the generated keystore + * + * @return generated password + */ + private String generatePassword() { + + SecureRandom random = new SecureRandom(); + String randString = new BigInteger(130, random).toString(12); + return randString.substring(randString.length() - 10); + } + + /** + * Persists a context-specific KeyStore for a given tenant domain. + *

+ * This method stores the provided KeyStore in a persistent storage using the {@code KeyStoreManager}. + * It generates a KeyStore name based on the tenant domain and context, converts the KeyStore + * into a byte array, and saves it securely along with the provided password. + *

+ * + * @param keyStore the KeyStore to be persisted. + * @param context the specific context for which the KeyStore is being persisted. + * @param tenantDomain the tenant domain associated with the KeyStore. + * @param password the password used to protect the KeyStore. + * @param keyStoreManager the {@code KeyStoreManager} instance responsible for managing the persistence of the KeyStore. + * @throws KeyStoreManagementException if an error occurs while persisting the KeyStore or if security issues arise. + */ + private void persistContextKeyStore(KeyStore keyStore, String context, String tenantDomain, String password, + KeyStoreManager keyStoreManager) throws KeyStoreManagementException { + + String keyStoreName = generateContextKSNameFromDomainName(context, tenantDomain); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + char[] passwordChar = password.toCharArray(); + try { + keyStore.store(outputStream, passwordChar); + outputStream.flush(); + outputStream.close(); + } catch (Exception e) { + String msg = "Error occurred while storing the keystore or processing the public certificate for tenant: " + + tenantDomain + " and context: " + context + ". Ensure the keystore is valid and writable."; + throw new KeyStoreManagementException(msg, e); + } + + try { + + keyStoreManager.addKeyStore(outputStream.toByteArray(), keyStoreName, + passwordChar, " ", KeystoreUtils.getKeyStoreFileType(tenantDomain), passwordChar); + } catch (SecurityException e) { + if (e.getMessage() != null && e.getMessage().contains("Key store " + keyStoreName + " already available")) { + + LOG.warn("Key store " + keyStoreName + " is already available, ignoring."); + } else { + + String msg = "Error when adding a keyStore"; + throw new KeyStoreManagementException(msg, e); + } + } + } + + /** + * This method generates the keypair and stores it in the keystore. + * + * @param keyStore the KeyStore to be persisted. + * @param context the specific context for which the KeyStore is being persisted. + * @param tenantDomain the tenant domain associated with the KeyStore. + * @param password the password used to protect the KeyStore. + * @throws KeyStoreManagementException Error when generating key pair + */ + private void generateContextKeyPair(KeyStore keyStore, String context, String tenantDomain, String password) + throws KeyStoreManagementException { + + try { + CryptoUtil.getDefaultCryptoUtil(); + // Generate key pair + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + + // Common Name and alias for the generated certificate + String commonName = "CN=" + buildDomainWithContext(tenantDomain, context) + + ", OU=None, O=None, L=None, C=None"; + + // Generate certificates + X500Name distinguishedName = new X500Name(commonName); + + Date notBefore = new Date(System.currentTimeMillis() - CERT_NOT_BEFORE_TIME); + Date notAfter = new Date(System.currentTimeMillis() + CERT_NOT_AFTER_TIME); + + SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()); + BigInteger serialNumber = BigInteger.valueOf(new SecureRandom().nextInt()); + + X509v3CertificateBuilder certificateBuilder = new X509v3CertificateBuilder( + distinguishedName, + serialNumber, + notBefore, + notAfter, + distinguishedName, + subPubKeyInfo + ); + + String algorithmName = getSignatureAlgorithm(); + JcaContentSignerBuilder signerBuilder = + new JcaContentSignerBuilder(algorithmName).setProvider(getJCEProvider()); + PrivateKey privateKey = keyPair.getPrivate(); + X509Certificate x509Cert = new JcaX509CertificateConverter().setProvider(getJCEProvider()) + .getCertificate(certificateBuilder.build(signerBuilder.build(privateKey))); + + // Add private key to KS + keyStore.setKeyEntry(buildDomainWithContext(tenantDomain, context), + keyPair.getPrivate(), password.toCharArray(), + new java.security.cert.Certificate[]{x509Cert}); + } catch (Exception ex) { + String msg = "Error while generating the Context certificate for tenant :" + + tenantDomain + "."; + throw new KeyStoreManagementException(msg, ex); + } + } + + private static String getSignatureAlgorithm() { + + String algorithm = ServerConfiguration.getInstance().getFirstProperty(SIGNING_ALG); + // Find in a list of supported signature algorithms. + for (String supportedAlgorithm : signatureAlgorithms) { + if (supportedAlgorithm.equalsIgnoreCase(algorithm)) { + return supportedAlgorithm; + } + } + return RSA_MD5; + } + + private static String getJCEProvider() { + + String provider = ServerConfiguration.getInstance().getFirstProperty(ServerConstants.JCE_PROVIDER); + if (!StringUtils.isBlank(provider)) { + return provider; + } + return ServerConstants.JCE_PROVIDER_BC; + } + + /** + * This method generates the key store file name from the Domain Name + * @return keystore name. + */ + private String generateContextKSNameFromDomainName(String context, String tenantDomain) + throws KeyStoreManagementException { + + String ksName = tenantDomain.trim().replace(".", "-"); + ksName = buildDomainWithContext(ksName, context); + return (ksName + KeystoreUtils.getKeyStoreFileExtension(tenantDomain)); + } + + /** + * Concatenates ksName and context with the separator. + * + * @param ksName the key store name + * @param context the context + * @return a concatenated string in the format ksName:context + */ + private String buildDomainWithContext(String ksName, String context) throws KeyStoreManagementException { + + if (ksName == null || context == null) { + throw new KeyStoreManagementException("ksName and context must not be null"); + } + return ksName + KEY_STORE_CONTEXT_SEPARATOR + context; + } +} diff --git a/components/security-mgt/org.wso2.carbon.security.mgt/src/test/java/org/wso2/carbon/security/keystore/service/IdentityKeyStoreGeneratorImplTest.java b/components/security-mgt/org.wso2.carbon.security.mgt/src/test/java/org/wso2/carbon/security/keystore/service/IdentityKeyStoreGeneratorImplTest.java new file mode 100644 index 000000000000..a8dfb803f402 --- /dev/null +++ b/components/security-mgt/org.wso2.carbon.security.mgt/src/test/java/org/wso2/carbon/security/keystore/service/IdentityKeyStoreGeneratorImplTest.java @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you 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.wso2.carbon.security.keystore.service; + +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.stubbing.Answer; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.*; +import org.wso2.carbon.base.CarbonBaseConstants; +import org.wso2.carbon.core.util.KeyStoreManager; +import org.wso2.carbon.identity.core.util.IdentityTenantUtil; +import org.wso2.carbon.identity.testutil.IdentityBaseTest; +import org.wso2.carbon.security.keystore.KeyStoreManagementException; +import org.wso2.carbon.utils.security.KeystoreUtils; + +import java.io.FileInputStream; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.Security; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@Listeners(MockitoTestNGListener.class) +public class IdentityKeyStoreGeneratorImplTest extends IdentityBaseTest { + + private static final String KEYSTORE_PASSWORD = "wso2carbon"; + + private IdentityKeyStoreGeneratorImpl identityKeyStoreGenerator; + + private MockedStatic identityTenantUtil; + @Mock + private KeyStoreManager keyStoreManager; + + @Mock + private KeyStore mockKeyStore; + + + @BeforeMethod + public void setUp() throws Exception { + + if (Security.getProvider("BC") == null) { + Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + } + + System.setProperty( + CarbonBaseConstants.CARBON_HOME, + Paths.get(System.getProperty("user.dir"), "src", "test", "resources").toString() + ); + identityTenantUtil = mockStatic(IdentityTenantUtil.class); + } + + @AfterMethod + public void tearDown() throws Exception { + + identityKeyStoreGenerator = null; + identityTenantUtil.close(); + } + + @Test(description = "Test the generation of a keystore for a given tenant domain and context if exits.") + public void testGenerateKeystoreIfExists() throws Exception { + + try (MockedStatic keyStoreManager = mockStatic(KeyStoreManager.class); + MockedStatic keyStoreUtils = mockStatic(KeystoreUtils.class)) { + + keyStoreManager.when(() -> KeyStoreManager.getInstance(anyInt())).thenReturn(this.keyStoreManager); + identityTenantUtil.when(()->IdentityTenantUtil.getTenantId("carbon.super")) + .thenReturn(-1234); + identityTenantUtil.when(() -> IdentityTenantUtil.initializeRegistry(anyInt())) + .thenAnswer((Answer) invocation -> null); + keyStoreUtils.when(() -> KeystoreUtils.getKeyStoreFileLocation("carbon.super--cookie")) + .thenReturn("carbon-super--cookie.jks"); + when(this.keyStoreManager.getKeyStore("carbon-super--cookie.jks")) + .thenReturn(getKeyStoreFromFile("carbon-super--cookie.jks", KEYSTORE_PASSWORD)); + identityKeyStoreGenerator = new IdentityKeyStoreGeneratorImpl(); + identityKeyStoreGenerator.generateKeyStore("carbon.super", "cookie"); + } + } + + /** + * Sets up the mock behavior for KeyStoreManager and KeystoreUtils. + * + * @param exceptionToThrow the exception to throw when `getKeyStore` is called. + * @throws Exception if any setup steps fail. + */ + private void setupKeyStoreMocksWithException(Exception exceptionToThrow) throws Exception { + try (MockedStatic keyStoreManager = mockStatic(KeyStoreManager.class); + MockedStatic keyStoreUtils = mockStatic(KeystoreUtils.class)) { + + keyStoreManager.when(() -> KeyStoreManager.getInstance(anyInt())).thenReturn(this.keyStoreManager); + identityTenantUtil.when(() -> IdentityTenantUtil.getTenantId("carbon.super")).thenReturn(-1234); + identityTenantUtil.when(() -> IdentityTenantUtil.initializeRegistry(anyInt())) + .thenAnswer((Answer) invocation -> null); + keyStoreUtils.when(() -> KeystoreUtils.getKeyStoreFileLocation("carbon.super--cookie")) + .thenReturn("wso2carbon--cookie.jks"); + + when(this.keyStoreManager.getKeyStore("wso2carbon--cookie.jks")).thenThrow(exceptionToThrow); + } + } + + @Test(description = "Test error creating a keystore for a given tenant domain and context with SecurityException.", + expectedExceptions = KeyStoreManagementException.class) + public void testGenerateKeystoreWithSecurityException() throws Exception { + + setupKeyStoreMocksWithException(new SecurityException("Error while creating keystore.")); + identityKeyStoreGenerator = new IdentityKeyStoreGeneratorImpl(); + identityKeyStoreGenerator.generateKeyStore("carbon.super", "cookie"); + } + + @Test(description = "Test error creating a keystore for a given tenant domain and context with generic Exception.", + expectedExceptions = KeyStoreManagementException.class) + public void testGenerateKeystoreWithGenericException() throws Exception { + + setupKeyStoreMocksWithException(new Exception("Error while creating keystore.")); + identityKeyStoreGenerator = new IdentityKeyStoreGeneratorImpl(); + identityKeyStoreGenerator.generateKeyStore("carbon.super", "cookie"); + } + + + @Test(description = "Test the generation of a keystore for a given tenant domain and context if not exits.") + public void testGenerateKeystoreIfNotExists() throws Exception { + + try (MockedStatic keyStoreManager = mockStatic(KeyStoreManager.class); + MockedStatic keyStoreUtils = mockStatic(KeystoreUtils.class)) { + + keyStoreManager.when(() -> KeyStoreManager.getInstance(anyInt())).thenReturn(this.keyStoreManager); + identityTenantUtil.when(()->IdentityTenantUtil.getTenantId("carbon.super")) + .thenReturn(-1234); + identityTenantUtil.when(() -> IdentityTenantUtil.initializeRegistry(anyInt())) + .thenAnswer((Answer) invocation -> null); + keyStoreUtils.when(() -> KeystoreUtils.getKeyStoreFileLocation("carbon.super--cookie")) + .thenReturn("carbon-super--cookie.jks"); + when(this.keyStoreManager.getKeyStore("carbon-super--cookie.jks")) + .thenThrow(new SecurityException("Key Store with a name: carbon-super--cookie.jks" + + " does not exist.")); + keyStoreUtils.when(() -> KeystoreUtils.getKeyStoreFileType("carbon.super")) + .thenReturn("JKS"); + keyStoreUtils.when(() -> KeystoreUtils.getKeystoreInstance("JKS")) + .thenReturn(this.mockKeyStore); + doNothing().when(this.mockKeyStore).setKeyEntry(anyString(), any(PrivateKey.class), any(), any()); + + identityKeyStoreGenerator = new IdentityKeyStoreGeneratorImpl(); + identityKeyStoreGenerator.generateKeyStore("carbon.super", "cookie"); + } + } + + @Test(description = "Test the generation of a keystore for a given tenant domain and context if not exits.") + public void testGenerateKeystoreAlreadyExists() throws Exception { + + try (MockedStatic keyStoreManager = mockStatic(KeyStoreManager.class); + MockedStatic keyStoreUtils = mockStatic(KeystoreUtils.class)) { + + keyStoreManager.when(() -> KeyStoreManager.getInstance(anyInt())).thenReturn(this.keyStoreManager); + identityTenantUtil.when(()->IdentityTenantUtil.getTenantId("carbon.super")) + .thenReturn(-1234); + identityTenantUtil.when(() -> IdentityTenantUtil.initializeRegistry(anyInt())) + .thenAnswer((Answer) invocation -> null); + keyStoreUtils.when(() -> KeystoreUtils.getKeyStoreFileLocation("carbon.super--cookie")) + .thenReturn("carbon-super--cookie.jks"); + when(this.keyStoreManager.getKeyStore("carbon-super--cookie.jks")) + .thenThrow(new SecurityException("Key Store with a name: carbon-super--cookie.jks" + + " does not exist.")); + keyStoreUtils.when(() -> KeystoreUtils.getKeyStoreFileType("carbon.super")) + .thenReturn("JKS"); + keyStoreUtils.when(() -> KeystoreUtils.getKeystoreInstance("JKS")) + .thenReturn(this.mockKeyStore); + doNothing().when(this.mockKeyStore).setKeyEntry(anyString(), any(PrivateKey.class), any(), any()); + keyStoreUtils.when(() -> KeystoreUtils.getKeyStoreFileExtension("carbon.super")) + .thenReturn(".jks"); + doThrow(new SecurityException("Key store carbon-super--cookie.jks already available")) + .when(this.keyStoreManager) + .addKeyStore( + any(byte[].class), // Match any byte array + anyString(), // Match any String + any(char[].class), // Match any char array + anyString(), // Match any String + anyString(), // Match any String + any(char[].class) // Match any char array + ); + + identityKeyStoreGenerator = new IdentityKeyStoreGeneratorImpl(); + identityKeyStoreGenerator.generateKeyStore("carbon.super", "cookie"); + } + } + + @Test(description = "Test the generation of a keystore for a given tenant domain and context if not exits.", + expectedExceptions = KeyStoreManagementException.class) + public void testGenerateKeystoreIfNotExistsNegative() throws Exception { + + try (MockedStatic keyStoreManager = mockStatic(KeyStoreManager.class); + MockedStatic keyStoreUtils = mockStatic(KeystoreUtils.class)) { + + keyStoreManager.when(() -> KeyStoreManager.getInstance(anyInt())).thenReturn(this.keyStoreManager); + identityTenantUtil.when(()->IdentityTenantUtil.getTenantId("carbon.super")) + .thenReturn(-1234); + identityTenantUtil.when(() -> IdentityTenantUtil.initializeRegistry(anyInt())) + .thenAnswer((Answer) invocation -> null); + keyStoreUtils.when(() -> KeystoreUtils.getKeyStoreFileLocation("carbon.super--cookie")) + .thenReturn("carbon-super--cookie.jks"); + when(this.keyStoreManager.getKeyStore("carbon-super--cookie.jks")) + .thenThrow(new SecurityException("Key Store with a name: carbon-super--cookie.jks" + + " does not exist.")); + keyStoreUtils.when(() -> KeystoreUtils.getKeyStoreFileType("carbon.super")) + .thenReturn("JKS"); + keyStoreUtils.when(() -> KeystoreUtils.getKeystoreInstance("JKS")) + .thenReturn(this.mockKeyStore); + doNothing().when(this.mockKeyStore).setKeyEntry(anyString(), any(PrivateKey.class), any(), any()); + keyStoreUtils.when(() -> KeystoreUtils.getKeyStoreFileExtension("carbon.super")) + .thenReturn(".jks"); + doThrow(new SecurityException("Error while adding keystore")) + .when(this.keyStoreManager) + .addKeyStore( + any(byte[].class), // Match any byte array + anyString(), // Match any String + any(char[].class), // Match any char array + anyString(), // Match any String + anyString(), // Match any String + any(char[].class) // Match any char array + ); + + identityKeyStoreGenerator = new IdentityKeyStoreGeneratorImpl(); + identityKeyStoreGenerator.generateKeyStore("carbon.super", "cookie"); + } + } + + + private Path createPath(String keystoreName) { + + return Paths.get(System.getProperty(CarbonBaseConstants.CARBON_HOME), "repository", + "resources", "security", keystoreName); + } + + private KeyStore getKeyStoreFromFile(String keystoreName, String password) throws Exception { + + Path tenantKeystorePath = createPath(keystoreName); + FileInputStream file = new FileInputStream(tenantKeystorePath.toString()); + KeyStore keystore = KeyStore.getInstance("JKS"); + keystore.load(file, password.toCharArray()); + return keystore; + } +} diff --git a/components/security-mgt/org.wso2.carbon.security.mgt/src/test/resources/repository/resources/security/carbon-super--cookie.jks b/components/security-mgt/org.wso2.carbon.security.mgt/src/test/resources/repository/resources/security/carbon-super--cookie.jks new file mode 100644 index 000000000000..d5af4f42973a Binary files /dev/null and b/components/security-mgt/org.wso2.carbon.security.mgt/src/test/resources/repository/resources/security/carbon-super--cookie.jks differ diff --git a/components/security-mgt/org.wso2.carbon.security.mgt/src/test/resources/testng.xml b/components/security-mgt/org.wso2.carbon.security.mgt/src/test/resources/testng.xml index ccc588beafc0..b74b69a95034 100644 --- a/components/security-mgt/org.wso2.carbon.security.mgt/src/test/resources/testng.xml +++ b/components/security-mgt/org.wso2.carbon.security.mgt/src/test/resources/testng.xml @@ -23,6 +23,7 @@ + diff --git a/pom.xml b/pom.xml index ffac22766008..3db549b9a56b 100644 --- a/pom.xml +++ b/pom.xml @@ -922,6 +922,17 @@ ${version.javax.servlet} + + org.wso2.orbit.org.bouncycastle + bcprov-jdk18on + ${bcprov-jdk18.version} + + + org.wso2.orbit.org.bouncycastle + bcpkix-jdk18on + ${bcpkix-jdk18.version} + + org.opensaml @@ -1840,6 +1851,7 @@ ${project.version} [5.14.0, 8.0.0) + [0.0.0,2.0.0) 1.0.90 [1.0.0, 2.0.0) @@ -1950,6 +1962,9 @@ 1.4.1 [1.4.0,1.5.0) + 1.78.1.wso2v1 + 1.78.1.wso2v1 + 1.8.0 3.2.2.wso2v1