Jump to content

CMD (chave móvel digital) assinar PDF


Jose Sanches
 Share

Recommended Posts

Jose Sanches

Olá Viva,

Ando com um problema já algum tempo.

Necessito de assinar um PDF, recorrendo aos serviços da AMA, com Chave Móvel Digital.

Recentemente vi um tópico em C# aqui.

Tendo por base os contributos do @Labreu, tentei implementar o projeto em java (está disponível uma versão no github), não correu bem.

Enquadramento:

  • Obtém certificados utilizador
  • Cria assinatura vazia no PDF
  • Obtém hash do PDF a assinar, com prefixo da AMA
  • Cria PDF temporário com a assinatura vazia
  • Envia hash para AMA
  • AMA -> envio SMS para envio de hash assinada
  • Obtenção de hash assinada e inclusão da mesma e PDF.
  • Criação do PDF assinado, mas falha.

Alguma ideia/sugestão?

Obrigado

 

Edited by Jose Sanches
Link to comment
Share on other sites

marcolopes
1 hour ago, americob said:

O SAFE é para assinar Faturas Eletronicas.

Neste caso parece-me ser para assinar qualquer PDF.

O SAFE assina PDF... o PDF pode ser uma factura electrónica, como poder ser um outro documento qualquer 😄

The simplest explanation is usually the correct one

JAVA Utilities: https://github.com/marcolopes/dma

Link to comment
Share on other sites

americob
14 horas atrás, marcolopes disse:

O SAFE assina PDF... o PDF pode ser uma factura electrónica, como poder ser um outro documento qualquer 😄

Sim, em princípio, o SAFE pode assinar um PDF qualquer. Mas, não ficará com a assinatura: "em representação da Sociedade "xxx" com poderes para assinar faturas"?

Parece-me que é assim que funciona, já que para poder assinar Faturas é necessário pedir a representação da Sociedade respetiva, com poderes para diversos atos, neste caso a assinatura de Faturas.

É mesmo preciso enviar a Certidão Permanente da empresa para poderem confirmar que o assinante tem realmente poderes para o fazer, e só depois eles libertam essa opção no Cartão do Cidadão do assinante.

No meu caso, uso o Cartão do Cidadão para assinar Documentos como Contabilista Certificado e é assim que aparece a assinatura nos PDFs, quando opto pela respetiva qualidade.

Link to comment
Share on other sites

marcolopes
5 hours ago, americob said:

Sim, em princípio, o SAFE pode assinar um PDF qualquer. Mas, não ficará com a assinatura: "em representação da Sociedade "xxx" com poderes para assinar faturas"?

Parece-me que é assim que funciona, já que para poder assinar Faturas é necessário pedir a representação da Sociedade respetiva, com poderes para diversos atos, neste caso a assinatura de Faturas.

É mesmo preciso enviar a Certidão Permanente da empresa para poderem confirmar que o assinante tem realmente poderes para o fazer, e só depois eles libertam essa opção no Cartão do Cidadão do assinante.

No meu caso, uso o Cartão do Cidadão para assinar Documentos como Contabilista Certificado e é assim que aparece a assinatura nos PDFs, quando opto pela respetiva qualidade.

Nesse caso, e se o objectivo é mesmo assinar um qualquer PDF documental, o SAFE não será o mais indicado não senhor...

Acho que devemos criar um tópico para implementação SAFE...

The simplest explanation is usually the correct one

JAVA Utilities: https://github.com/marcolopes/dma

Link to comment
Share on other sites

Jose Sanches
Posted (edited)

Olá,

A ideia a criar uma uma funcionalidade e integra-la numa aplicação que possa assinar PDF's.

Existem algumas abordagens com as frameworks iText v7 ou PDFBox v2, mas nenhuma foi satisfatória.

já ultrapassei alguns problemas... mas ainda "não está lá", dento de dias vou disponibilizar uma nova versão com os progressos.

Edited by Jose Sanches
Link to comment
Share on other sites

marcolopes

Criei um tópico próprio para o SAFE: https://www.portugal-a-programar.pt/forums/topic/79044-serviço-de-assinatura-de-faturas-eletrónicas-safe/

Acho que vai ser o caminho de muita gente para permitir assinaturas de FE sem limite, pela módica quantia de 20 euros por ano...

The simplest explanation is usually the correct one

JAVA Utilities: https://github.com/marcolopes/dma

Link to comment
Share on other sites

