javaazurepdfdigital-signaturevault

Azure Key Vault signing fails when signing BouncyCastle generated signed attributes of PDF document


We're trying to use Azure Key Value private key to sign BouncyCastle generated signed attributes (embeds PDF hashable content digest) for a PDF document to allow for PDF signing.

However, the signing process with Azure Key Vault is failing because the BouncyCastle generated signed attributes to sign is not equal to 32 bytes in length with the following error message:

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 should not tell me what content to sign, should it?

The same signing process works perfectly with a local self-generated private key. However such a private key is not stored in an HSM (Hardware Security Module) and may be revoked by Adobe.

We tried the Azure Spring Boot library leveraging CryptoClient instance to attempt the signing process.

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();
    }
}

Solution

  • Great! @Mr. Y for identifying the root cause, the correct approach for signing PDFs is to use certificates stored in Azure Key Vault instead of keys.

    Thank you to @mkl for suggesting that I share this as an answer to help others who might face a similar issue.

    enter image description here

    Use the CryptographyClient.signData method to sign the raw PDF content instead of the digest.

    Code:

    CryptographyClient cryptoClient = new CryptographyClientBuilder()
        .keyIdentifier("<your-key-vault-url>/certificates/my-certificate")
        .credential(new DefaultAzureCredentialBuilder().build())
        .buildClient();
    
    // Sign the raw PDF content
    SignResult signResult = cryptoClient.signData(SignatureAlgorithm.RS256, pdfContent);
    byte[] signature = signResult.getSignature();
    

    The signed PDF is now valid, as it contains the required certificate chain. Adobe Acrobat validates the signature successfully.