Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Crittografia e firma XML in Java - Apache Santuario

Come implementare i principali standard per la sicurezza degli XML tramite Java e il progetto Apache Santuario.
Come implementare i principali standard per la sicurezza degli XML tramite Java e il progetto Apache Santuario.
Link copiato negli appunti

La diffusione di utilizzo dell'XML (eXtensible Markup Language) ha portato alla richiesta di misure di sicurezza volte a proteggere privacy, confidenzialità e integrità dei documenti XML, sfruttandone le peculiarità e salvaguardandone i vantaggi. Ad esempio, può nascere l'esigenza o l'opportunità di criptare solo alcuni elementi, o di criptare elementi diversi con chiavi diverse per poter distribuire lo stesso documento a diversi soggetti. Il W3C (World Wide Web Consortium) ha definito di conseguenza una serie di standard di sicurezza integrati con le soluzioni XML attraverso vocabolari e regole di elaborazione, in modo da soddisfare i crescenti requisiti di sicurezza.

Tra i principali standard per la sicurezza vi sono quelli volti a garantire integrità e autenticazione (XML Digital Signature) e confidenzialità (XML Encryption). In questo articolo, dopo un'introduzione all'XML Security, presenteremo Apache Santuario, un progetto che punta ad offrire un'implementazione di questi standard XML proponendo librerie sia per Java che per C++. Per mostrare le potenzialità del framework, affronteremo alcuni esempi di utilizzo in Java coprendo i principali casi d'uso, ossia crittografia con chiave simmetrica e asimmetrica e firma digitale.

L'articolo è costituito dalle seguenti sezioni:

  • XML Security: introduzione
  • Crittografia e firma XML in Java, Apache Santuario
  • Apache Santuario: crittografia XML
  • Apache Santuario: firma XML

Apache Santuario richiede Java 1.5 (o superiore). Nell'articolo verranno presentati esempi programmatici dell'utilizzo del framework. Per svolgerli bisognerà assicurarsi di avere correttamente installato il Java Development Kit 1.5 (o superiore). Nell'archivio che verrà allegato a fine trattazione sarà possibile trovare un progetto Eclipse con gli esempi successivamente descritti.

XML Security: introduzione

La sicurezza è fondamentale per garantire il corretto svolgimento delle transazioni, per mantenere privacy e confidenzialità e per assicurare che le informazioni siano usate nel modo appropriato. I vecchi sistemi di sicurezza basati su uno strato fisico non scalano efficacemente su Internet a causa della natura eterogenea dei sistemi hardware e software e dei conflitti amministrativi, applicativi, o più generale per le diverse regole di sicurezza. Ne è derivata la richiesta per standard estensibili in grado di venire incontro o adattarsi a requisiti che cambiano costantemente, in grado di interagire con i vecchi sistemi ereditati e di essere utilizzati in modo modulare, evitando cioè di dovere essere vincolati alle porzioni inutilizzate.

Il requisito essenziale di questi standard è che siano perfettamente in grado di lavorare con contenuti creati usando l'XML, con l'obiettivo di salvaguardare nel migliore modo possibile i vantaggi e le funzionalità proprie dell'XML.

Le vecchie tecnologie per la sicurezza fornivano un set di algoritmi e tecnologie che spesso risultano inappropriate per le applicazioni di sicurezza per l'XML. Tra i principali motivi vi è il fatto che spesso queste tecnologie utilizzano formati binari, a scapito della leggibilità dei documenti e dell'utilizzo dei comuni parser XML, o non ne supportano funzionalità di gestione dei contenuti, ad esempio l'XPath.

L'idea alla base dell'XML Security è quella di definire strutture e regole di elaborazione che possano essere condivise dalle applicazioni utilizzando tools comuni ed evitando customizzazioni eccessive delle applicazioni che ne dovrebbero garantire la sicurezza. Nell'XML Security vi è quindi il riuso di concetti e algoritmi al fine di garantire interoperabilità tra una vasta scala di applicazioni.

Tra i principali XML Security standards possiamo annoverare:

Standard Descrizione
Integrità e firma XML Signatures
Confidenzialità XML Encryption
Canonizzazione Canonical XML
Gestione delle chiavi XML Key Management Specification
Autenticazione e autorizzazioni Security Assertion Markup Language
Regole per l'autorizzazione XML Access Control Markup Language

Tra le principali applicazioni dell'XML Security possiamo annoverare la sicurezza per i Web Services, privacy (Platform for Privacy Preferences) e DRM (Digital Rights Management, ad esempio l'eXtensible Rights Markup Language 2.0).

Standard Java e Apache Santuario

XML Encryption e XML Signatures costituiscono gli standard di riferimento del W3C per la criptografia e l'autenticazione degli XML. Queste due specifiche sono già esposte nella guida ai linguaggi XML, per cui ci limiteremo a una breve introduzione.

XML Encryption: lo standard punta alla crittografia di dati e tags di un documento XML, consentendo di crittografare solo alcune parti, seguendo l'idea che di norma non è necessario crittografare l'intero documento. Inoltre, la possibilità di crittografare diverse parti del documento con diverse chiavi consente di distribuire lo stesso documento XML a diversi destinatari, con i singoli destinatari in grado di decifrare solo le parti di propria competenza. Le informazioni criptate vengono racchiuse da un tag e i dati criptati rappresentati con la stringa risultante dalla crittografia. Questa crittografia fornisce dunque un livello di controllo granulare, consentendo di diversificare l'accesso al documento in base all'utente. Inoltre, poiché ad essere cifrati sono i dati ma non il file, il documento è riconoscibile dai parser XML e come tale elaborato.

XML Signatures: tramite la firma digitale si punta a garantire l'autenticazione e l'integrità dei documenti, ossia stabilire l'identità del mittente e verificare che non vi siano state alterazioni durante il trasporto. Per compensare variazioni dovute ai parser o al file system, l'XML Signatures dipende strettamente dal concetto di canonizzazione, che punta a riconoscere l'equivalenza tra due documenti XML anche in presenza di ambienti eterogenei. Per applicare una firma al contenuto, viene utilizzato un algoritmo di canonizzazione che sfrutta dati e tags al fine di creare una firma unica. Il client che riceve le informazioni decritta la firma distinguendo tra contenuto crittato prima di aggiungere la firma e contenuto crittato dopo la firma. Successivamente avviene la decrittazione di quanto segue la firma e la verifica dell'integrità dei dati applicando lo stesso algoritmo di canonizzazione al contenuto decrittato, in modo da confrontare il risultato con la firma inclusa nel documento XML. Utilizzando XML Signatures in combinazione con XML Encryption è possibile assicurare che i dati inviati siano gli stessi di quelli ricevuti, senza compromettere il concetto di pubblico mirato.

Gli standards in Java: In Java si sono avviati due progetti per fornire API per i due standard, rispettivamente il progetto 105 (JSR 105) per definire API standard per generare e validare firme XML in Java e il progetto 106 (JSR 106) per definire API standard per la crittografia XML in Java. Però, mentre il progetto 105 è arrivato ad uno stato finale, lo stesso non si può dire per il progetto 106, ritirato nel 2010, pertanto Java non offre supporto nativo all'XML Encryption.

