OracleCloudSigningService.java
/**
* 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.IOException;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyStoreException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.jsign.DigestAlgorithm;
/**
* Signing service using the Oracle Cloud API.
*
* @since 7.0
*/
public class OracleCloudSigningService implements SigningService {
/** Source for the certificates */
private final Function<String, Certificate[]> certificateStore;
/** The credentials */
private final OracleCloudCredentials credentials;
/** Mapping between Java and OCI signing algorithms */
private final Map<String, String> algorithmMapping = new HashMap<>();
{
algorithmMapping.put("SHA256withRSA", "SHA_256_RSA_PKCS1_V1_5");
algorithmMapping.put("SHA384withRSA", "SHA_384_RSA_PKCS1_V1_5");
algorithmMapping.put("SHA512withRSA", "SHA_512_RSA_PKCS1_V1_5");
algorithmMapping.put("SHA256withECDSA", "ECDSA_SHA_256");
algorithmMapping.put("SHA384withECDSA", "ECDSA_SHA_384");
algorithmMapping.put("SHA512withECDSA", "ECDSA_SHA_512");
algorithmMapping.put("SHA256withRSA/PSS", "SHA_256_RSA_PKCS_PSS");
algorithmMapping.put("SHA384withRSA/PSS", "SHA_394_RSA_PKCS_PSS");
algorithmMapping.put("SHA512withRSA/PSS", "SHA_512_RSA_PKCS_PSS");
}
/**
* Creates a new Oracle Cloud signing service.
*
* @param credentials the Oracle Cloud credentials (user, tenancy, region, private key)
* @param certificateStore provides the certificate chain for the keys
*/
public OracleCloudSigningService(OracleCloudCredentials credentials, Function<String, Certificate[]> certificateStore) {
this.credentials = credentials;
this.certificateStore = certificateStore;
}
@Override
public String getName() {
return "OracleCloud";
}
String getVaultEndpoint() {
return "https://kms." + credentials.getRegion() + ".oraclecloud.com";
}
@Override
public List<String> aliases() throws KeyStoreException {
List<String> aliases = new ArrayList<>();
try {
// VaultSummary/ListVaults (https://docs.oracle.com/en-us/iaas/api/#/en/key/release/VaultSummary/ListVaults)
RESTClient kmsClient = new RESTClient(getVaultEndpoint()).authentication(this::sign).errorHandler(this::error);
Map<String, ?> result = kmsClient.get("/20180608/vaults?compartmentId=" + credentials.getTenancy());
Object[] vaults = (Object[]) result.get("result");
for (Object v : vaults) {
Map<String, ?> vault = (Map<String, ?>) v;
if ("ACTIVE".equals(vault.get("lifecycleState"))) {
String endpoint = (String) vault.get("managementEndpoint");
RESTClient managementClient = new RESTClient(endpoint).authentication(this::sign).errorHandler(this::error);
// KeySummary/ListKeys (https://docs.oracle.com/en-us/iaas/api/#/en/key/release/KeySummary/ListKeys)
result = managementClient.get("/20180608/keys?compartmentId=" + credentials.getTenancy());
Object[] keys = (Object[]) result.get("result");
for (Object k : keys) {
Map<String, ?> key = (Map<String, ?>) k;
if ("ENABLED".equals(key.get("lifecycleState")) && !"EXTERNAL".equals(key.get("protectionMode"))) {
aliases.add((String) key.get("id"));
}
}
}
}
} catch (IOException e) {
throw new KeyStoreException(e);
}
return aliases;
}
@Override
public Certificate[] getCertificateChain(String alias) {
return certificateStore.apply(alias);
}
@Override
public SigningServicePrivateKey getPrivateKey(String alias, char[] password) throws UnrecoverableKeyException {
Certificate[] chain = getCertificateChain(alias);
String algorithm = chain[0].getPublicKey().getAlgorithm();
return new SigningServicePrivateKey(alias, algorithm, this);
}
@Override
public byte[] sign(SigningServicePrivateKey privateKey, String algorithm, byte[] data) throws GeneralSecurityException {
String alg = algorithmMapping.get(algorithm);
if (alg == null) {
throw new InvalidAlgorithmParameterException("Unsupported signing algorithm: " + algorithm);
}
// SignedData/Sign (https://docs.oracle.com/en-us/iaas/api/#/en/key/release/SignedData/Sign)
Map<String, String> request = new HashMap<>();
request.put("keyId", privateKey.getId());
request.put("messageType", "RAW");
request.put("message", Base64.getEncoder().encodeToString(data));
request.put("signingAlgorithm", alg);
try {
RESTClient client = new RESTClient(getKeyEndpoint(privateKey.getId())).authentication(this::sign).errorHandler(this::error);
Map<String, ?> response = client.post("/20180608/sign", JsonWriter.format(request));
String signature = (String) response.get("signature");
return Base64.getDecoder().decode(signature);
} catch (IOException e) {
throw new GeneralSecurityException(e);
}
}
String getKeyEndpoint(String keyId) {
// extract the vault from the key id
Pattern pattern = Pattern.compile("ocid1\\.key\\.oc1\\.([^.]*)\\.([^.]*)\\..*");
Matcher matcher = pattern.matcher(keyId);
if (!matcher.matches()) {
throw new IllegalArgumentException("Invalid key id: " + keyId);
}
String region = matcher.group(1);
String vaultId = matcher.group(2);
String hostname = vaultId + "-crypto.kms." + region + ".oci.oraclecloud.com";
if (isUnknownHost(hostname)) {
hostname = vaultId + "-crypto.kms." + region + ".oraclecloud.com";
}
return "https://" + hostname;
}
boolean isUnknownHost(String hostname) {
try {
InetAddress.getByName(hostname);
return false;
} catch (UnknownHostException uhe) {
return true;
}
}
/**
* Signs the request
*
* @see <a href="https://docs.oracle.com/en-us/iaas/Content/API/Concepts/signingrequests.htm">Request signatures</a>
* @see <a href="https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-08">Signing HTTP Messages draft-cavage-http-signatures-08</a>
*/
private void sign(HttpURLConnection conn, byte[] data) {
StringBuilder signedHeaders = new StringBuilder();
StringBuilder stringToSign = new StringBuilder();
// date
DateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
String date = dateFormat.format(new Date());
conn.setRequestProperty("Date", date);
addSignedHeader(signedHeaders, stringToSign, "date", date);
// request target
String query = conn.getURL().getPath() + (conn.getURL().getQuery() != null ? "?" + conn.getURL().getQuery() : "");
addSignedHeader(signedHeaders, stringToSign, "(request-target)", conn.getRequestMethod().toLowerCase() + " " + query);
// host
addSignedHeader(signedHeaders, stringToSign, "host", conn.getURL().getHost());
if (data != null) {
// content length
int contentLength = data.length;
conn.setRequestProperty("Content-Length", String.valueOf(contentLength));
addSignedHeader(signedHeaders, stringToSign, "content-length", String.valueOf(contentLength));
// content type
conn.setRequestProperty("Content-Type", "application/json");
addSignedHeader(signedHeaders, stringToSign, "content-type", "application/json");
// content sha256
String digest = Base64.getEncoder().encodeToString(DigestAlgorithm.SHA256.getMessageDigest().digest(data));
conn.setRequestProperty("x-content-sha256", digest);
addSignedHeader(signedHeaders, stringToSign, "x-content-sha256", digest);
}
String signature = Base64.getEncoder().encodeToString(rsa256sign(credentials.getPrivateKey(), stringToSign.toString().trim()));
String authorization = String.format("Signature headers=\"%s\",keyId=\"%s\",algorithm=\"rsa-sha256\",signature=\"%s\",version=\"1\"", signedHeaders.toString().trim(), credentials.getKeyId(), signature);
conn.setRequestProperty("Authorization", authorization);
}
private void addSignedHeader(StringBuilder signedHeaders, StringBuilder stringToSign, String key, String value) {
signedHeaders.append(key).append(" ");
stringToSign.append(key).append(": ").append(value).append("\n");
}
private byte[] rsa256sign(PrivateKey privateKey, String message) {
try {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(message.getBytes(StandardCharsets.UTF_8));
return signature.sign();
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
private String error(Map<String, ?> response) {
return response.get("code") + ": " + response.get("message");
}
}