diff --git a/jsign-core/src/main/java/net/jsign/jca/OracleCloudSigningService.java b/jsign-core/src/main/java/net/jsign/jca/OracleCloudSigningService.java index ad5693e2..23e8b234 100644 --- a/jsign-core/src/main/java/net/jsign/jca/OracleCloudSigningService.java +++ b/jsign-core/src/main/java/net/jsign/jca/OracleCloudSigningService.java @@ -87,13 +87,17 @@ public String getName() { return "OracleCloud"; } + String getManagementEndpoint() { + return "https://kms." + credentials.getRegion() + ".oraclecloud.com"; + } + @Override public List aliases() throws KeyStoreException { List aliases = new ArrayList<>(); try { // VaultSummary/ListVaults (https://docs.oracle.com/en-us/iaas/api/#/en/key/release/VaultSummary/ListVaults) - RESTClient kmsClient = new RESTClient("https://kms." + credentials.getRegion() + ".oraclecloud.com", this::sign); + RESTClient kmsClient = new RESTClient(getManagementEndpoint(), this::sign); Map result = kmsClient.get("/20180608/vaults?compartmentId=" + credentials.getTenancy()); Object[] vaults = (Object[]) result.get("result"); for (Object v : vaults) { @@ -160,7 +164,7 @@ public byte[] sign(SigningServicePrivateKey privateKey, String algorithm, byte[] } } - private String getKeyEndpoint(String keyId) { + String getKeyEndpoint(String keyId) { // extract the vault from the key id Pattern pattern = Pattern.compile("ocid1\\.key\\.oc1\\.([^.]*)\\.([^.]*)\\..*"); Matcher matcher = pattern.matcher(keyId); diff --git a/jsign-core/src/test/java/net/jsign/jca/OracleCloudSigningServiceTest.java b/jsign-core/src/test/java/net/jsign/jca/OracleCloudSigningServiceTest.java new file mode 100644 index 00000000..d8ef79e9 --- /dev/null +++ b/jsign-core/src/test/java/net/jsign/jca/OracleCloudSigningServiceTest.java @@ -0,0 +1,149 @@ +/** + * Copyright 2024 Emmanuel Bourg + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.jsign.jca; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collection; +import java.util.List; + +import org.apache.commons.io.FileUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static net.jadler.Jadler.*; +import static org.junit.Assert.*; + +public class OracleCloudSigningServiceTest { + + @Before + public void setUp() { + initJadlerListeningOn(18080).withDefaultResponseStatus(404); + } + + @After + public void tearDown() { + closeJadler(); + } + + private OracleCloudCredentials getCredentials() throws Exception { + File config = File.createTempFile("ociconfig", null); + FileUtils.writeStringToFile(config, "[DEFAULT]\n" + + "user=ocid1.user.oc1..abcdefghijk\n" + + "tenancy=ocid1.tenancy.oc1..abcdefghijk\n" + + "region=eu-paris-1\n" + + "key_file=src/test/resources/keystores/privatekey.pkcs8.pem\n" + + "fingerprint=97:a2:2f:f5:e8:39:d3:44:b7:63:f2:4e:31:18:a6:62\n" + + "pass_phrase=password\n", "UTF-8"); + + OracleCloudCredentials credentials = new OracleCloudCredentials(); + credentials.load(config, "DEFAULT"); + return credentials; + } + + private SigningService getTestService() throws Exception { + return new OracleCloudSigningService(getCredentials(), alias -> { + try { + try (FileInputStream in = new FileInputStream("src/test/resources/keystores/jsign-test-certificate-full-chain.pem")) { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + Collection certificates = certificateFactory.generateCertificates(in); + return certificates.toArray(new Certificate[0]); + } + } catch (IOException | CertificateException e) { + throw new RuntimeException(e); + } + }) { + @Override + String getManagementEndpoint() { + return "http://localhost:" + port(); + } + + @Override + String getKeyEndpoint(String keyId) { + return "http://localhost:" + port(); + } + }; + } + + @Test + public void testGetAliases() throws Exception { + onRequest() + .havingMethodEqualTo("GET") + .havingPathEqualTo("/20180608/vaults") + .havingQueryStringEqualTo("compartmentId=ocid1.tenancy.oc1..abcdefghijk") + .respond() + .withStatus(200) + .withContentType("application/json") + .withBody(new FileInputStream("target/test-classes/services/oraclecloud-listvaults.json")); + onRequest() + .havingMethodEqualTo("GET") + .havingPathEqualTo("/20180608/keys") + .havingQueryStringEqualTo("compartmentId=ocid1.tenancy.oc1..abcdefghijk") + .respond() + .withStatus(200) + .withContentType("application/json") + .withBody(new FileInputStream("target/test-classes/services/oraclecloud-listkeys.json")); + + SigningService service = getTestService(); + List aliases = service.aliases(); + + assertEquals("aliases", Arrays.asList("ocid1.key.oc1.eu-paris-1.h5tafwboaahxq.abrwiljrwkhgllb5zfqchmvdkmqnzutqeq5pz7yo6z7yhl2zyn2yncwzxiza", + "ocid1.key.oc1.eu-paris-1.h5tafwboaahxq.abrwiljr7tub2mmyv5x2w6hwdlbpa3l567vih67yypquqkm4pjgk4cx7rkpa"), aliases); + } + + @Test + public void testSign() throws Exception { + onRequest() + .havingMethodEqualTo("POST") + .havingPathEqualTo("/20180608/sign") + .respond() + .withStatus(200) + .withContentType("application/json") + .withBody(new FileInputStream("target/test-classes/services/oraclecloud-sign.json")); + SigningService service = getTestService(); + SigningServicePrivateKey privateKey = service.getPrivateKey("ocid1.key.oc1.eu-paris-1.h5tafwboaahxq.abrwiljrwkhgllb5zfqchmvdkmqnzutqeq5pz7yo6z7yhl2zyn2yncwzxiza", null); + String signature = Base64.getEncoder().encodeToString(service.sign(privateKey, "SHA256withRSA", "Hello".getBytes())); + assertEquals("signature", "MiZ/YXfluqyuMfR3cnChG7+K7JmU2b8SzBAc6+WOpWQwIV4GfkLcRe0A68H45Lf+XPiMPPLrs7EqOv1EAnkYDFx5AqZBTWBfoaBeqKpy30OBvNbxIsaTLsaJYGypwmHOUTP+Djz7FxQUyM0uWVfUnHUDT564gQLz0cta6PKE/oMUo9fZhpv5VQcgfrbdUlPaD/cSAOb833ZSRzPWbnqztWO6py5sUugvqGFHKhsEXesx5yrPvJTKu5HVF3QM3E8YrgnVfFK14W8oyTJmXIWQxfYpwm/CW037UmolDMqwc3mjx1758kR+9lOcf8c/LSmD/SVD18SDSK4FyLQWOmn16A==", signature); + } + + @Test + public void testSignWithInvalidKey() throws Exception { + onRequest() + .havingMethodEqualTo("POST") + .havingPathEqualTo("/20180608/sign") + .respond() + .withStatus(404) + .withContentType("application/json") + .withBody(new FileInputStream("target/test-classes/services/oraclecloud-error.json")); + SigningService service = getTestService(); + SigningServicePrivateKey privateKey = service.getPrivateKey("ocid1.key.oc1.eu-paris-2.h5tafwboaahxq.abrwiljrwkhgllb5zfqchmvdkmqnzutqeq5pz7yo6z7yhl2zyn2yncwzxiza", null); + try { + service.sign(privateKey, "SHA256withRSA", "Hello".getBytes()); + fail("No exception thrown"); + } catch (GeneralSecurityException e) { + assertEquals("message", "NotAuthorizedOrNotFound: resource does not exist or you are not authorized to access it.", e.getCause().getMessage()); + } + } +} diff --git a/jsign-core/src/test/resources/services/oraclecloud-error.json b/jsign-core/src/test/resources/services/oraclecloud-error.json new file mode 100644 index 00000000..870041f3 --- /dev/null +++ b/jsign-core/src/test/resources/services/oraclecloud-error.json @@ -0,0 +1,4 @@ +{ + "code": "NotAuthorizedOrNotFound", + "message": "resource does not exist or you are not authorized to access it." +} diff --git a/jsign-core/src/test/resources/services/oraclecloud-listkeys.json b/jsign-core/src/test/resources/services/oraclecloud-listkeys.json new file mode 100644 index 00000000..5a995ae2 --- /dev/null +++ b/jsign-core/src/test/resources/services/oraclecloud-listkeys.json @@ -0,0 +1,30 @@ +[ + { + "compartmentId": "ocid1.tenancy.oc1..abcdefghijk", + "definedTags": {}, + "displayName": "jsign-rsa-2048", + "freeformTags": {}, + "id": "ocid1.key.oc1.eu-paris-1.h5tafwboaahxq.abrwiljrwkhgllb5zfqchmvdkmqnzutqeq5pz7yo6z7yhl2zyn2yncwzxiza", + "lifecycleState": "ENABLED", + "timeCreated": "2024-03-26T15:49:25.868Z", + "vaultId": "ocid1.vault.oc1.eu-paris-1.h5tafwboaahxq.abrwiljr4qztopmd3icfsehbfzegglkbihuk7gcc7a7epanvxmoghtmqyzqq", + "protectionMode": "SOFTWARE", + "algorithm": "RSA", + "externalKeyReferenceDetails": null, + "isAutoRotationEnabled": false + }, + { + "compartmentId": "ocid1.tenancy.oc1..abcdefghijk", + "definedTags": {}, + "displayName": "jsign-rsa-2048-hsm", + "freeformTags": {}, + "id": "ocid1.key.oc1.eu-paris-1.h5tafwboaahxq.abrwiljr7tub2mmyv5x2w6hwdlbpa3l567vih67yypquqkm4pjgk4cx7rkpa", + "lifecycleState": "ENABLED", + "timeCreated": "2024-03-26T14:16:16.099Z", + "vaultId": "ocid1.vault.oc1.eu-paris-1.h5tafwboaahxq.abrwiljr4qztopmd3icfsehbfzegglkbihuk7gcc7a7epanvxmoghtmqyzqq", + "protectionMode": "HSM", + "algorithm": "RSA", + "externalKeyReferenceDetails": null, + "isAutoRotationEnabled": false + } +] diff --git a/jsign-core/src/test/resources/services/oraclecloud-listvaults.json b/jsign-core/src/test/resources/services/oraclecloud-listvaults.json new file mode 100644 index 00000000..88aefb91 --- /dev/null +++ b/jsign-core/src/test/resources/services/oraclecloud-listvaults.json @@ -0,0 +1,48 @@ +[ + { + "compartmentId": "ocid1.tenancy.oc1..abcdefghijk", + "cryptoEndpoint": "https://h5tafwboaahxq-crypto.kms.eu-paris-1.oci.oraclecloud.com", + "definedTags": { + "Oracle-Tags": { + "CreatedBy": "default/ebourg@apache.org", + "CreatedOn": "2024-03-26T14:14:06.394Z" + } + }, + "displayName": "jsign", + "freeformTags": {}, + "id": "ocid1.vault.oc1.eu-paris-1.h5tafwboaahxq.abrwiljr4qztopmd3icfsehbfzegglkbihuk7gcc7a7epanvxmoghtmqyzqq", + "lifecycleState": "ACTIVE", + "managementEndpoint": "http://localhost:18080", + "timeCreated": "2024-03-26T14:14:09.366Z", + "timeOfDeletion": null, + "vaultType": "DEFAULT", + "restoredFromVaultId": null, + "wrappingkeyId": "", + "replicaDetails": null, + "isPrimary": true, + "externalKeyManagerMetadataSummary": null + }, + { + "compartmentId": "ocid1.tenancy.oc1..abcdefghijk", + "cryptoEndpoint": "https://h5s765syaagfm-crypto.kms.eu-paris-1.oci.oraclecloud.com", + "definedTags": { + "Oracle-Tags": { + "CreatedBy": "default/ebourg@apache.org", + "CreatedOn": "2024-03-24T00:39:52.255Z" + } + }, + "displayName": "test", + "freeformTags": {}, + "id": "ocid1.vault.oc1.eu-paris-1.h5s765syaagfm.abrwiljridvzkq3wh47e6tk5xw2ma65q2pz34idhfeo54n5nmqw7cquntn5a", + "lifecycleState": "PENDING_DELETION", + "managementEndpoint": "http://localhost:18080", + "timeCreated": "2024-03-24T00:39:53.764Z", + "timeOfDeletion": "2024-04-25T13:00:00.000Z", + "vaultType": "DEFAULT", + "restoredFromVaultId": null, + "wrappingkeyId": "", + "replicaDetails": null, + "isPrimary": true, + "externalKeyManagerMetadataSummary": null + } +] \ No newline at end of file diff --git a/jsign-core/src/test/resources/services/oraclecloud-sign.json b/jsign-core/src/test/resources/services/oraclecloud-sign.json new file mode 100644 index 00000000..7208f814 --- /dev/null +++ b/jsign-core/src/test/resources/services/oraclecloud-sign.json @@ -0,0 +1,6 @@ +{ + "keyId": "ocid1.key.oc1.eu-paris-1.h5tafwboaahxq.abrwiljrwkhgllb5zfqchmvdkmqnzutqeq5pz7yo6z7yhl2zyn2yncwzxiza", + "keyVersionId": "ocid1.keyversion.oc1.eu-paris-1.h5tafwboaahxq.pcevmybo5aqaa.abrwiljrbymibdobzkfsgobbzwzezvw57bgq3qg3y5dn733iqyjenergjfpa", + "signature": "MiZ/YXfluqyuMfR3cnChG7+K7JmU2b8SzBAc6+WOpWQwIV4GfkLcRe0A68H45Lf+XPiMPPLrs7EqOv1EAnkYDFx5AqZBTWBfoaBeqKpy30OBvNbxIsaTLsaJYGypwmHOUTP+Djz7FxQUyM0uWVfUnHUDT564gQLz0cta6PKE/oMUo9fZhpv5VQcgfrbdUlPaD/cSAOb833ZSRzPWbnqztWO6py5sUugvqGFHKhsEXesx5yrPvJTKu5HVF3QM3E8YrgnVfFK14W8oyTJmXIWQxfYpwm/CW037UmolDMqwc3mjx1758kR+9lOcf8c/LSmD/SVD18SDSK4FyLQWOmn16A==", + "signingAlgorithm": "SHA_256_RSA_PKCS1_V1_5" +}