Come risultato, per disporre delle API necessarie a crittare documenti XML seguendo le specifiche dell'XML Encryption, dobbiamo fare affidamento su librerie di terze parti. In questo articolo introdurremo la libreria per Java messa sviluppata nell'ambito del progetto Apache Santuario.

Apache Santuario ha avuto origine attorno al 2000 nell'Università Di Siegen, Germania, in collaborazione con alcune aziende greche ed è cofinanziato dalla Commissione Europea (programma ISIS). Nel 2001, al fine di promuovere l'uso della firma digitale nell'XML, si decise di rendere il codice liberamente disponibile al pubblico portando il progetto sotto l'ombrello dell'Apache Software Foundation e relativa licenza Apache. L'esperienza ha avuto successo e nel 2006 è stato votato come Apache TLP (Top Level Project) e rinominato Apache Santuario.

Apache Santuario punta a fornire l'implementazione dei principali standard di sicurezza per l'XML, attualmente XML-Signature e XML Encryption. Sono disponibili distribuzioni per C++ e Java, in questo articolo approfondiremo la distribuzione Java, arrivata alla versione 1.5.6. Dalla pagina di download è possibile scaricare il progetto nella versione stabile, nella versione beta (attualmente 2.0.0), o nelle versioni precedenti. E' anche possibile importare il progetto d'esempio partendo dal repository Apache-SVN.

Nelle prossime sezioni seguiranno esempi di utilizzo del framework in cui si mostrerà come utilizzare le API per crittare e decrittare e successivamente come applicare e verificare la firma XML. Gli esempi sono stati realizzati utilizzando un progetto Eclipse con la seguente struttura.

ProgettoSantuario

ALT_TEXT

I package sono divisi in base alle sezioni dell'articolo, con un package (example.utilities) destinato ad accogliere classi di uso comune. Nella cartella libs sono presenti le librerie importate nel progetto, librerie prelevate dall'omonima cartella del progetto Apache Santuario. La cartella build verrà utilizzata per ospitare i files generati dagli esempi, mentre la cartella samples contiene alcuni files preesistenti che verranno utilizzati dall'esempio sulla firma digitale. Sarà possibile trovare il progetto con gli esempi descritti nell'archivio allegato all'articolo.

Apache Santuario: crittografia XML

In questa sezione vengono presentati esempi di crittografia con chiave simmetrica e asimmetrica. Inizieremo con un caso semplice per vedere come il framework consente di criptare un documento, gettando le basi per affrontare altri esempi in cui si chiede di criptare solo una porzione o elementi differenti con chiavi diverse.

Crittografia XML: Chiave simmetrica

Nella crittografia a chiave simmetrica la funzione di codifica e quella di decodifica usano la stessa chiave privata (o chiavi private diverse ma correlate), soluzione che pone problemi di sicurezza legati alla distribuzione della chiave. Partendo dalla crittografia a chiave simmetrica, presenteremo esempi di complessità crescente con i quali esploreremo diverse possibilità offerte dal frameworks.

Iniziamo introducendo le classi del package example.utilities che contengono metodi sfruttati negli esempi che seguiranno. La prima classe, DomAndParserUtilities, contiene metodi statici per scrivere su file e caricare da file un documento XML.

public class DomAndParserUtilities {
	public static void outputDocToFile(Document doc, String fileName) throws Exception {
        File encryptionFile = new File(fileName);
        FileOutputStream f = new FileOutputStream(encryptionFile);
        TransformerFactory factory = TransformerFactory.newInstance();
        Transformer transformer = factory.newTransformer();
        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
        DOMSource source = new DOMSource(doc);
        StreamResult result = new StreamResult(f);
        transformer.transform(source, result);
        f.close();
        System.out.println("XML creato: " + encryptionFile.toURI().toURL().toString());
	}
	public static Document loadEncryptionDocument(String fileName) throws Exception {
        File encryptionFile = new File(fileName);
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);
        DocumentBuilder db = dbf.newDocumentBuilder();
        Document document = db.parse(encryptionFile);
        System.out.println("XML letto: " + encryptionFile.toURI().toURL().toString());
        return document;
    }
}

La seconda classe, KeysUtilities di generare chiavi e di memorizzare su file e caricare da file una KEK (Key Encryption Key), chiave utilizzata per proteggere altre chiavi.

public class KeysUtilities {
	public static SecretKey GenerateAndStoreKeyEncryptionKey(String fileName) throws Exception {
        String jceAlgorithmName = "DESede";
        KeyGenerator keyGenerator = KeyGenerator.getInstance(jceAlgorithmName);
        SecretKey kek = keyGenerator.generateKey();
        byte[] keyBytes = kek.getEncoded();
        File kekFile = new File(fileName);
        FileOutputStream f = new FileOutputStream(kekFile);
        f.write(keyBytes);
        f.close();
        System.out.println("Key encryption key file: " + kekFile.toURI().toURL().toString());
        return kek;
    }
    public static SecretKey GenerateDataEncryptionKey() throws Exception {
        String jceAlgorithmName = "AES";
        KeyGenerator keyGenerator = KeyGenerator.getInstance(jceAlgorithmName);
        keyGenerator.init(128);
        return keyGenerator.generateKey();
    }
    public static SecretKey loadKeyEncryptionKey(String fileName) throws Exception {
    	String jceAlgorithmName = "DESede";
        File kekFile = new File(fileName);
        System.out.println("Key encryption key file: " + kekFile.toURI().toURL().toString());
        DESedeKeySpec keySpec = new DESedeKeySpec(JavaUtils.getBytesFromFile(fileName));
        SecretKeyFactory skf = SecretKeyFactory.getInstance(jceAlgorithmName);
        SecretKey key = skf.generateSecret(keySpec);
        return key;
    }
}

Esempio 1: criptare e decriptare un documento XML

Il primo esempio proposto ricalca l'esempio presente nella distribuzione di Apache Santuario (samples/org/apache/xml/security/samples/encryption/) e consiste di due classi, Encrypter1 e Decrypter1. Con la prima classe andremo a crittare un semplice XML utilizzando una chiave simmetrica, con la seconda invertiremo il procedimento.

Encrypter1

public class Encrypter1{
	static {
        org.apache.xml.security.Init.init();
    }
    private static Document createSampleDocument() throws Exception {
        javax.xml.parsers.DocumentBuilderFactory dbf = javax.xml.parsers.DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);
        javax.xml.parsers.DocumentBuilder db = dbf.newDocumentBuilder();
        Document document = db.newDocument();
        Element root = document.createElementNS("http://www.apache.org/ns/#app1", "apache:RootElement");
        root.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:apache", "http://www.apache.org/ns/#app1");
        document.appendChild(root);
        root.appendChild(document.createTextNode("n"));
        Element childElement = document.createElementNS("http://www.apache.org/ns/#app1", "apache:foo");
        childElement.appendChild(document.createTextNode("Testo d'esempio"));
        root.appendChild(childElement);
        return document;
    }
    public static void main(String unused[]) throws Exception {
        Document document = createSampleDocument();
        //chiave AES per crittare l'elemento
        Key symmetricKey = KeysUtilities.GenerateDataEncryptionKey();
        //chiave DESede per crittare la chiave simmetrica
        Key kek = KeysUtilities.GenerateAndStoreKeyEncryptionKey("build/kek/filekey1.txt");
        String algorithmURI = XMLCipher.TRIPLEDES_KeyWrap;
        XMLCipher keyCipher = XMLCipher.getInstance(algorithmURI);
        keyCipher.init(XMLCipher.WRAP_MODE, kek);
        EncryptedKey encryptedKey = keyCipher.encryptKey(document, symmetricKey);
        //crittografia del documento
        Element rootElement = document.getDocumentElement();
        algorithmURI = XMLCipher.AES_128;
        XMLCipher xmlCipher = XMLCipher.getInstance(algorithmURI);
        xmlCipher.init(XMLCipher.ENCRYPT_MODE, symmetricKey);
        //impostazione delle informazioni nel documento
        EncryptedData encryptedData = xmlCipher.getEncryptedData();
        KeyInfo keyInfo = new KeyInfo(document);
        keyInfo.add(encryptedKey);
        encryptedData.setKeyInfo(keyInfo);
        //modifica del documento e scrittura del file crittato
        xmlCipher.doFinal(document, rootElement, true);
        DomAndParserUtilities.outputDocToFile(document, "build/symmetric/encrypted1.xml");
    }
}