marcolopes
6 hours ago, Jose Sanches said:

É uma abordagem válida como outra qualquer.

Vou continuar nesta abordagem...

Esta abordagem acaba por ser melhor, pois permite assinar qualquer tipo de documento

A implementação permite uma automatização completa sem interação do utilizador?

EDIT: Agora fiquei COMPLETAMENTE baralhado (deve ser da hora...)

Então se a assinatura com CHAVE MÓVEL ou directamente pelo CC permite assinar FACTURAS, porque razão foi criado o sistema SAFE??? https://www.autenticacao.gov.pt/web/guest/atributos-profissionais/assinatura-digital

Pelo que posso apurar será para evitar uma "autenticação" a cada assinatura... facilitando a assinatura de vários documentos...

Edited by marcolopes

The simplest explanation is usually the correct one

JAVA Utilities: https://github.com/marcolopes/dma

Link to comment
Share on other sites

americob
Em 30/06/2021 às 17:12, marcolopes disse:

Esta abordagem acaba por ser melhor, pois permite assinar qualquer tipo de documento

A implementação permite uma automatização completa sem interação do utilizador?

EDIT: Agora fiquei COMPLETAMENTE baralhado (deve ser da hora...)

Então se a assinatura com CHAVE MÓVEL ou directamente pelo CC permite assinar FACTURAS, porque razão foi criado o sistema SAFE??? https://www.autenticacao.gov.pt/web/guest/atributos-profissionais/assinatura-digital

Pelo que posso apurar será para evitar uma "autenticação" a cada assinatura... facilitando a assinatura de vários documentos...

Sim, a ideia com que fiquei é essa mesmo.

Se houver integração com o nosso software, não precisa código da chave móvel para cada assinatura, só no login do nosso programa. Ou seja, o gerente abre o programa de manhã e pode deixar um funcionário a fazer faturas o dia todo.

Não fiquei com certezas sobre a durabilidade do login perante o sistema SAFE (caduca ao fim de x minutos sem utilização???)

 

Link to comment
Share on other sites

bioshock
7 hours ago, americob said:

Não fiquei com certezas sobre a durabilidade do login perante o sistema SAFE (caduca ao fim de x minutos sem utilização???)

Se fizeres a integração via oAuth, sim expira mas podes renovar o token de acesso sem intervenção do utilizador.

Não andava muito atento a estas assinaturas electrónicas e tinha/tenho ideia que a sua obrigatoriedade é só para empresas públicas (?), para além disso vi os preços de algumas entidades e é impracticável.

Após ter conhecimento do SAFE e do custo (2 primeiros anos grátis e depois 40eur a cada 2 anos) parece-me uma solução apetecível para introduzir no meu software, independentemente de ser apenas ou não uma obrigatoriedade para a função pública.

  • Vote 1
Link to comment
Share on other sites

Jose Sanches

Olá Boa tarde,

Deixo aqui uma abordagem que assina o PDF sem a mensagem o documento foi alterado depois de assinando.

O PDF é assinado com a invocação dos serviços web da AMA.

 

 

