This is an automated email from the ASF dual-hosted git repository. weizhouapache pushed a commit to branch 4.24-new-api-sign-algorithm in repository https://gitbox.apache.org/repos/asf/cloudstack.git
commit b48d21f8305aed8d30f5702f95feba1dc1b9412b Author: Wei Zhou <[email protected]> AuthorDate: Fri Aug 21 16:22:17 2026 +0200 API: sign requests by algorithm HmacSHA512 by default --- .../cloudstack/config/ApiServiceConfiguration.java | 7 +++- server/src/main/java/com/cloud/api/ApiServer.java | 39 +++++++++++++++------- .../java/com/cloud/utils/security/Algorithms.java | 26 +++++++++++++++ 3 files changed, 59 insertions(+), 13 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/config/ApiServiceConfiguration.java b/api/src/main/java/org/apache/cloudstack/config/ApiServiceConfiguration.java index 113b97f43c8..9bf7b2fd376 100644 --- a/api/src/main/java/org/apache/cloudstack/config/ApiServiceConfiguration.java +++ b/api/src/main/java/org/apache/cloudstack/config/ApiServiceConfiguration.java @@ -35,6 +35,11 @@ public class ApiServiceConfiguration implements Configurable { public static final ConfigKey<String> ApiAllowedSourceCidrList = new ConfigKey<>(String.class, "api.allowed.source.cidr.list", "Advanced", "0.0.0.0/0,::/0", "Comma separated list of IPv4/IPv6 CIDRs from which API calls can be performed. Can be set on Global and Account levels.", true, ConfigKey.Scope.Account, null, null, null, null, null, ConfigKey.Kind.CSV, null); + public static final ConfigKey<Boolean> ApiLegacyAlgorithmSupported = new ConfigKey<>("Advanced", Boolean.class, "api.legacy.algorithm.supported", + "true", "In older versions, the CloudStack API server used the HmacSHA1 algorithm to sign and validate requests. " + + "If this setting is enabled, HmacSHA1 will remain supported (alongside the newer HmacSHA512 algorithm), ensuring compatibility with tools " + + "such as CloudMonkey, cloudstack-go, and other third-party projects. " + + "If disabled, only the HmacSHA512 algorithm will be supported.", true, ConfigKey.Scope.Global); public static void validateEndpointUrl() { String csUrl = getApiServletPathValue(); @@ -55,7 +60,7 @@ public class ApiServiceConfiguration implements Configurable { @Override public ConfigKey<?>[] getConfigKeys() { - return new ConfigKey<?>[] {ManagementServerAddresses, ApiServletPath, DefaultUIPageSize, ApiSourceCidrChecksEnabled, ApiAllowedSourceCidrList}; + return new ConfigKey<?>[] {ManagementServerAddresses, ApiServletPath, DefaultUIPageSize, ApiSourceCidrChecksEnabled, ApiAllowedSourceCidrList, ApiLegacyAlgorithmSupported}; } } diff --git a/server/src/main/java/com/cloud/api/ApiServer.java b/server/src/main/java/com/cloud/api/ApiServer.java index 61c8ae68c5b..32d8b66b3b4 100644 --- a/server/src/main/java/com/cloud/api/ApiServer.java +++ b/server/src/main/java/com/cloud/api/ApiServer.java @@ -31,8 +31,9 @@ import java.net.Socket; import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; -import java.security.Security; import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; @@ -152,7 +153,6 @@ import org.apache.http.protocol.ResponseDate; import org.apache.http.protocol.ResponseServer; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.springframework.stereotype.Component; import com.cloud.api.dispatch.DispatchChainFactory; @@ -204,6 +204,7 @@ import com.cloud.utils.db.UUIDManager; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.exception.ExceptionProxyObject; import com.cloud.utils.net.NetUtils; +import com.cloud.utils.security.Algorithms; import com.google.gson.reflect.TypeToken; @Component @@ -467,7 +468,6 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer @Override public boolean start() { - Security.addProvider(new BouncyCastleProvider()); Integer apiPort = IntegrationAPIPort.value(); // api port, null by default isPostRequestsAndTimestampsEnforced = EnforcePostRequestsAndTimestamps.value(); @@ -1139,18 +1139,23 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer unsignedRequest = unsignedRequest.toLowerCase(); - final Mac mac = Mac.getInstance("HmacSHA1"); - final SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(), "HmacSHA1"); - mac.init(keySpec); - mac.update(unsignedRequest.getBytes()); + String santizedSignature = signature.replaceAll(SANITIZATION_REGEX, "_"); - final byte[] encryptedBytes = mac.doFinal(); - final String computedSignature = Base64.encodeBase64String(encryptedBytes); - final boolean equalSig = ConstantTimeComparator.compareStrings(signature, computedSignature); + final boolean apiLegacyAlgorithmSupported = ApiServiceConfiguration.ApiLegacyAlgorithmSupported.value(); + List<String> algorithms = apiLegacyAlgorithmSupported ? List.of(Algorithms.HMAC_SHA512, Algorithms.HMAC_SHA1) : List.of(Algorithms.HMAC_SHA512); + boolean equalSig = false; + for (String algorithm : algorithms) { + String computedSignature = getComputedSignature(algorithm, secretKey, unsignedRequest); + equalSig = ConstantTimeComparator.compareStrings(signature, computedSignature); + if (!equalSig) { + logger.info("User signature [{}] is not equaled to computed signature [{}] with algorithm {}.", santizedSignature, computedSignature, algorithm); + } else { + logger.debug("User signature [{}] is equaled to computed signature [{}] with algorithm {}.", santizedSignature, computedSignature, algorithm); + break; + } + } if (!equalSig) { - signature = signature.replaceAll(SANITIZATION_REGEX, "_"); - logger.info("User signature [{}] is not equaled to computed signature [{}].", signature, computedSignature); return false; } CallContext.register(user, account); @@ -1170,6 +1175,16 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer return false; } + private String getComputedSignature(String algorithm, String secretKey, String unsignedRequest) throws NoSuchAlgorithmException, InvalidKeyException { + final Mac mac = Mac.getInstance(algorithm); + final SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(), algorithm); + mac.init(keySpec); + mac.update(unsignedRequest.getBytes()); + + final byte[] encryptedBytes = mac.doFinal(); + return Base64.encodeBase64String(encryptedBytes); + } + private boolean commandAvailable(final InetAddress remoteAddress, final String commandName, final User user, ApiKeyPair keyPair, ApiKeyPairPermission... rolePermissions) { try { checkCommandAvailable(user, commandName, remoteAddress, keyPair, rolePermissions); diff --git a/utils/src/main/java/com/cloud/utils/security/Algorithms.java b/utils/src/main/java/com/cloud/utils/security/Algorithms.java new file mode 100644 index 00000000000..7ccf74e6db2 --- /dev/null +++ b/utils/src/main/java/com/cloud/utils/security/Algorithms.java @@ -0,0 +1,26 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF 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 com.cloud.utils.security; + +public class Algorithms { + + public static final String HMAC_SHA1 = "HmacSHA1"; + public static final String HMAC_SHA512 = "HmacSHA512"; +}
