p7m viewer firma digitale: come estrarre il contenuto - parte seconda

p7m viewer firma digitale: come estrarre il contenuto – parte seconda

See the article in English

Firma digitale e file p7m (p7m viewer): seconda parte

Visto il successo dell’articolo precedente sui file p7m volevo continuare in questo aggiungendo alcune cose che avevo tralasciato e che sono venute fuori nei vostri commenti, inoltre ho creato uno script che gestisce i file p7m, un vero e proprio p7m viewer,  e volevo darne annuncio in modo tale che possa esser testato e se necessario migliorato.

I file p7m possono essere di tipo DER o PEM

Nel precedente articolo abbiamo fatto sempre riferimento a p7m codificato in DER (Distinguished Encoding Rules) ovvero in formato binario mentre è possibile trovarsi con dei p7m codificati in PEM (Privacy Enhancement for Internet Electronic Mail) come è successo in un commento sempre nell’articolo precedente.
Quindi, se abbiamo la codifica DER non abbiamo problemi mentre per i PEM dobbiamo dire ad openssl del cambio di codifica tramite il parametro:

-inform PEM

Quindi il solito comando di verifica ed estrazione dell’allegato sarà:

openssl smime -in documento.pdf.p7m -inform PEM -verify -CAfile CA.pem -out documento.pdf

Per i sottocomandi openssl che usano direttamente la codifica DER dobbiamo considerare di convertire la codifica.

Conversione da PEM a DER

Un file pm7 codificato PEM non è altro che un file ascii editabile con un qualunque editor a caratteri (come vim, nano, kate e molti altri) avente la prima riga uguale a

-----BEGIN PKCS7-----

e l’ultima uguale a

-----END PKCS7-----

mentre le altre non sono altro che la codifica base64 (con allineamento a 64 byte per riga) del certificati DER
Quindi per la conversione sono equivalenti i seguenti comandi:

1- openssl pkcs7 -outform der -in "file_codica-pem.p7m" -out "file_codifica-der.p7m"
2- openssl base64 -d -in "file_codica-pem.p7m" -out "file_codifica-der.p7m"
3- perl -MMIME::Base64 -ne 'print decode_base64($_) if ! /-----BEGIN|END/' file_codifica-pem.p7m" > "file_codifica-der.p7m"

Naturalmente il primo è quello ufficiale di conversione da PEM a DER di openssl

Come verificare il formato di codifica del file p7m

Come abbiamo visto nel paragrafo precedente basta editare il file p7m con un editor di testo per capire se è in formato PEM ma se volessimo farlo in automatico tramite script basta semplicemente:

file_p7m="file_originale.p7m"
file_der_p7m="$file_p7m"
openssl pkcs7 -print_certs -text -noout -inform pem -in "$file_p7m" >/dev/null2>&1
if [ $? == 0 ]; then
# il file è in formato pem lo converto da in der
file_der_p7m="der_${file_p7m}"
openssl pkcs7 -outform der -in "$file_p7m" -out "$file_der_p7m}"
fi
# da qui in poi $file_der_p7m è codificato DER

Scaricamento portafoglio CA (CNIPA): metodo alternativo

Nel precedente articolo abbiamo visto un metodo per scaricare e convertire il portafoglio del CNIPA in portafoglio PEM usabile per openssl, ora vediamo come utilizzare un altro metodo direttamente con openssl invece di utilizzare perl e un ulteriore in php:

XML_CERTS='https://applicazioni.cnipa.gov.it/TSL/_IT_TSL_signed.xml'
wget --tries=2 -O Ca.xml ${XML_CERTS}
for i in `grep '<X509Certificate' Ca.xml`; do
echo -e "-----BEGIN CERTIFICATE-----"
echo $i| sed -e 's/<\/*X509Certificate>//g'| openssl base64 -d -A| openssl base64
echo -e "-----END CERTIFICATE-----"
done >Ca.pem
rm Ca.xml

Anche questo script lo potete trovare nel pacchetto sotto Github getTrustCAP7m.

