Assine PDF usando um serviço externo e o iText

Eu tenho esse cenário.

Eu tenho um aplicativo que gera um PDF e precisa ser assinado.

Não temos os certificados para assinar o documento, porque eles estão em um HSM, e a única maneira de podermos fazer uso dos certificados é usando um serviço da web.

Este serviço da web oferece duas opções, envia o documento PDF e retorna um pdf assinado ou envia um hash que será assinado.

A primeira opção não é viável, porque o PDF é assinado sem um carimbo de data / hora (este é um requisito muito importante), portanto, a segunda opção é escolhida.

Este é o nosso código, primeiro obtemos a aparência da assinatura e calculamos o hash:

PdfReader reader = new PdfReader(Base64.decode(pdfB64));
reader.setAppendable(true);
baos = new ByteArrayOutputStream();

PdfStamper stamper = PdfStamper.createSignature(reader, baos, '\0', null, true);
appearance = stamper.getSignatureAppearance();
appearance.setCrypto(null, chain, null, PdfSignatureAppearance.SELF_SIGNED);
appearance.setVisibleSignature("Representant");
cal = Calendar.getInstance();
PdfDictionary dic = new PdfDictionary();
dic.put(PdfName.TYPE, PdfName.SIG);
dic.put(PdfName.FILTER, PdfName.ADOBE_PPKLITE);
dic.put(PdfName.SUBFILTER, new PdfName("adbe.pkcs7.detached"));
dic.put(PdfName.M, new PdfDate(cal));
appearance.setCryptoDictionary(dic);
HashMap<PdfName, Integer> exc = new HashMap<PdfName, Integer>();
exc.put(PdfName.CONTENTS, Integer.valueOf(reservedSpace.intValue() * 2 + 2));
appearance.setCertificationLevel(1);
appearance.preClose(exc);

AbstractChecksum checksum = JacksumAPI.getChecksumInstance("sha1");
checksum.reset();
checksum.update(Utils.streamToByteArray(appearance.getRangeStream()));
hash = checksum.getByteArray();

Neste ponto, temos o código de hash do documento. Em seguida, enviamos o hash para o serviço da web e obtemos o código de hash assinado.

Por fim, colocamos o hash assinado no PDF:

byte[] paddedSig = new byte[reservedSpace.intValue()];
System.arraycopy(signedHash, 0, paddedSig, 0, signedHash.length);

PdfDictionary dic = new PdfDictionary();
dic.put(PdfName.CONTENTS, new PdfString(paddedSig).setHexWriting(true));
appearance.close(dic);

byte[] pdf = baos.toByteArray();

Neste ponto, temos um PDF assinado, mas com uma assinatura inválida. A Adobe diz que "o documento foi alterado ou corrompido desde que foi assinado".

Acho que cometemos algo errado no processo e não sabemos exatamente o que poderia ser.

Agradecemos a ajuda, ou uma maneira alternativa de fazer isso.

Obrigado.

EDITADO

Conforme sugerido por mkl, segui a seção 4.3.3 deste livroAssinaturas digitais para documentos PDFe meu código agora, o que se segue:

A primeira parte, quando calculamos o hash:

PdfReader reader = new PdfReader(Base64.decode(pdfB64));
reader.setAppendable(true);
baos = new ByteArrayOutputStream();

PdfStamper stamper = PdfStamper.createSignature(reader, baos, '\0');
appearance = stamper.getSignatureAppearance();

appearance.setReason("Test");
appearance.setLocation("A casa de la caputeta");
appearance.setVisibleSignature("TMAQ-TSR[0].Pagina1[0].DadesSignatura[0].Representant[0]");
appearance.setCertificate(chain[0]);

PdfSignature dic = new PdfSignature(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
dic.setReason(appearance.getReason());
dic.setLocation(appearance.getLocation());
dic.setContact(appearance.getContact());
dic.setDate(new PdfDate(appearance.getSignDate()));
appearance.setCryptoDictionary(dic);

HashMap<PdfName, Integer> exc = new HashMap<PdfName, Integer>();
exc.put(PdfName.CONTENTS, new Integer(reservedSpace.intValue() * 2 + 2));
appearance.preClose(exc);

ExternalDigest externalDigest = new ExternalDigest()
{
    public MessageDigest getMessageDigest(String hashAlgorithm) throws GeneralSecurityException
    {
        return DigestAlgorithms.getMessageDigest(hashAlgorithm, null);
    }
};

sgn = new PdfPKCS7(null, chain, "SHA256", null, externalDigest, false);
InputStream data = appearance.getRangeStream();
hash = DigestAlgorithms.digest(data, externalDigest.getMessageDigest("SHA256"));
cal = Calendar.getInstance();

byte[] sh = sgn.getAuthenticatedAttributeBytes(hash, cal, null, null, CryptoStandard.CMS);
sh = MessageDigest.getInstance("SHA256", "BC").digest(sh);

hashPdf = new String(Base64.encode(sh));

E na segunda parte, obtemos o hash assinado e o colocamos no PDF:

sgn.setExternalDigest(Base64.decode(hashSignat), null, "RSA");
byte[] encodedSign = sgn.getEncodedPKCS7(hash, cal, null, null, null, CryptoStandard.CMS);
byte[] paddedSig = new byte[reservedSpace.intValue()];
System.arraycopy(encodedSign, 0, paddedSig, 0, encodedSign.length);

PdfDictionary dic2 = new PdfDictionary();
dic2.put(PdfName.CONTENTS, new PdfString(paddedSig).setHexWriting(true));

appearance.close(dic2);

byte[] pdf = baos.toByteArray();

Agora, a Adobe gera um erro de biblioteca criptográfica interna. Código de erro: 0x2726, quando tentamos validar a assinatura.

questionAnswers(2)

yourAnswerToTheQuestion