Per prima cosa, occorre dire che per utilizzare le funzioni della libreria XML Security occorrerà invocare il metodo statico org.apache.xml.security.Init.init(), ciò va fatto sia in fase di crittografia che di decrittografia, anche se in qualcuno degli esempi che seguiranno potrà non essere posto in evidenza.

La classe contiene un metodo statico (createSampleDocument) nel quale viene creato un semplice documento XML utilizzando le API DOM (Document Object Model), documento basato sulla radice e su un elemento figlio contenente una stringa di testo. Questo metodo viene richiamato dal main per generare l'XML da crittografare. Successivamente, utilizzando il metodo GenerateDataEncryptionKey, viene generata una chiave simmetrica, chiave che viene a sua volta crittata tramite un'ulteriore chiave (KEK) generata e memorizzata (kek/filekey1.txt) tramite il metodo GenerateAndStoreKeyEncryptionKey. Poiché la KEK protegge l'effettiva chiave, la KEK ci consente di aggiungere le informazioni relative alla chiave al documento XML.

La criptografia avviene per mezzo del metodo doFinal, al quale vengono passati tre parametri: il documento da criptare, l'elemento radice del documento e un terzo parametro booleano che impostato su true informa che deve esserne codificato solo il contenuto, escludendo pertanto dalla crittografia l'elemento radice, che invece sarebbe stato criptato anch'esso impostando il parametro su false. Da notare che il metodo doFinal agisce direttamente sul documento XML che viene passato, pertanto in seguito all'invocazione il documento verrà criptato e sarà pronto per essere scritto su file, come avviene nel successivo passaggio (outputDocToFile) memorizzandolo nel file encrypted1.xml.

Esplorando il file generato (symmetric/encrypted1.xml) si potrà vedere che l'elemento figlio è stato rimpiazzato dall'elemento EncyptedData contenente informazioni riguardanti la modalità con cui è avvenuta la criptazione (EncryptionMethod), informazioni sulla chiave (KeyInfo, contiene la chiave e le informazioni sul metodo con la quale è stata criptata, nel nostro caso il Triple DES, e infine il contenuto originario dell'elemento, criptato (CipherData). Ora ossiamo passare alla decriptazione.

Decrypter1

public class Decrypter1{
	static {
        org.apache.xml.security.Init.init();
    }
    public static void main(String unused[]) throws Exception {
        Document document = DomAndParserUtilities.loadEncryptionDocument("build/symmetric/encrypted1.xml");
        Element encryptedDataElement = (Element) document.getElementsByTagNameNS(
                EncryptionConstants.EncryptionSpecNS,
                EncryptionConstants._TAG_ENCRYPTEDDATA).item(0);
        //caricamento della chiave per decrittare la chiave
        Key kek = KeysUtilities.loadKeyEncryptionKey("build/kek/filekey1.txt");
        XMLCipher xmlCipher = XMLCipher.getInstance();
        xmlCipher.init(XMLCipher.DECRYPT_MODE, null);
        xmlCipher.setKEK(kek);
        //documento decrittato e memorizzato
        xmlCipher.doFinal(document, encryptedDataElement);
        DomAndParserUtilities.outputDocToFile(document, "build/symmetric/decrypted1.xml");
    }
}

La decrittazione avviene in 3 fasi: nella prima fase, viene isolato il nodo da decrittare, partendo dal documento precedentemente crittato (loadEncryptionDocument); successivamente viene caricata la chiave di decrittazione dal file contenente la KEK (filekey1.txt), chiave che servirà per decrittare la chiave contenuta a sua volta nel file encrypted1.xml. A questo punto le informazioni necessarie per decrittare sono al completo e può avvenire la decrittazione del documento e successiva memorizzazione su un file, che avviene ancora una volta mediante un'istanza della classe XMLCipher generando un xml che questa volta sarà in chiaro, symmetric/decrypted1.xml. Notiamo che l’elemento che occorre condividere tra le due esecuzioni è il file contenente la KEK.

In questi esempi la classe XMLCipher, fornita dalla libreria XMLSecurity, è la classe cardine, in quanto fornisce i metodi per criptare e decriptare documenti ed elementi (in entrambi i casi il metodo doFinal) e i metodi per raccogliere e leggere le informazioni principali come la KEK o la chiave simmetrica; essa è stata sviluppata in modo da assomigliare alla classe javax.crypto.Cipher per facilitare la comprensione delle sue funzionalità.

Esempio 2: criptare e decriptare un elemento

Nella sezione introduttiva si è visto che uno dei motivi che ha portato alla richiesta di standard di sicurezza specifici per l’XML è l'opportunità di criptare solo le sezioni che richiedano di preservare la riservatezza dei dati. Segue un esempio nel quale mostreremo come criptare solo uno degli elementi di un documento XML.

Da notare che, come faremo in questo esempio, possiamo utilizzare senza modifiche la classe Decrypter1 per la decrittazione, a patto di utilizzare gli stessi nomi dei files scelti in precedenza per memorizzare la KEK e l'XML criptato. L'unico inconveniente è che così facendo sovrascriveremo i files precedentemente creati. Se si vuole evitare di sovrascrivere i vecchi files, si dovranno modificare i nomi assegnati nella seguente classe e conseguentemente nella classe Decrypter1.

Passiamo alla realizzazione della classe Encrypter1_1, essa è organizzata in modo simile alla precedente, una chiamata statica al metodo org.apache.xml.security.Init.init(), un metodo createSampleDocument per creare il documento XML e il main nel quale eseguiremo i passi per criptare un elemento. Di seguito il metodo createSampleDocument.

private static Document createSampleDocument() throws Exception {
        javax.xml.parsers.DocumentBuilderFactory dbf =
            javax.xml.parsers.DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);
        javax.xml.parsers.DocumentBuilder db = dbf.newDocumentBuilder();
        Document document = db.newDocument();
        //1 (root)
        Element root =
            document.createElementNS("http://www.apache.org/ns/#app1", "apache:RootElement");
        root.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:apache", "http://www.apache.org/ns/#app1");
        root.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:child", "http://www.apache.org/ns/#app2");
        document.appendChild(root);
        root.appendChild(document.createTextNode("n"));
        //1.1 (child)
        Element childElement =
            document.createElementNS("http://www.apache.org/ns/#app1", "child");
        root.appendChild(childElement);
        root.appendChild(document.createTextNode("n"));
        //1.1.1 (child 1 of child)
        Element childElement2 =
                document.createElementNS("http://www.apache.org/ns/#app2", "child:elA");
        childElement2.appendChild(
                document.createTextNode("Testo d'esempio 1"));
        childElement.appendChild(childElement2);
        //1.1.1 (child 2 of child)
        Element childElement3 =
                document.createElementNS("http://www.apache.org/ns/#app2", "child:elB");
        childElement3.appendChild(
                document.createTextNode("Testo d'esempio 2"));
        childElement.appendChild(childElement3);
        return document;
    }