Scaricamento portafoglio CA (CNIPA): metodo alternativo: php


<!--?php
$XML_CERTS='https://eidas.agid.gov.it/TL/TSL-IT.xml';
$xmlDoc = simplexml_load_file($XML_CERTS);
if ($xmlDoc === FALSE) {
	echo "Ci sono problemi a scaricare il file $XML_CERTS\n";
	exit(1);
}
foreach($xmlDoc--->getDocNamespaces() as $strPrefix => $strNamespace) {
  if(strlen($strPrefix)==0) {
	  $strPrefix="p7m";
	  $xmlDoc->registerXPathNamespace($strPrefix,$strNamespace);
  }
}
foreach ($xmlDoc->xpath('//p7m:X509Certificate') as $cert) {
	echo "-----BEGIN CERTIFICATE-----" . PHP_EOL;
	echo chunk_split($cert,64,PHP_EOL);
	echo "-----END CERTIFICATE-----" . PHP_EOL;
}

Anche questo script lo potete trovare nel pacchetto sotto Github getTrustCAP7m.

Estrazione campi dal certificato

Per estrarre il certificato abbiamo visto che basta utilizzare il comando:

openssl pkcs7 -inform DER -in file.p7m -print_certs -out cert.pem

Ora nel file cert.pem abbiamo il certificato di chi ha firmato il documento e per veder i campi del certificato possiamo utilizzare il comando:

openssl x509 -in cert.pem -text -noout

Se invece volessimo utilizzare i campi del certificato in uno script potremmo semplicemente scrivere:


cert=$(openssl pkcs7 -print_certs -text -inform der -in "file.p7m" 2>&1)
cn=$(echo "$cert"| grep 'Subject:'|sed -e 's/^.*CN=//' -e 's/[\/,].*$//' 2>&1)
cf=$(echo "$cert"| grep 'Subject:'|sed -e 's/^.*serialNumber=//' -e 's/[\/,].*$//' -e 's/^.*://' 2>&1)
ou=$(echo "$cert"| grep 'Subject:'|sed -e 's/^.*O=//' -e 's/[\/,].*$//' -e 's/^.*://' 2>&1)
issuer=$(echo "$cert"| grep 'Issuer:'|sed -e 's/^.*CN=//' -e 's/[\/,].*$//' 2>&1)
notbefore=$(echo "$cert"| grep 'Not Before\s*:' | sed -e 's/^.*Not Before\s*:\s*//')
notafter=$(echo "$cert"| grep -E 'Not After\s*:' | sed -e 's/^.*Not After\s*:\s*//')

In tal modo abbiamo il nome e cognome ($cn),il codice fiscale ($cf), l’ente di appartenenza ($ou), l’ente certificatore ($issuer), la validità del certificato (compresa tra $notbefore e $notafter).
Nel caso volessimo controllare se il certificato è valido potremmo confrontare l’intervallo tra $notbefore e $notafter con la data attuale e tramite script scrivere le seguenti righe:


notbefore_ts=$(date --date="${notbefore}" "+%s")
notafter_ts=$(date --date="${notafter}" "+%s")
now=$(date "+%s")
if [ "${now}" -ge "${notbefore_ts}" -a "${now}" -le "${notafter_ts}" ]; then
echo "certificato non scaduto"
else
echo "certificato scaduto"
fi

Debug di un p7m (esperti)

A volte potrebbe essere importante poter capire ed analizzare il contenuto di un p7m byte per byte in ogni struttura interna e poter estrarre solo delle parti significative per verificarne il contenuto.
Openssl offre un sottocomando molto utile per tale operazione: asn1parse
Per vedere il risultato basta lanciare il comando:

 openssl asn1parse -i -inform der -in file-der.pdf.p7m 

L’output sarà qualcosa del genere:

