skip to Main Content

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

2

Answers


  1. Chosen as BEST ANSWER

    First of all, mkl was right to point out that CryptographyClient.signData instead of CryptographyClient.sign had to be used to sign the PDF signed attributes, which are always more than 32 bytes long.

    After some investigation, we have also reached the following conclusions, which led to a working solution to the signature invalid issue:

    1. A Keys object can never be matched to a Certificates object in Azure Key Vault. This means that signing using a Keys object, while embedding a downloaded Certificates object in the signed PDF will always fail because the public keys of the two objects will never match!
    2. A Certificates(self-signed certificates allowed) object is always created with an associated hidden private key, which means that it can be used to sign content as well. In addition, the Certificates object may be downloaded as a certificate chain to be embedded into the signed PDF and its public key matches that of the signing hidden private key. This is actually the solution to the problem.

    The correct keyID is as follows:

    keyID: https://<my-vault>.vault.azure.net/certificates/<my-certificate>/<certificate-version>
    

    with <my-vault>, <my-certificate> and <certificate-version> being replaced with their corresponding values.

    In addition, the following were used to implement the PDF signing process using the PDFBox library:

    1. Class CreateSignatureBase.java and its supporting classes
    2. Implement a custom version of org.bouncycastle.operator.ContentSigner to leverage the CryptographyClient.signData method for signing with Azure Key Vault instead of the local private key.

  2. 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.

    • Replacing the key object (my-key) with a certificate object (my-vault.vault.azure.net/certificates/my-certificate) in Azure Key Vault. Certificates allow the certificate chain to be downloaded and embedded into the signed PDF.

    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();
    
    • Embed the signature and the certificate chain into the PDF.

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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search