private static final String input = "c:/tmp/ama/PDF-1.pdf";
    private static final String output =  "c:/tmp/ama/signed_signed.pdf";


    public static void main(String[] args) throws IOException, GeneralSecurityException {
        Security.addProvider(new BouncyCastleProvider());
        PdfReader          reader             = new PdfReader(input);
        OutputStream       fos                = new FileOutputStream(output);
        StampingProperties stampingProperties = new StampingProperties();
        //For any signature in the Pdf  but the first one, you need to use appendMode
        //        stampingProperties.useAppendMode();
        PdfSigner pdfSigner = new PdfSigner(reader, fos, stampingProperties);

         /*you can modify the signature appearance */
        PdfSignatureAppearance appearance = pdfSigner.getSignatureAppearance();
        appearance.setPageRect(new Rectangle(36, 508, 254, 200));
        appearance.setPageNumber(1);
        appearance.setLayer2FontSize(10f);
        appearance.setReason("Teste Assinatura");
        appearance.setLocation("Lisboa");

        IExternalSignatureContainer gsContainer = new GSSignatureContainer(PdfName.Adobe_PPKLite, PdfName.Adbe_pkcs7_detached);
        pdfSigner.signExternalContainer(gsContainer, 8049);
    }

	public class GSSignatureContainer implements IExternalSignatureContainer {

      /* Signature dictionary. Filter and SubFilter.  */
      private PdfDictionary sigDic;

      public GSSignatureContainer( PdfName filter, PdfName subFilter) {
          sigDic = new PdfDictionary();
          sigDic.put(PdfName.Filter, filter);
          sigDic.put(PdfName.SubFilter, subFilter);
      }

      @Override
      public byte[] sign(InputStream data) throws GeneralSecurityException {
          try {
              //get all certificates (3) from client via web service
              Certificate[] chain = new CallAMA().getCertificates();

              String hashAlgorithm = DigestAlgorithms.SHA256;//"SHA-256";
              BouncyCastleDigest digest = new BouncyCastleDigest();
              MessageDigest md = digest.getMessageDigest(hashAlgorithm);

              byte[]   hash = DigestAlgorithms.digest(data, md);
              PdfPKCS7 sgn  = new PdfPKCS7(null, chain, hashAlgorithm, null, digest, false);

              //Collection<byte[]> ocsp = new OcspClientBouncyCastle(null);
              OcspClientBouncyCastle ocspClient = new OcspClientBouncyCastle(null);
              Collection<byte[]> ocsp = new ArrayList<byte[]>();
              for(var i = 0; i < chain.length - 1; i++) {
                  byte[] encoded = ocspClient.getEncoded((X509Certificate)chain[i], (X509Certificate)chain[i + 1], null);
                  if(encoded != null) ocsp.add(encoded);
              }

              byte[] attributeBytes = sgn.getAuthenticatedAttributeBytes(hash, PdfSigner.CryptoStandard.CADES, ocsp, null);

               //criar sha256 message digest
              byte[] attributeBytesDigest = MessageDigest.getInstance(hashAlgorithm).digest(attributeBytes);

              /****************************************************
               * CALL client (AMA) -> receive SMS and signed hash *
               ****************************************************/
              byte[] signedHash = new CallAMA().getHashSignedByAma(attributeBytesDigest);
              sgn.setExternalDigest(signedHash, null, "RSA");

              ITSAClient tsaClient = null;//new GSTSAClient(access);
              return sgn.getEncodedPKCS7(hash, PdfSigner.CryptoStandard.CADES, tsaClient, ocsp, null);
          } catch (IOException | GeneralSecurityException de) {
              de.printStackTrace();
              throw new GeneralSecurityException(de);
          }
      }

      @Override
      public void modifySigningDictionary(PdfDictionary signDic) {
          signDic.putAll(sigDic);
      }
	}
}

 

  • Vote 1
Link to comment
Share on other sites

Olá José,

Conseguiste chegar a uma versão que funcione e resulte no PDF assinado? Estou também a tentar assinar PDFs usando a CMD com os serviços da AMA e não consigo obter um ficheiro válido.

Edited by Gus
Link to comment
Share on other sites

Jose Sanches

Ola Gus,

Esta ação não é fácil. A falta de documentação técnica assim como a falta de exemplos torna tudo mais complicado.

No exemplo que disponibilizei (o mais recente), assina o PDF e o Adobe Reader dá a mensagem "A validade da assinatura é desconhecida" (rectângulo laranja).

Questionei a AMA com o exemplo, e a informação que obtive foi:

"No ambiente de pré-produção é natural que não seja verde - isso apenas em produção."

Mas.... não sei

Link to comment
Share on other sites

On 7/23/2021 at 4:26 PM, Jose Sanches said:

"No ambiente de pré-produção é natural que não seja verde - isso apenas em produção."

Sim, isso é verdade, só em produção vai ser possível confirmar que está tudo a funcionar.

O meu problema parece que está a ser ao aplicar o hash devolvido pela AMA, o PDF final fica sempre com o erro de que foi modificado ou corrompido desde que foi colocado o placeholder da assinatura.

Tinha visto aqui que também estavas com alguns problemas, mas parece que conseguiste que funcionasse: https://stackoverflow.com/questions/67889692/java-itext7-deferred-with-sign-prefix-producing-invalid-signed-pdf

É possível partilhares o código da versão que achas que funciona? Ou este último que colocaste neste tópico funciona?

 

Link to comment
Share on other sites

Olá Gus,

O meu último post no stackoverflow já resolve o problema que referes.

Voltei a questionar a AMA com o exemplo no sentido de aferir se estava tudo bem. Passei o PDF pelo código que me valida a assinatura (mas ainda em triângulo laranja)