0:d=0 hl=4 l=12818 cons: SEQUENCE
4:d=1 hl=2 l= 9 prim: OBJECT :pkcs7-signedData
15:d=1 hl=4 l=12803 cons: cont [ 0 ]
19:d=2 hl=4 l=12799 cons: SEQUENCE
23:d=3 hl=2 l= 1 prim: INTEGER :01
26:d=3 hl=2 l= 15 cons: SET
28:d=4 hl=2 l= 13 cons: SEQUENCE
30:d=5 hl=2 l= 9 prim: OBJECT :sha256
41:d=5 hl=2 l= 0 prim: NULL
43:d=3 hl=4 l=10302 cons: SEQUENCE
47:d=4 hl=2 l= 9 prim: OBJECT :pkcs7-data
58:d=4 hl=4 l=10287 cons: cont [ 0 ]
62:d=5 hl=4 l=10283 prim: OCTET STRING [HEX DUMP]:255044462D312E340 ...
...
10458:d=9 hl=2 l= 3 prim: OBJECT :organizationName
10463:d=9 hl=2 l= 10 prim: UTF8STRING :Quoll tech
...
12045:d=5 hl=2 l= 13 cons: SEQUENCE
12047:d=6 hl=2 l= 9 prim: OBJECT :sha256
12058:d=6 hl=2 l= 0 prim: NULL
12060:d=5 hl=3 l= 228 cons: cont [ 0 ]
12063:d=6 hl=2 l= 24 cons: SEQUENCE
12065:d=7 hl=2 l= 9 prim: OBJECT :contentType
12076:d=7 hl=2 l= 11 cons: SET
12078:d=8 hl=2 l= 9 prim: OBJECT :pkcs7-data
12089:d=6 hl=2 l= 28 cons: SEQUENCE
12091:d=7 hl=2 l= 9 prim: OBJECT :signingTime
12102:d=7 hl=2 l= 15 cons: SET
12104:d=8 hl=2 l= 13 prim: UTCTIME :180514085900Z
12119:d=6 hl=2 l= 47 cons: SEQUENCE
12121:d=7 hl=2 l= 9 prim: OBJECT :messageDigest
...
12291:d=5 hl=2 l= 13 cons: SEQUENCE
12293:d=6 hl=2 l= 9 prim: OBJECT :rsaEncryption
12304:d=6 hl=2 l= 0 prim: NULL
12306:d=5 hl=4 l= 512 prim: OCTET STRING [HEX DUMP]:4549F88C2174EF74F....

Dove nella prima colonna troviamo l’offset in byte dell’inizio dell’oggetto (OBJECT) o del singolo campo (per esempio OCTET STRING, UTCTIME), mentre con “hl” si indica lo scostamento relativo all’header del campo stesso prima del dato effettivo (che quindi si troverà a partire dall’offset + hl). Infine è molto importante il valore di “l” nella terza colonna che indica la lunghezza in byte di quel dato.
Per fare un esempio concreto se volessimo estrarre il contenuto e quindi l’allegato di questo p7m dovremmo vedere l’oggetto “:pkcs7-data” nel campo di tipo OCTET STRING

62:d=5 hl=4 l=10283 prim: OCTET STRING [HEX DUMP]:255044462D312E340 ...

Quindi per estrarre l’allegato bisogna prendere 10283 byte a partire dal byte 66 (62 + 4) del file p7m in formato DER (importante che sia in formato DER e non PEM dato che gli offset e la lunghezza e tutti i dati visualizzati si riferiscono al DER)

Quindi lo estraiamo semplicemente lanciando il comando:

 dd in=file-der.pdf.p7m of=file.pdf skip=$((62 +4)) bs=1 count=10283 

Estrazione del documento originale: metodo alternativo

Nel precedente articolo sui p7m abbiamo visto due metodi per estrarre l’allegato del p7m ora ne presentiamo un altro con il sottocomado asn1parese di openssl.

data=$(openssl asn1parse -inform der -i -in file-der.pdf.p7m | sed -n '/:pkcs7-signedData/,/OCTET STRING/p' | grep 'OCTET STRING'))
offset=$(echo $data| awk '{print $1}'| awk -F ":" {'print $1}')
hl=$(echo $data| awk '{print $2}'| awk -F "=" '{print $2}')
l=$(echo $data| awk '{print $3}'| awk -F "=" '{print $2}')
dd if=file-der.pdf.p7m of=file.pdf skip=(($offset +$hl)) bs=1 count=$l

Estrazione del riferimento temporale associato alla firma del p7m

Per estrarre il riferimento temporale (il timestamp) associato al momento della firma basta estrarre dal contenuto decodificato con asn1parse l’UTCTIME dell’oggetto :signingTime

12089:d=6 hl=2 l= 28 cons: SEQUENCE
12091:d=7 hl=2 l= 9 prim: OBJECT :signingTime
12102:d=7 hl=2 l= 15 cons: SET
12104:d=8 hl=2 l= 13 prim: UTCTIME :180514085900Z

e quindi lo estraiamo con:

 utctime=$(dd if=file-der.pdf.p7m of=- skip=$((12104 +2)) bs=1 count=13) 

Il risultato $utctime è in formato ASN.1  ed ha il formato della data “YYMMDDhhmm[ss]Z” (quindi anno in 2 cifre, mese poi giorno poi ora e minuti e secondi e quidi il fuso “Z” che indica UTC).
Automatizzando il tutto potremmo scrivere:

timestamp_orig=`openssl asn1parse -inform DER -in "file-der-pdf-p7m"|sed -n -e '/:signingTime/,/UTCTIME/p'| grep UTCTIME | awk '{print $7}'| sed 's/[:Z]//g'`
timestamp_yymmgg="$(echo $timestamp_orig | sed -e 's/^\([0-9][0-9]\)\([0-9][0-9]\)\([0-9][0-9]\)\([0-9][0-9]\)\([0-9][0-9]\)\([0-9][0-9]\)/-- ::/') UTC"
timestamp_readable=$(date --date="${timestamp_yymmgg}" "+%Y-%m-%d %H:%M:%S %Z")
echo $timestamp_readable

Verifica manuale della firma passo dopo passo

Naturalmente per verificare la firma possiamo utilizzare il solito comando “openssl smime -verify …” ma ora stiamo analizzando la struttura del formato DER tramite il sottocomando asn1parse e quindi vediamo come farlo analizzando i vari componenti.
Per prima cosa estraiamo il certificato dal p7m (nel file cert.pem) e quindi la chiave pubblica (nel file pubkey.pem):

openssl pkcs7 -inform DER -in file-der.pdf.p7m -print_certs -out cert.pem
openssl x509 -inform pem -in cert.pem -noout -pubkey > pubkey.pem

Scarichiamoci la firma rsa dall’oggetto :rsaEncryption nel campo di tipo OCTET STRING

12293:d=6 hl=2 l= 9 prim: OBJECT :rsaEncryption
12304:d=6 hl=2 l= 0 prim: NULL
12306:d=5 hl=4 l= 512 prim: OCTET STRING [HEX DUMP]:4549F88C2174EF74F....

e salviamolo nel file rsa-signed.bin

 dd if=file-derpdf.p7m of=rsa-signed.bin bs=1 skip=$(( 12306 + 4 )) count=512

Per sicurezza controlliamo il contenuto che sia identico a quello visto con asn1parse con il comando

hexdump -C rsa-signed.bin

A questo punto verifichiamo la firma salvando il risultato della verifica nel file verified.bin

openssl rsautl -verify -pubin -inkey pubkey.pem < rsa-signed.bin > verified.bin

Il risultato di tale verifica è anch’esso un file codificato DER e quindi lo destrutturiamo con

openssl asn1parse -inform der -in verified.bin

ottenendo l’output seguente:

0:d=0 hl=2 l= 49 cons: SEQUENCE
2:d=1 hl=2 l= 13 cons: SEQUENCE
4:d=2 hl=2 l= 9 prim: OBJECT :sha256
15:d=2 hl=2 l= 0 prim: NULL
17:d=1 hl=2 l= 32 prim: OCTET STRING [HEX DUMP]:4D4B399DBD896FC1A4150CC488CC56069176A5369C4D51F9B4216D177A13240A

dove l’ultima riga non è altro che hash associato allo sha256 del contenuto del file originale file-der.pdf ed i vari attributi di firma utilizzati (come ad esempio il timestamp).
Per vedere che l’hash corrisponde dobbiamo scaricarci la parte dell’oggetto :sha256 dal nostro file p7m

12047:d=6 hl=2 l= 9 prim: OBJECT :sha256
12058:d=6 hl=2 l= 0 prim: NULL
12060:d=5 hl=3 l= 228 cons: cont [ 0 ]

e più precisamente quella relativa a “cont [ 0 ]” compresa del suo header e salvarla nel file signed-256.bin

 dd if=../file-der.pdf.p7m of=signed-256.bin bs=1 skip=12060 count=$((3+228))

Bisogna cambiare il tipo di struttura ASN da SEQUENCE a SET mettendo “1” (hex \x31) come primo carattere invece di “0“.

echo -ne "\x31" | dd conv=notrunc bs=1 count=1 of=signed-256.bin

A questo punto possiamo verificare che il suo hash sha256 corrisponde con il precedente:

sha256sum signed-256.bin
4d4b399dbd896fc1a4150cc488cc56069176a5369c4d51f9b4216d177a13240a signed-256.bin

Comando p7m (p7m viewer)

Come conclusione di questo articolo presento un riassunto di tutto tramite il programma p7m (p7m viewer), un semplice script che ho creato per poter automatizzare tutte le operazioni sui file p7m.
Il programma potete scaricarlo da github .
p7m funziona sotto Linux come script o in modalità grafica in almeno KDE, GNOME, XFCE dato che è possibile integrarlo facilmente con i vari file manager Dolphin, Nautilux e Thunar (diventando un vero e proprio p7m viewer).

Thunar (XFCE): menù contestuale di p7m viewer
Thunar (XFCE): menù contestuale di p7m viewer
Dolphin (KDE): menù contestuale di p7m viewer
Dolphin (KDE): menù contestuale di p7m viewer
Nautilus (GNOME): menù contestuale di p7m viewer
Nautilus (GNOME): menù contestuale di p7m viewer

Basta lanciarlo con un file p7m come argomento per poter aprire direttamente l’allegato (è un p7m viewer) verificando in automatico la firma.
In automatico scarica il portafoglio dei certificatotori dal CNIPA e li aggiorna dopo 15 giorni (configurabile).
Permette, inoltre di scaricare e visualizzare il certificato di chi ha firmato, di estrarre l’allegato e visualizzarlo, di ispezionare il p7m tramite il sottocomando asn1parse.
Per problemi o richieste di modifiche è possibile utilizzare direttamente Github issue.

Esempio1: verifica firma:

p7m -v file.p7m
Verifica certificato riuscita (riferimento temporale entro la validità del certificato)

La firma è stata fatta nel 2017-04-26 13:45:18 CEST da Enio Carboni
Ente: Quoll tech
CF: CRBNE...
Certificatore: InfoCert Firma Qualificata 2

Esempio2: apertura file con certificato scaduto

p7m file2.pdf
Verifica certificato non riuscita (certificato scaduto) ma riferimento temporale entro la validità del certificato

La firma è stata fatta nel 2013-12-11 16:06:00 CET da Paolo Rossi
Ente: NON PRESENTE
CF: PLO...
Certificatore: InfoCert Firma Qualificata

Provo comunque ad estrarre il contenuto?

Verification failure
140123291941632:error:21075075:PKCS7 routines:PKCS7_verify:certificate verify error:pk7_smime.c:336:Verify error:certificate has expired [exit code 4]

Esempio3: visualizzare il certificato in modalità grafica

Per usare finestra grafiche invece della console aggiungere l’opzione -g al comado p7m

p7m -g -c file2.pdf

Riferimenti:

p7m firma digitale: come estrarre il contenuto su quoll.it
Script p7m su github.com
getTrustCAP7m su github.com
PEM(Privacy Enhanced Electronic Mail) su Wikipedia
RFC 1421 – PEM (Privacy Enhancement Electronic Mail) su tools.ietf.org
DER (Distinguished Encoding Rules) su Wikipedia
DER (Distinguished Encoding Rules) – pdf su itu.int
Timestamps ASN.1 (utctime) su  obj-sys.com
asn1parse (openssl) su qistoph.blogspot.it : manual verify PKCS#7 signed data with OpenSSL
asn1parse (openssl) su openssl.org
Openssl su openssl.org
CNIPA ora Agid su agid.gov.it
KDE su kde.org
GNOME su gnome.org
XFCE su xfce.org
Dolphin su kde.org
Nautilus su gnome.org
Thunar su xfce.org

6 pensieri su “p7m viewer firma digitale: come estrarre il contenuto – parte seconda

  1. Bello script e grazie per averlo condiviso,
    ho fatto una piccolissima correzione su Ubuntu Mate,
    ho le bubble notifications, e ‘zenity –notification’ mi va in hang, quindi non scarica i certificati e si blocca tutto lo script.
    Quindi ho solo aggiunto un timeout, riga 257 è diventata:
    zenity –notification –title=”$3″ –text=”$2″ –timeout=1 2>/dev/null
    Ciao e grazie

  2. ciao, ho fatto una versione su una sola linea di comando, per i file senza header e footer. che mi serve dentro gli script.
    Se vi serve, eccola:

    cat <(echo "—–BEGIN PKCS7—–") senza_header_xml.xml.p7m <(echo "—–END PKCS7—–")|grep '[^[:space:]]' |openssl pkcs7 -outform der |openssl smime -verify -inform der -noverify

  3. Ciao, avrei un problema in fase di estrazione di una fattura elettronica XML dal file P7M.
    Attualmente questa operazione viene svolta senza problemi tramite riga di comando con openssl:

    openssl.exe smime -inform DER -verify -in fattura.p7m -out fattura.xml

    Ultimamente sto avendo problemi con i file firmati con firma EIDAS, open ssl infatti restituisce il seguente errore:

    Error reading S/MIME message
    6984:error:0909006C:PEM routines:get_name:no start line:crypto\pem\pem_lib.c:745:Expecting: PKCS7
    Error reading S/MIME message
    10788:error:0D0680A8:asn1 encoding routines:asn1_check_tlen:wrong tag:crypto\asn1\tasn_dec.c:1130:
    10788:error:0D07803A:asn1 encoding routines:asn1_item_embed_d2i:nested asn1 error:crypto\asn1\tasn_dec.c:290:Type=PKCS7_ISSUER_AND_SERIAL
    10788:error:0D08303A:asn1 encoding routines:asn1_template_noexp_d2i:nested asn1 error:crypto\asn1\tasn_dec.c:627:Field=issuer_and_serial, Type=PKCS7_SIGNER_INFO
    10788:error:0D08303A:asn1 encoding routines:asn1_template_noexp_d2i:nested asn1 error:crypto\asn1\tasn_dec.c:596:Field=signer_info, Type=PKCS7_SIGNED
    10788:error:0D08303A:asn1 encoding routines:asn1_template_noexp_d2i:nested asn1 error:crypto\asn1\tasn_dec.c:627:
    10788:error:0D08403A:asn1 encoding routines:asn1_template_ex_d2i:nested asn1 err or:crypto\asn1\tasn_dec.c:477:Field=d.sign, Type=PKCS7

    Avresti qualche idea a riguardo?
    Grazie

    1. Ciao Nicola,
      per poterti rispondere avrei bisogno di un file p7m che presenta il problema.
      Avresti la possibilità di mandarmelo in privato tramite email così controllo velocemente se riesco a darti una soluzione (enio.carboni at quoll.it)?

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

71 + = 73