Questa volta il documento XML sarà formato da una radice, un figlio e due foglie. Nel main andremo a criptare solo il primo elemento.

public static void main(String unused[]) throws Exception {
        Document document = createSampleDocument();
        //chiave AES per crittare l'elemento
        Key symmetricKey = KeysUtilities.GenerateDataEncryptionKey();
        //chiave DESede per crittare la chiave simmetrica
        Key kek = KeysUtilities.GenerateAndStoreKeyEncryptionKey("build/kek/filekey1.txt");
        String algorithmURI = XMLCipher.TRIPLEDES_KeyWrap;
        XMLCipher keyCipher = XMLCipher.getInstance(algorithmURI);
        keyCipher.init(XMLCipher.WRAP_MODE, kek);
        EncryptedKey encryptedKey = keyCipher.encryptKey(document, symmetricKey);
        //selezione dell'elemento da crittare
        Element rootElement = document.getDocumentElement();
        NodeList listNode1 = rootElement.getChildNodes();
        Element child=null;
        int count=0;
        for(int i=0;i<listNode1.getLength();i++){
        	if(listNode1.item(i).getNodeType()==Node.ELEMENT_NODE){
        		count++;
        		if(count==1){
        			child = (Element) listNode1.item(i);
            		break;
        		}
        	}
        }
        Element childOfChild=null;
        int count2=0;
        NodeList listNode2 = child.getChildNodes();
        for(int i=0;i<listNode2.getLength();i++){
        	if(listNode2.item(i).getNodeType()==Node.ELEMENT_NODE){
        		count2++;
        		if(count2==1){
        			childOfChild = (Element) listNode2.item(i);
            		break;
        		}
        	}
        }
        //crittografia del documento
        algorithmURI = XMLCipher.AES_128;
        XMLCipher xmlCipher = XMLCipher.getInstance(algorithmURI);
        xmlCipher.init(XMLCipher.ENCRYPT_MODE, symmetricKey);
        //impostazione delle informazioni nel documento
        EncryptedData encryptedData = xmlCipher.getEncryptedData();
        KeyInfo keyInfo = new KeyInfo(document);
        keyInfo.add(encryptedKey);
        encryptedData.setKeyInfo(keyInfo);
        //modifica del documento e scrittura del file crittato
        xmlCipher.doFinal(document, childOfChild, true);
        DomAndParserUtilities.outputDocToFile(document, "build/symmetric/encrypted1.xml");
    }

Rispetto al caso precedente, c’è in più il codice per individuare l'elemento da criptare (childOfChild), esso viene passato al metodo doFinal al posto della radice. Il passaggio dalla criptazione dell'intero documento alla criptazione di un suo elemento viene naturale, potendo sfruttare API XML preesistenti e API di crittografia progettate appositamente per l'XML.

Osservando il documento criptato ritroveremo invariati i restanti elementi, inclusa l'altra foglia, mentre dell'elemento criptato avremo in chiaro solo il tag; volendo oscurare anche il tag, dovremmo semplicemente impostare su false il terzo parametro della chiamata doFinal. Se abbiamo lasciato invariati i nomi dei files, per ottenere il documento decriptato non dovremo fare altro che utilizzare la classe Decrypter1.

Esempio 3: criptare e decriptare elementi diversi con chiavi diverse

Un’altra opportunità che ci viene offerta dall'XML Security riguarda la possibilità di diversificare l'accesso al documento in base all'utente. In questo terzo esempio vedremo come criptare elementi diversi con chiavi diverse con Apache Santuario, in modo che ogni utente sia in grado di decrittare solo le informazioni di propria competenza.

Anche questa volta l'organizzazione delle classi (Encrypter2 e Decrypter2) ricalca quella vista in precedenza. Il metodo createSampleDocument ci restituisce un documento basato sulla radice, un figlio e tre foglie.

private static Document createSampleDocument() throws Exception {
        javax.xml.parsers.DocumentBuilderFactory dbf =
            javax.xml.parsers.DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);
        javax.xml.parsers.DocumentBuilder db = dbf.newDocumentBuilder();
        Document document = db.newDocument();
        //1 (root)
        Element root = document.createElementNS("http://www.apache.org/ns/#app1", "apache:RootElement");
        root.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:apache", "http://www.apache.org/ns/#app1");
        root.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:child", "http://www.apache.org/ns/#app2");
        document.appendChild(root);
        //1.1 (child)
        Element childElement = document.createElementNS("http://www.apache.org/ns/#app1", "child");
        root.appendChild(childElement);
        //1.1.1 (child 1 of child)
        Element childElement2 = document.createElementNS("http://www.apache.org/ns/#app2", "child:elA");
        childElement2.appendChild(document.createTextNode("Testo d'esempio 1"));
        childElement.appendChild(childElement2);
        //1.1.1 (child 2 of child)
        Element childElement3 = document.createElementNS("http://www.apache.org/ns/#app2", "child:elB");
        childElement3.appendChild(document.createTextNode("Testo d'esempio 2"));
        childElement.appendChild(childElement3);
        //1.1.3 (child 3 of child)
        Element childElement4 = document.createElementNS("http://www.apache.org/ns/#app2", "child:elC");
        childElement4.appendChild(document.createTextNode("Testo d'esempio 3"));
        childElement.appendChild(childElement4);
        return document;
    }

Segue il main.

public static void main(String unused[]) throws Exception {
        Document document = createSampleDocument();
        //chiavi AES per crittare due elementi con due chiavi differenti
        Key symmetricKey1 = KeysUtilities.GenerateDataEncryptionKey();
        Key symmetricKey2 = KeysUtilities.GenerateDataEncryptionKey();
        //chiavi DESede per crittare le chiavi simmetriche
        Key kek1 = KeysUtilities.GenerateAndStoreKeyEncryptionKey("build/kek/filekey2-1.txt");
        Key kek2 = KeysUtilities.GenerateAndStoreKeyEncryptionKey("build/kek/filekey2-2.txt");
        String algorithmURI = XMLCipher.TRIPLEDES_KeyWrap;
        XMLCipher keyCipher = XMLCipher.getInstance(algorithmURI);
        keyCipher.init(XMLCipher.WRAP_MODE, kek1);
        EncryptedKey encryptedKey1 = keyCipher.encryptKey(document, symmetricKey1);
        XMLCipher keyCipher2 = XMLCipher.getInstance(algorithmURI);
        keyCipher2.init(XMLCipher.WRAP_MODE, kek2);
        EncryptedKey encryptedKey2 = keyCipher2.encryptKey(document, symmetricKey2);
        //selezione degli elementi da crittare
        Element rootElement = document.getDocumentElement();
        NodeList listNode1 = rootElement.getChildNodes();
        Element child=null;
        int count=0;
        for(int i=0;i<listNode1.getLength();i++){
        	if(listNode1.item(i).getNodeType()==Node.ELEMENT_NODE){
        		count++;
        		if(count==1){
        			child = (Element) listNode1.item(i);
            		break;
        		}
        	}
        }
        Element childOfChild1=null;
        int count2=0;
        NodeList listNode2 = child.getChildNodes();
        for(int i=0;i<listNode2.getLength();i++){
        	if(listNode2.item(i).getNodeType()==Node.ELEMENT_NODE){
        		count2++;
        		if(count2==1){
        			childOfChild1 = (Element) listNode2.item(i);
            		break;
        		}
        	}
        }
        Element childOfChild2=null;
        int count3=0;
        for(int i=0;i<listNode2.getLength();i++){
        	if(listNode2.item(i).getNodeType()==Node.ELEMENT_NODE){
        		count3++;
        		if(count3==2){
        			childOfChild2 = (Element) listNode2.item(i);
            		break;
        		}
        	}
        }
        algorithmURI = XMLCipher.AES_128;
        XMLCipher xmlCipher = XMLCipher.getInstance(algorithmURI);
        xmlCipher.init(XMLCipher.ENCRYPT_MODE, symmetricKey1);
        XMLCipher xmlCipher2 = XMLCipher.getInstance(algorithmURI);
        xmlCipher2.init(XMLCipher.ENCRYPT_MODE, symmetricKey2);
        //impostazione delle informazioni nel documento
        EncryptedData encryptedData = xmlCipher.getEncryptedData();
        KeyInfo keyInfo = new KeyInfo(document);
        keyInfo.add(encryptedKey1);
        encryptedData.setKeyInfo(keyInfo);
        EncryptedData encryptedData2 = xmlCipher2.getEncryptedData();
        KeyInfo keyInfo2 = new KeyInfo(document);
        keyInfo2.add(encryptedKey2);
        encryptedData2.setKeyInfo(keyInfo2);
        //modifica del documento e scrittura del file crittato
        xmlCipher.doFinal(document, childOfChild1, true);
        xmlCipher2.doFinal(document, childOfChild2, true);
        DomAndParserUtilities.outputDocToFile(document, "build/symmetric/encrypted2.xml");
    }

Nel main troviamo duplicate buona parte delle chiamate dei metodi e delle istanze delle classi usate in precedenza. Abbiamo infatti creato due chiavi e conseguentemente due KEK, memorizzate in file separati. Allo stesso modo troviamo due chiamate al metodo doFinal, ogni chiamata è fatta sua foglia differente, in modo che alla fine troveremo un documento con due foglie criptate, ma con chiavi differenti, e una foglia in chiaro. In base alla politica di distribuzione dei file che contengono le KEK, potremo scegliere se abilitare la lettura completa del documento o se consentire di leggerne solo una parte, fornendo la relativa KEK.

Differenze analoghe si riportano nella classe Decrypter2:

public static void main(String unused[]) throws Exception {
        Document document = DomAndParserUtilities.loadEncryptionDocument("build/symmetric/encrypted2.xml");
        //individuazione elementi da decriptare
        NodeList list1 = document.getElementsByTagNameNS(EncryptionConstants.EncryptionSpecNS, EncryptionConstants._TAG_ENCRYPTEDDATA);
        Element encryptedDataElement1 = (Element) list1.item(0);
        Element encryptedDataElement2 = (Element) list1.item(1);
        //caricamento delle kek
        Key kek1 = KeysUtilities.loadKeyEncryptionKey("build/kek/filekey2-1.txt");
        Key kek2 = KeysUtilities.loadKeyEncryptionKey("build/kek/filekey2-2.txt");
        XMLCipher xmlCipher1 = XMLCipher.getInstance();
        XMLCipher xmlCipher2 = XMLCipher.getInstance();
        xmlCipher1.init(XMLCipher.DECRYPT_MODE, null);
        xmlCipher1.setKEK(kek1);
        xmlCipher2.init(XMLCipher.DECRYPT_MODE, null);
        xmlCipher2.setKEK(kek2);
        //documento decrittato e memorizzato (commentare una delle due chiamate doFinal per decriptare solo una parte)
        xmlCipher1.doFinal(document, encryptedDataElement1);
        xmlCipher2.doFinal(document, encryptedDataElement2);
        DomAndParserUtilities.outputDocToFile(document, "build/symmetric/decrypted2.xml");
    }

Individuati gli elementi da decriptare, in questo esempio vengono acquisite entrambe le KEK e richiamato il metodo doFinal da ognuna delle due istanze della classe XMLCipher, avendo associato a ognuna di esse una specifica KEK. Il risultato finale è il documento interamente decrittato. Possiamo commentare una delle due chiamate al metodo doFinal e vedere che il documento verrà decriptato solo in parte, in base a quale delle due chiamate viene commentata. Disponendo pertanto di solo uno dei file contenenti le KEK, non potremo decriptare l'intero documento ma dovremo limitarci alla parte di nostra competenza.

Chiave asimmetrica

Nella crittografia a chiave asimmetrica vengono utilizzate due chiavi: una di cifratura che può essere resa pubblica e una di decifratura che deve restare privata. Da una chiave non si può ottenere l'altra, per cui è possibile distribuire la chiave pubblica senza preoccupazioni legate alla riservatezza; ciò al contrario di quanto accade con la crittografia a chiave simmetrica. Tale vantaggio si paga però in termini di complessità computazionale. Per ovviare al problema del tempo necessario a criptare i dati, un approccio comunemente utilizzato consiste nell'adottare un algoritmo a chiave condivisa per cifrare e decifrare i dati, per poi utilizzare un algoritmo a chiave pubblica per cifrare e decifrare la chiave condivisa. In questo modo è possibile cifrare e decifrare velocemente pur potendo contare sulla maggiore sicurezza garantita dalle chiavi asimmetriche.

Criptare e decriptare un documento con una chiave asimmetrica

In questo esempio basato su EncryptAndDecrypt, concentreremo nel main sia la parte di criptazione che quella di decriptazione; il metodo createSampleDocument si limita come in precedenza a creare un elementare documento XML.

private static Document createSampleDocument() throws Exception {
		Document document = XMLUtils.createDocumentBuilder(false).newDocument();
        Element rootElement = document.createElement("root");
        document.appendChild(rootElement);
        Element elem = document.createElement("elem");
        Text text = document.createTextNode("text");
        elem.appendChild(text);
        rootElement.appendChild(elem);
        return document;
    }

Di seguito il main.

public static void main(String[] args) throws Exception {
	        Document document = createSampleDocument();
	        DomAndParserUtilities.outputDocToFile(document, "build/asymmetric/doc1.xml");
	        //Creazione dalla chiave di criptazione dati
	        byte[] keyBytes = { 0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7 };
	        SecretKeySpec dataEncryptKey = new SecretKeySpec(keyBytes, "AES");
	        //Creazione di chiave pubblica e privata
	        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
	        BigInteger keyOrigin = new BigInteger("8710a2bcb2f3fdac177f0ae0461c2dd0ebf72e0d88a5400583a7d8bdabd6" +
                    "ae009d30cfdf6acb5b6a64cdc730bc630a39d946d08babffe62ea20a87e37c93b3b0e8a8e576045b" +
                    "bddfbde83ca9bfa180fe6a5f5eee60661936d728314e809201ef52cd71d9fa3c8ce83f9d30ab5e08" +
                    "1539219e7e45dd6a60be65ac95d2049b8f21", 16);
	        BigInteger pubPartial =  new BigInteger("10001", 16);
	        BigInteger privPartial = new BigInteger("20c39e569c2aa80cc91e5e6b0d56e49e5bbf78827bf56a546c1d996c597" +
                    "5187cb9a50fa828e5efe51d52f5d112c20bc700b836facadca6e0051afcdfe866841e37d207c0295" +
                    "36ff8674b301e2198b2c56abb0a0313f8ff84c1fcd6fa541aa6e5d9c018fab4784d2940def5dc709" +
                    "ddc714d73b6c23b5d178eaa5933577b8e8ae9", 16);
	        RSAPublicKeySpec pubKeySpec = new RSAPublicKeySpec(keyOrigin, pubPartial);
	        RSAPrivateKeySpec privKeySpec = new RSAPrivateKeySpec(keyOrigin, privPartial);
	        RSAPublicKey pubKey = (RSAPublicKey) keyFactory.generatePublic(pubKeySpec);
	        RSAPrivateKey privKey = (RSAPrivateKey) keyFactory.generatePrivate(privKeySpec);
	        //Crittografia della chiave di criptazione dati con la kek
	        XMLCipher keyCipher = XMLCipher.getInstance(XMLCipher.RSA_v1dot5);
	        keyCipher.init(XMLCipher.WRAP_MODE, pubKey);
	        EncryptedKey encryptedKey = keyCipher.encryptKey(document, dataEncryptKey);
	        String keyName = "asymmetricExample";
	        KeyInfo kekInfo = new KeyInfo(document);
	        kekInfo.addKeyName(keyName);
	        encryptedKey.setKeyInfo(kekInfo);
	        //Crittografia dei dati
	        XMLCipher xmlCipher = XMLCipher.getInstance(XMLCipher.AES_128);
	        xmlCipher.init(XMLCipher.ENCRYPT_MODE, dataEncryptKey);
	        EncryptedData encryptedData = xmlCipher.getEncryptedData();
	        KeyInfo keyInfo = new KeyInfo(document);
	        keyInfo.add(encryptedKey);
	        encryptedData.setKeyInfo(keyInfo);
	        Element rootElement = (Element) document.getFirstChild();
	        xmlCipher.doFinal(document, rootElement, true);
	        DomAndParserUtilities.outputDocToFile(document, "build/asymmetric/doc2.xml");
	        //Decriptazione
	        //Esempio con utilizzo di un KeyResolver interno
	        MyPrivateKeyResolver.pk = privKey;
	        MyPrivateKeyResolver.pkName = keyName;
	        decryptDocument(document, new MyPrivateKeyResolver(), 3);
	        //Esempio con utilizzo di un KeyResolver statico
	        KeyResolver.registerAtStart(MyPrivateKeyResolver.class.getName(), false);
	        decryptDocument(document, null, 4);
	}

Alla generazione dell'XML seguono le invocazioni per la generazione delle chiavi pubblica e privata. La chiave pubblica servirà come KEK per criptare la chiave di criptazione del documento. E' interessante porre l'attenzione sulla modalità di costruzione delle chiavi; chiave pubblica e chiave privata vengono costruite partendo da una matrice comune, il BigInteger keyOrigin, quel valore che nell'algoritmo RSA prende il nome di modulo. Assieme al modulo, le due chiavi vengono generate utilizzando i rispettivi esponenti, esclusivi di ognuna delle due chiavi e anch’essi BigInteger. La generazione delle chiavi avviene infine tramite una factory (java.security.KeyFactory).

Successivamente avviene in primo luogo la criptazione della chiave di cifratura dei dati con la KEK, dove la chiave pubblica svolge il ruolo di KEK, e successivamente avviene la criptazione del documento, cui vengono allegate le informazioni relative alla chiave di criptazione dei dati (come visto già negli esempi relativi alla chiave simmetrica). A differenza degli esempi precedenti, impostando le informazioni relative alla chiave di criptazione dei dati (KeyInfo) abbiamo inserito anche un tag KeyName contenente un nome, un'etichetta (nel nostro caso asymmetricExample), che consentirà successivamente una rapida individuazione della parte di documento da decriptare.

In generale, la classe KeyInfo può contenere chiavi, nomi, certificati e altri informazioni pubbliche utili alla gestione delle chiavi.

La restante parte di codice relativo alla criptazione non presenta ulteriori novità; come nel caso a chiave simmetrica si utilizza il metodo doFinal appartenente alla classe XMLCipher, in questo caso passandogli come elemento da criptare quello che funge da radice.

Di seguito la sezione relativa alla decriptazione; in essa si presentano due modalità di decriptazione, entrambe basate su un resolver, ossia la classe interna MyPrivateKeyResolver, estensione della classe KeyResolverSpi appartenente al package denominato apache.xml.security, il cui compito è quello di individuare la chiave inclusa nel documento criptato partendo dall'etichetta associata (nel nostro caso asymmetricExample) e di offrire al metodo doFinal la chiave privata per la decriptazione. Nel primo caso la classe MyPrivateKeyResolver viene istanziata, nel secondo l’utilizzo è statico.

La decriptazione avviene nel metodo decryptDocument che, come negli esempi precedenti, utilizza per l'effettiva decriptazione il metodo doFinal della classe XMLChiper. La differenza rispetto ai casi precedenti consiste nel fatto che prima di richiedere la decriptazione invochiamo il metodo registerInternalKeyResolver per acquisire il resolver.

Classe interna MyPrivateKeyResolver:

public static class MyPrivateKeyResolver extends KeyResolverSpi {
        private static PrivateKey pk;
        private static String pkName;
        public boolean engineCanResolve(Element element, String BaseURI, StorageResolver storage) {
            return false;
        }
        public PrivateKey engineLookupAndResolvePrivateKey(
            Element element, String BaseURI, StorageResolver storage
        ) throws KeyResolverException {
            if (Constants.SignatureSpecNS.equals(element.getNamespaceURI()) &&
                Constants._TAG_KEYNAME.equals(element.getLocalName())) {
                String keyName = element.getFirstChild().getNodeValue();
                if (pkName.equals(keyName)) {
                    return pk;
                }
            }
            return null;
        }
    }

Metodo decryptDocument:

private static void decryptDocument(Document docSource, KeyResolverSpi internalResolver, int i) throws Exception
    {
        Document document = (Document)docSource.cloneNode(true);
        Element rootElement = document.getDocumentElement();
        Element encryptedDataElement = (Element)rootElement.getFirstChild();
        XMLCipher decryptCipher = XMLCipher.getInstance();
        decryptCipher.init(XMLCipher.DECRYPT_MODE, null);
        if (internalResolver != null) {
            decryptCipher.registerInternalKeyResolver(internalResolver);
        }
        decryptCipher.doFinal(document, encryptedDataElement);
        DomAndParserUtilities.outputDocToFile(document, "build/asymmetric/doc"+i+".xml");
    }

Apache Santuario: firma XML

La firma XML consente al destinatario di un messaggio di verificare se il messaggio che ha ricevuto è stato modificato rispetto al messaggio inviato dal mittente, permettendo al contempo l'autenticazione di quest'ultimo.

Una firma digitale fornisce un controllo di integrità su alcuni contenuti, permettendo ad esempio di individuare il cambiamento tra un 'Si' e un 'No', l'aggiunta di zeri o la modifica di un qualsiasi intero in un altro e così via.

Per farlo, il primo passo è la creazione di un hash del messaggio. Un hash crittografico prende un flusso arbitrario di bytes e lo converte in un valore basato su un numero di dimensione prefissata, conosciuto come digest. Un digest è pertanto il risultato di un processo a senso unico, è computazionalmente impossibile replicare il messaggio a partire dall'hash, o trovare due messaggi diversi che producano lo stesso digest.

Riassumendo, partendo dal messaggio M, ne creiamo il digest H(M). Chi riceverà il messaggio riceverà sia M che H(M) e potrà ottenere il proprio digest H’(M). Se i due digest sono gli stessi, è possibile dire che quanto è stato inviato corrisponde a quanto si è ricevuto.

Nella pratica si applicano due approcci. Il primo consiste nell'inserire una chiave condivisa nel digest, ossia creare H(S+M), in modo che chi riceve il messaggio deve utilizzare la chiave condivisa S per ottenere il proprio digest H’(S+M) e confrontarlo con quello ricevuto. In questo modo il problema si sposta sulla sicurezza della chiave condivisa S. Un modo comune di procedere consiste nell'utilizzare autorità centrali per la distribuzione di chiavi comuni per determinate sessioni.

Un altro approccio si basa invece sull'uso di crittografia a chiave asimmetrica, ad esempio RSA. Usando RSA, viene generato il digest H(M) e criptato con la chiave privata, ottenendo la firma. Chi riceve M genera il proprio digest H’(M) e decripta la firma usando la chiave pubblica. Se il digest H(M) così prodotto e H’(M) sono gli stessi, il messaggio M è identico.

Appare subito evidente come il problema della firma sia strettamente legato al problema della crittografia.

Nell'esempio che mostreremo di seguito, procederemo in primo luogo a generare un documento XML con annessa firma digitale. Successivamente andremo a verificare che il documento ottenuto sia coerente con la firma annessa.

Ecco quindi un esempio di firma di un documento XML:

CreateSignature main

public static void main(String unused[]) throws Exception {
        ElementProxy.setDefaultPrefix(Constants.SignatureSpecNS, "ds");
        //Caricamento del keystore
        String keystoreType = "JKS";
        String keystoreFile = "samples/data/sig/keystore.jks";
        String keystorePass = "xmlsecurity";
        String privateKeyAlias = "test";
        String privateKeyPass = "xmlsecurity";
        String certificateAlias = "test";
        String signatureDoc = "build/signature/signature.xml";
        File signatureFile = new File(signatureDoc);
        KeyStore ks = KeyStore.getInstance(keystoreType);
        FileInputStream fis = new FileInputStream(keystoreFile);
        ks.load(fis, keystorePass.toCharArray());
        //Acquisizione della chiave privata per la firma
        PrivateKey privateKey = (PrivateKey) ks.getKey(privateKeyAlias, privateKeyPass.toCharArray());
        //Inizializzazione del documento XML
        javax.xml.parsers.DocumentBuilderFactory dbf = javax.xml.parsers.DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);
        javax.xml.parsers.DocumentBuilder db = dbf.newDocumentBuilder();
        org.w3c.dom.Document doc = db.newDocument();
        Element root = doc.createElementNS("http://www.apache.org/ns/#app1", "apache:RootElement");
        root.setAttributeNS(null, "Scope", "SignatureTest");
        root.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:foo", "http://example.org/#foo");
        root.setAttributeNS("http://example.org/#foo", "foo:attr1", "SigTest");
        root.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:apache", "http://www.apache.org/ns/#app1");
        doc.appendChild(root);
        root.appendChild(doc.createTextNode("Un semplice documento con firman"));
        DomAndParserUtilities.outputDocToFile(doc, "build/signature/unsigDoc.xml");
        //BaseURI è l'URI da anteporre ai relativi URIs
        String BaseURI = signatureFile.toURI().toURL().toString();
        //Oggetto XML Signature creato a partire dal documento, BaseURI e algoritmo di firma (in questo caso DSA)
        XMLSignature sig = new XMLSignature(doc, BaseURI, XMLSignature.ALGO_ID_SIGNATURE_DSA);
        //Append dell'elemento che conterrà la firma alla radice del documento
        //enveloped signature
        root.appendChild(sig.getElement());
        sig.getSignedInfo().addResourceResolver(
            new org.apache.xml.security.samples.utils.resolver.OfflineResolver()
        );
        {
        //Enveloped signature, Oggetto transforms
            Transforms transforms = new Transforms(doc);
            transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE);
            transforms.addTransform(Transforms.TRANSFORM_C14N_WITH_COMMENTS);
            sig.addDocument("", transforms, Constants.ALGO_ID_DIGEST_SHA1);
        }
        {
            //URIs esterni (OfflineResolver): detached Reference. Firma mista
            sig.addDocument("http://www.w3.org/TR/xml-stylesheet");
            sig.addDocument("http://www.nue.et-inf.uni-siegen.de/index.html");
        }
        {
            //Aggiunta delle informazioni riguardanti il certificato (e la chiave) e creazione della firma
            X509Certificate cert = (X509Certificate) ks.getCertificate(certificateAlias);
            sig.addKeyInfo(cert);
            sig.addKeyInfo(cert.getPublicKey());
            System.out.println("Firma...");
            sig.sign(privateKey);
            System.out.println("...aggiunta");
        }
        DomAndParserUtilities.outputDocToFile(doc, signatureDoc);
    }

La prima parte consiste nel popolare un oggetto della classe KeyStore a partire dal file keystore.jks. Questa classe ha il compito di fornire un contenitore per chiavi di criptografia e certificati, con le voci identificate per mezzo di alias, come mostrato nell'esempio.

Successivamente viene inizializzato il documento XML. Da notare che l'XML Signature richiede supporto al namespace, pertanto viene richiesto al parser che si sta istanziando di fornire tale supporto (setNamespaceAware(true)), in quanto di default ciò non avviene. Prima di andare avanti, il documento non ancora firmato viene scritto su file (unsigDoc.xml) in modo da poterlo confrontare successivamente con quello firmato.

Arriviamo ora alla creazione della firma, essa si basa sulla classe XMLSignature, responsabile della creazione e della verifica della firma in Apache Santuario. Nel nostro esempio l'oggetto viene inizializzato a partire dal documento da firmare, dall'URI di destinazione e dal metodo utilizzato per firmare. La firma viene quindi posposta all'elemento radice, creando una enveloped signature. Ciò significa che la firma farà parte del contenuto del documento XML come elemento figlio, ma questo elemento viene escluso dal calcolo per ottenere il digest, il che può avvenire tramite un oggetto Transform (come si può vedere nell'esempio).

Vi sono altre due modalità: la prima consiste nell'inserire l'intero documento all'interno della firma, la seconda consiste nel mantenere separati firma e documento. E’ possibile comunque un approccio misto fornendo un riferimento.

Nel main presentato troviamo diversi approcci. La firma viene a far parte del documento e si utilizza un transform per separarla dal documento nel momento del calcolo della firma.

Successivamente vengono aggiunti due riferimenti a documenti esterni (posizionati nella cartella samples e accessibili mediante l'OfflineResolver aggiunto come ResourceResolver alla firma).

La firma viene aggiunta utilizzando un certificato X.509 per difendere la chiave pubblica. Tramite un certificato X.509 possiamo associare un nome a una chiave pubblica. Ciò può essere fatto per ovviare a un possibile attacco. Se infatti viene sostituita una chiave pubblica con un'altra chiave pubblica, è possibile sostituirsi all'applicazione originale e ottenere dati riservati. Per prevenire questi attacchi, è possibile usare certificati firmati da un'autorità di certificazione che può confermare l'integrità di una chiave pubblica in un certificato. Un certificato X.509 contiene informazioni sul soggetto del certificato e su chi emette il certificato. Il certificato è quindi codificato in un formato standard (ASN.1 o Abstract Syntax Notation One) inviabile o ricevibile mediante una rete.

Il certificato è creato partendo dalle informazioni contenute nel Keystore. Alla firma viene allegato il certificato e la chiave pubblica utile per decriptare il messaggio. A questo punto tutto è pronto per la firma, che avviene mediante il metodo sign della classe XMLSignature, passando come argomento la chiave privata utilizzata per ottenere il digest. Infine il documento firmato viene salvato su file come fatto in precedenza.

Da notare che, oltre ad aggiungere il certificato (addKeyInfo(cert)), viene aggiunta la chiave pubblica (addKeyInfo(cert.getPublicKey())). Questa operazione è ridondante in quanto stiamo già passando il certificato, ma è utile per provare un’altra combinazione di firma e verifica, come vedremo successivamente.

Osservando il file generato (signature.xml), potremo notare che sono state create tre firme (tag ds:DigestValue). La prima firma, con annesso Transform, è associata al documento XML creato nel main. Vi sono inoltre altre due firme associate a XML esterni, gli XML identificati dal riferimento reference. Inoltre, vi sono le informazioni relative al certificato X.509 e alla chiave pubblica, informazioni che potranno essere utilizzate in fase di verifica della firma.

Esempio: verifica della firma

Nella verifica, una volta acquisito il documento, il primo passo consiste nell'individuazione della firma, ossia dell'elemento Signature. In questo esempio utilizziamo un'espressione XPath, che ci permette di individuare il nodo da passare all'oggetto istanza della classe XMLSignature. Come in precedenza la classe XMLCipher metteva a disposizione i metodi per criptare e decriptare, così ora utilizziamo la classe XMLSignature per firma e verifica.

VerifySignature main

public static void main(String unused[]) {
        String signatureFileName = "build/signature/signature.xml";
        try {
            File f = new File(signatureFileName);
            Document doc = DomAndParserUtilities.loadEncryptionDocument("build/signature/signature.xml");
            //Individuazione e acquisizione firma
            XPathFactory xpf = XPathFactory.newInstance();
            XPath xpath = xpf.newXPath();
            xpath.setNamespaceContext(new DSNamespaceContext());
            String expression = "//ds:Signature[1]";
            Element sigElement = (Element) xpath.evaluate(expression, doc, XPathConstants.NODE);
            XMLSignature signature = new XMLSignature(sigElement, f.toURI().toURL().toString());
            signature.addResourceResolver(new OfflineResolver());
            //Acquisizione informazioni dal documento
            KeyInfo ki = signature.getKeyInfo();
            if (ki != null) {
            	//Verifica tramite certificato
                if (ki.containsX509Data()) {
                    System.out.println("X509Data in KeyInfo!");
                }
                X509Certificate cert = ki.getX509Certificate();
                if (cert != null) {
                    System.out.println("Firma XML: "
                                       + (signature.checkSignatureValue(cert)
                                           ? "valida (Ok)!"  : "invalida !!! (NOk)"));
                } else {
                	//Verifica tramite chiave pubblica
                    System.out.println("Certificato non trovato, verifica tramite chiave pubblica!");
                    PublicKey pk = ki.getPublicKey();
                    if (pk != null) {
                        System.out.println("Firma XML: "
                                           + (signature.checkSignatureValue(pk)
                                        	? "valida (Ok)!"  : "invalida !!! (NOk)"));
                    } else {
                        System.out.println(
                            "Chiave pubblica non trovata. Verifica della firma fallita!");
                    }
                }
            } else {
                System.out.println("KeyInfo assente. Verifica della firma fallita!");
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

Poiché in fase di firma avevamo aggiunto sia il certificato (nel nostro caso il certificato X509) che la chiave pubblica, la verifica può avvenire in due modalità. Le informazioni vengono reperite a partire dalle informazioni aggiunte al documento firmato (KeyInfo). Segue quindi il test (checkSignatureValue) che inizia dalla verifica della presenza di un certificato. Ciò è sufficiente per stabilire la validità del documento firmato, per cui il processo si arresta. Modificando leggermente il comportamento della classe (ad esempio commentando la sezione relativa al certificato nella classe di verifica o di firma) sarà possibile eseguire il test tramite la chiave pubblica, e si vedrà che il documento supera il test senza problemi anche in questo caso. Da notare che la verifica della firma avviene in un singolo passaggio, senza dover prevedere un comportamento separato per la firma relativa al documento e le firme relative ai due documenti referenziati e senza dover eseguire manipolazioni sul documento da verificare.

Conclusioni

In questo articolo siamo partiti dalle problematiche che hanno portato alla richiesta di frameworks specifici per la sicurezza nell'XML, abbiamo visto che alle richieste è seguita la definizione di diversi standard modulari volti a soddisfare le differenti richieste e abbiamo infine mostrato una panoramica di Apache Santuario, progetto volto ad abilitare firma e criptografia digitale dell'XML in Java.

Abbiamo introdotto i principali argomenti, mostrandone varietà e complessità. I singoli aspetti accennati, ad esempio la creazione delle chiavi o la gestione dei certificati, meritano ulteriori approfondimenti, ma scopo dell'articolo era in realtà quello di constatare che il framework consente di supportare le diverse sfumature connesse al mondo della sicurezza.

Grazie a queste caratteristiche l'utilizzo del framework è diffuso. A titolo di esempio se ne riportano alcuni dei principali utilizzatori open source: Apache WSS4J, Apache CXF API e Apache ServiceMix.

Un'incognita da segnalare riguarda la scarsità di documentazione. Il progetto ha origine prima del 2000, ciò nonostante il materiale reperibile in Rete spesso risulta carente (ad esempio nel sito della community Apache troviamo un laconico The documentation available here is not very huge), pertanto, per un maggior approfondimento, può risultare necessario studiare gli esempi messi a disposizione nella cartella samples del progetto. Qualora non bastasse, è possibile consultare la cartella src/test per ricavare altri esempi.

Per finire, occorre dire che nessuna delle specifiche dell'XML Security è completamente realizzata e che sia il W3C che Oasis stanno lavorando duramente per finalizzare gli standards. Sono inoltre emerse delle falle nella sicurezza di un'implementazione di XML Encryption individuate a partire dalle informazioni ricavabili dai messaggi di errore (ACM Conference). L'XML security è dunque un processo tuttora in corso e ciò potrà avere ripercussioni sulle implementazioni dei relativi standard, come Apache Santuario.

Ti consigliamo anche