Estou a ultimar uma versão com a inclusão de um PDF temporário (para incluir a assinatura vazia e obter o HASH), depois é invocar o endpoint da AMA (PRE + HASH) obter o HASH assinado e inclui-lo no PDF (criar uma novo ficheiro) assinado.

Nota: se tudo estiver bem... actualizo o código no github com uma versão web funcional

Edited by Jose Sanches
Link to comment
Share on other sites

Olá,

Finalmente resolvo o problema...

Nota:

O PDF assinado não apresenta problemas (foi verificado com código para validar a assinatura).

O PDF apresente um triângulo laranja (natural de pré-produção) - Falha na AMA a URL: http://ocsp.cmd.teste.cartaodecidadao.pt/publico/ocsp para validação OCSP

private static final String input = "c:/tmp/ama/PDF-1.pdf";
	private static final String tmp = "c:/tmp/ama/tmpPDF.pdf";
    private static final String output =  "c:/tmp/ama/signed.pdf";

    public static void main(String[] args) throws IOException, GeneralSecurityException {
        BouncyCastleProvider providerBC = new BouncyCastleProvider();
        Security.addProvider(providerBC);

        //Obter via serviço web os certificado (a AMA devolve 3)
        Certificate[] chain = new CallAMA().getCertificates();

        Teste app = new Teste();
        
        //A partir do PDF original, lê o HASH para ser assinado,
      	//Cria um PDF temporário (com uma assinatura vazia).
      	//Nota: "sig" é o nome do campo que identifica a assinatura no PDF
        byte[] hash4Sign = app.emptySignature(input, tmp, "sig", chain);
        
        //Concatena "sha256SigPrefix" com o HASH obtido do PDF (o HASH deve ter 32 bytes, com o valor "sha256SigPrefix" fica com 51 bytes)
        //Envia o HASH (51 bytes) para a AMA
        //Aguarda o SMS com o codigo e envia-o novamente para a AMA via serviço Web
        //Por fim, recebe o HASH (da AMA) assinado
      	//Nota: Importante, aqui deve ser implementada toda a logica da aplicação web, não o faço, esta classe é um exemplo
        byte[] signedHash = new CallAMA().getHashSignedByAma(hash4Sign);
        
        //insere o HASH (da AMA) no PDF temporário e cria um ficheiro final assinado
        app.createSignature(signedHash, tmp, output, "sig", chain);
    }

    public byte[] emptySignature(String src, String dest, String fieldname, Certificate[] chain) throws IOException, GeneralSecurityException {
        PdfReader reader = new PdfReader(src);
        PdfSigner signer = new PdfSigner(reader, new FileOutputStream(dest), new StampingProperties().useAppendMode());
        PdfSignatureAppearance appearance = signer.getSignatureAppearance();

        appearance
        		.setLayer2FontSize(6)
                .setPageRect(new Rectangle(36, 748, 250, 50))
                .setPageNumber(1)
                .setCertificate(chain[0])
                .setSignatureCreator("ORIGEM");
        signer.setFieldName(fieldname);
	        
	    /* ExternalBlankSignatureContainer constructor will create the PdfDictionary for the signature
         * information and will insert the /Filter and /SubFilter values into this dictionary.
         * It will leave just a blank placeholder for the signature that is to be inserted later.
         */
        MyExternalBlankSignatureContainer external = new MyExternalBlankSignatureContainer(chain, PdfName.Adobe_PPKLite, PdfName.Adbe_pkcs7_detached);

       
        // Sign the document using an external container.
        // 8192 is the size of the empty signature placeholder.
        signer.signExternalContainer(external, 8192);
        byte[] hash4Sign = external.getHash4Sign();
        return hash4Sign;
    }

    public void createSignature(byte[] hashSigned, String src, String dest, String fieldName, Certificate[] chain)
            throws IOException, GeneralSecurityException {
        PdfReader reader = new PdfReader(src);
        try(FileOutputStream os = new FileOutputStream(dest)) {
            PdfSigner signer = new PdfSigner(reader, os, new StampingProperties());

            IExternalSignatureContainer external = new MyExternalSignatureContainer(hashSigned, chain, PdfName.Adobe_PPKLite, PdfName.Adbe_pkcs7_detached);

            // Signs a PDF where space was already reserved. The field must cover the whole document.
            signer.signDeferred(signer.getDocument(), fieldName, os, external);
        }
    }

    class MyExternalSignatureContainer implements IExternalSignatureContainer {

    	/* Signature dictionary. Filter and SubFilter.  */
        private PdfDictionary sigDic;
        private byte[] signedHash =null;
        private Certificate[] chain = null;

        public MyExternalSignatureContainer(byte[] _signedHash, Certificate[] _chain, PdfName filter, PdfName subFilter) {
            sigDic = new PdfDictionary();
            sigDic.put(PdfName.Filter, filter);
            sigDic.put(PdfName.SubFilter, subFilter);
            signedHash = _signedHash;
            chain = _chain;
        }

        @Override
        public byte[] sign(InputStream data) throws GeneralSecurityException {
            try {
                String hashAlgorithm = DigestAlgorithms.SHA256;//"SHA-256";
                BouncyCastleDigest digest = new BouncyCastleDigest();
                MessageDigest md = digest.getMessageDigest(hashAlgorithm);

                byte[]   hash = DigestAlgorithms.digest(data, md);
                PdfPKCS7 sgn  = new PdfPKCS7(null, chain, hashAlgorithm, null, digest, false);

                OcspClientBouncyCastle ocspClient = new OcspClientBouncyCastle(null);
                Collection<byte[]> ocsp = new ArrayList<byte[]>();
         	   	for(var i = 0; i < chain.length - 1; i++) {
         	   		byte[] encoded = ocspClient.getEncoded((X509Certificate)chain[i], (X509Certificate)chain[i + 1], null);
         	   		if(encoded != null) ocsp.add(encoded);
         	   	}

                sgn.setExternalDigest(signedHash, null, "RSA");

                ITSAClient tsaClient = null;//new GSTSAClient(access);
                return sgn.getEncodedPKCS7(hash, PdfSigner.CryptoStandard.CADES, tsaClient, ocsp, null);
            } catch (IOException | GeneralSecurityException de) {
                de.printStackTrace();
                throw new GeneralSecurityException(de);
            }
        }

        @Override
        public void modifySigningDictionary(PdfDictionary signDic) {
            signDic.putAll(sigDic);
        }
    }
    
    
    class MyExternalBlankSignatureContainer implements IExternalSignatureContainer {

    	/* Signature dictionary. Filter and SubFilter.  */
        private PdfDictionary sigDic;
        private byte[] hash4Sign = null;
        private Certificate[] chain = null;

        public MyExternalBlankSignatureContainer(Certificate[] _chain, PdfName filter, PdfName subFilter) {
            sigDic = new PdfDictionary();
            sigDic.put(PdfName.Filter, filter);
            sigDic.put(PdfName.SubFilter, subFilter);
            chain = _chain;
        }
        
        public byte[] getHash4Sign() {
        	return hash4Sign;
        }

        @Override
        public byte[] sign(InputStream data) throws GeneralSecurityException {
            try {
                String hashAlgorithm = DigestAlgorithms.SHA256;//"SHA-256";
                BouncyCastleDigest digest = new BouncyCastleDigest();
                MessageDigest md = digest.getMessageDigest(hashAlgorithm);

                byte[]   hash = DigestAlgorithms.digest(data, md);
                PdfPKCS7 sgn  = new PdfPKCS7(null, chain, hashAlgorithm, null, digest, false);

                OcspClientBouncyCastle ocspClient = new OcspClientBouncyCastle(null);
                Collection<byte[]> ocsp = new ArrayList<byte[]>();
         	   	for(var i = 0; i < chain.length - 1; i++) {
         	   		byte[] encoded = ocspClient.getEncoded((X509Certificate)chain[i], (X509Certificate)chain[i + 1], null);
         	   		if(encoded != null) ocsp.add(encoded);
         	   	}

                byte[] attributeBytes = sgn.getAuthenticatedAttributeBytes(hash, PdfSigner.CryptoStandard.CADES, ocsp, null);

                 //create sha256 message digest
                hash4Sign = MessageDigest.getInstance(hashAlgorithm).digest(attributeBytes);

                return new byte[0];
            } catch (IOException | GeneralSecurityException de) {
                de.printStackTrace();
                throw new GeneralSecurityException(de);
            }
        }

        @Override
        public void modifySigningDictionary(PdfDictionary signDic) {
            signDic.putAll(sigDic);
        }
    }

 

Edited by Jose Sanches
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
×
  • Create New...

Important Information

By using this site you accept our Terms of Use and Privacy Policy. We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.