我们正在尝试使用 Azure Key Value 私钥来签署 BouncyCastle 为 PDF 文档生成的签名属性(嵌入 PDF 可哈希内容摘要)以允许 PDF 签名。
但是,使用 Azure Key Vault 的签名过程失败,因为 BouncyCastle 生成的签名属性长度不等于 32 个字节,并显示以下错误消息:
com.azure.security.keyvault.keys.implementation.models.KeyVaultErrorException: Status code 400, "{"error":{"code":"BadParameter","message":"Invalid length of 'value': 153 bytes. RS256 requires 32 bytes, encoded with base64url."}}"
at java.base/java.lang.invoke.MethodHandle.invokeWithArguments(MethodHandle.java:733) ~[na:na]
at com.azure.core.implementation.MethodHandleReflectiveInvoker.invokeStatic(MethodHandleReflectiveInvoker.java:26) ~[azure-core-1.51.0.jar:1.51.0]
at com.azure.core.implementation.http.rest.ResponseExceptionConstructorCache.invoke(ResponseExceptionConstructorCache.java:53) ~[azure-core-1.51.0.jar:1.51.0]
at com.azure.core.implementation.http.rest.RestProxyBase.instantiateUnexpectedException(RestProxyBase.java:407) ~[azure-core-1.51.0.jar:1.51.0]
at com.azure.core.implementation.http.rest.SyncRestProxy.ensureExpectedStatus(SyncRestProxy.java:133) ~[azure-core-1.51.0.jar:1.51.0]
at com.azure.core.implementation.http.rest.SyncRestProxy.handleRestReturnType(SyncRestProxy.java:211) ~[azure-core-1.51.0.jar:1.51.0]
at com.azure.core.implementation.http.rest.SyncRestProxy.invoke(SyncRestProxy.java:86) ~[azure-core-1.51.0.jar:1.51.0]
at com.azure.core.implementation.http.rest.RestProxyBase.invoke(RestProxyBase.java:124) ~[azure-core-1.51.0.jar:1.51.0]
at com.azure.core.http.rest.RestProxy.invoke(RestProxy.java:95) ~[azure-core-1.51.0.jar:1.51.0]
at jdk.proxy2/jdk.proxy2.$Proxy94.signSync(Unknown Source) ~[na:na]
at com.azure.security.keyvault.keys.implementation.KeyClientImpl.signWithResponse(KeyClientImpl.java:3286) ~[azure-security-keyvault-keys-4.8.7.jar:4.8.7]
at com.azure.security.keyvault.keys.cryptography.implementation.CryptographyClientImpl.sign(CryptographyClientImpl.java:248) ~[azure-security-keyvault-keys-4.8.7.jar:4.8.7]
at com.azure.security.keyvault.keys.cryptography.implementation.RsaKeyCryptographyClient.sign(RsaKeyCryptographyClient.java:230) ~[azure-security-keyvault-keys-4.8.7.jar:4.8.7]
at com.azure.security.keyvault.keys.cryptography.CryptographyClient.sign(CryptographyClient.java:699) ~[azure-security-keyvault-keys-4.8.7.jar:4.8.7]
at com.azure.security.keyvault.keys.cryptography.CryptographyClient.sign(CryptographyClient.java:651) ~[azure-security-keyvault-keys-4.8.7.jar:4.8.7]
at com.stackoverflow.keyvault.service.impl.AzureKeyVaultServiceImpl.signBytes(AzureKeyVaultServiceImpl.java:26) ~[classes/:na]
at com.stackoverflow.keyvault.controller.KeyVaultController.signMoreByte32(KeyVaultController.java:51) ~[classes/:na]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:255) ~[spring-web-6.1.12.jar:6.1.12]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:188) ~[spring-web-6.1.12.jar:6.1.12]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118) ~[spring-webmvc-6.1.12.jar:6.1.12]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:926) ~[spring-webmvc-6.1.12.jar:6.1.12]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:831) ~[spring-webmvc-6.1.12.jar:6.1.12]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.1.12.jar:6.1.12]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089) ~[spring-webmvc-6.1.12.jar:6.1.12]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979) ~[spring-webmvc-6.1.12.jar:6.1.12]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.1.12.jar:6.1.12]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903) ~[spring-webmvc-6.1.12.jar:6.1.12]
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564) ~[tomcat-embed-core-10.1.28.jar:6.0]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.1.12.jar:6.1.12]
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.28.jar:6.0]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) ~[tomcat-embed-core-10.1.28.jar:10.1.28]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.28.jar:10.1.28]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-10.1.28.jar:10.1.28]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.28.jar:10.1.28]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.28.jar:10.1.28]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.1.12.jar:6.1.12]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.12.jar:6.1.12]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.28.jar:10.1.28]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.28.jar:10.1.28]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.1.12.jar:6.1.12]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.12.jar:6.1.12]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.28.jar:10.1.28]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.28.jar:10.1.28]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.1.12.jar:6.1.12]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.12.jar:6.1.12]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.28.jar:10.1.28]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.28.jar:10.1.28]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.28.jar:10.1.28]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.28.jar:10.1.28]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) ~[tomcat-embed-core-10.1.28.jar:10.1.28]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[tomcat-embed-core-10.1.28.jar:10.1.28]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.28.jar:10.1.28]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.28.jar:10.1.28]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) ~[tomcat-embed-core-10.1.28.jar:10.1.28]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:384) ~[tomcat-embed-core-10.1.28.jar:10.1.28]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.28.jar:10.1.28]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:904) ~[tomcat-embed-core-10.1.28.jar:10.1.28]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) ~[tomcat-embed-core-10.1.28.jar:10.1.28]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.28.jar:10.1.28]
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190) ~[tomcat-embed-core-10.1.28.jar:10.1.28]
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.28.jar:10.1.28]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) ~[tomcat-embed-core-10.1.28.jar:10.1.28]
at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]
Azure Key Vault 不应该告诉我要签署哪些内容,不是吗?
相同的签名过程与本地自生成的私钥完美配合。但是,此类私钥不会存储在 HSM(硬件安全模块)中,并且可能会被 Adobe 撤销。
我们尝试利用 Azure Spring Boot 库利用 CryptoClient 实例来尝试签名过程。
application.yml
server:
port: 8080
spring:
application:
name: Stackoverflow Key Vault
cloud:
azure:
keyVault:
url: https://my-vault.vault.azure.net/
keyID: https://my-vault.vault.azure.net/keys/my-key
signature:
algorithm: SHA256withRSA
KeyVaultController.java
@RestController
@RequestMapping("/api/vault")
@RequiredArgsConstructor
@Slf4j
public class KeyVaultController {
private final KeyVaultService keyVaultService;
@Value("${signature.algorithm}")
private String signAlgo;
@GetMapping("/sign-digest")
public String signByte32() {
// SHA-256 digest
byte[] bytes = { -10, -106, 74, -128, 121, -80, 90, -100, 115, 82, -61, 120, -84, 23, 7, 5, 7, 96, -38, 95, -13, -46, 33, 70, -127, -66, 57, -31, 52, 123, -26, -109 };
return Base64.getEncoder().encodeToString(keyVaultService.signBytes(signAlgo, bytes));
}
@GetMapping("/sign-pdf-digest")
public String signMoreByte32() {
// BouncyCastle signed attributes for PDF signing
byte[] bytes = { 49, -127, -106, 48, 24, 6, 9, 42, -122, 72, -122, -9, 13, 1, 9, 3, 49, 11, 6, 9, 42, -122, 72, -122, -9, 13, 1, 7, 1, 48, 28, 6, 9, 42, -122, 72, -122, -9, 13, 1, 9, 5, 49, 15, 23, 13, 50, 52, 49, 49, 49, 52, 49, 48, 53, 57, 49, 51, 90, 48, 43, 6, 9, 42, -122, 72, -122, -9, 13, 1, 9, 52, 49, 30, 48, 28, 48, 11, 6, 9, 96, -122, 72, 1, 101, 3, 4, 2, 1, -95, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 48, 47, 6, 9, 42, -122, 72, -122, -9, 13, 1, 9, 4, 49, 34, 4, 32, -29, -80, -60, 66, -104, -4, 28, 20, -102, -5, -12, -56, -103, 111, -71, 36, 39, -82, 65, -28, 100, -101, -109, 76, -92, -107, -103, 27, 120, 82, -72, 85 };
return Base64.getEncoder().encodeToString(keyVaultService.signBytes(signAlgo, bytes));
}
AzureKeyVaultServiceImpl.java
@Service
@RequiredArgsConstructor
public class AzureKeyVaultServiceImpl implements KeyVaultService {
private final static Map<String, SignatureAlgorithm> ALGO_SIGNATURE_MAP = new HashMap<>();
private final CryptographyClient cryptoClient;
static {
ALGO_SIGNATURE_MAP.put("SHA256withRSA", SignatureAlgorithm.RS256);
ALGO_SIGNATURE_MAP.put("SHA384withRSA", SignatureAlgorithm.RS384);
ALGO_SIGNATURE_MAP.put("SHA512withRSA", SignatureAlgorithm.RS512);
}
@Override
public byte[] signBytes(String signAlgo, byte[] bytesToSign) {
return cryptoClient.sign(ALGO_SIGNATURE_MAP.get(signAlgo), bytesToSign)
.getSignature();
}
}
首先,mkl正确地指出,必须使用CryptographyClient.signData而不是CryptographyClient.sign来对PDF签名属性进行签名,该属性总是超过32字节长。
经过一番调查,我们还得出以下结论,从而找到了解决签名无效问题的可行方案: