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

La sicurezza dei dati in un'app

Link copiato negli appunti

Le comunicazioni tramite reti pubbliche, come internet, possono essere facilmente intercettate da "individui" che potrebbero essere interessati a conoscerne o a manipolarne il contenuto.

È possibile proteggersi contro queste indebite intromissioni grazie all'uso di svariate tecniche di criptazione. In questo articolo vedremo come rendere la nostra applicazione Windows Store più sicura, in modo da poter scambiare dati da e verso l'esterno senza il timore che qualcuno intercetti le nostre comunicazioni.

In particolare, il namespace Windows.Security.Cryptography espone tipi e metodi che possono essere utilizzati per rendere più sicuri i dati di un'applicazione Windows Store. Queste nuove librerie sostituiscono le tradizionali librerie .NET incluse nel namespace System.Security.Cryptography.

Queste librerie consentono di raggiungere uno o più dei seguenti obiettivi:

  • Autenticazione: grazie all'uso di algoritmi criptografici è possibile identificare con sicurezza l'identità del soggetto da cui la comunicazione proviene.
  • Confidenzialità: gli algoritmi criptografici aiutano a evitare che il contenuto di una comunicazione possa essere "leggibile" da persone diverse da quelle autorizzate.
  • Integrità dei dati: la criptografia consente anche di evitare che i dati possano essere manipolati da terze parti.
  • Non-repudiation: come risultato dell'autenticazione, confidenzialità e integrità dei dati, la criptografia previene anche il disconoscimento da parte dei terzi dei messaggi da loro inviati.

Vale la pena aggiungere che il Bureau of Industry and Security (Department of Commerce) degli Stati Uniti disciplina l'esportazione dei vari meccanismi di criptazione. Queste limitazioni valgono anche per le applicazioni distribuite tramite il Windows Store (per maggiori informazioni su questo argomento si rinvia alla documentazione ufficiale su MSDN)

Gli algoritmi di hashing

Gli algoritmi di hashing trasformano, tramite particolari funzioni matematiche, valori binari di lunghezza arbitraria in valori binari più piccolo e di lunghezza predeterminata. Applicando un algoritmo di hashing a un messaggio di testo, il risultato è un codice binario, noto come "digest", che rappresenta una sorta di "impronta" che identifica univocamente il contenuto del messaggio stesso. Infatti, anche la più piccola modifica al messaggio originale (anche un singolo bit) produce un digest differente rispetto a quello iniziale.

Grazie a questa caratteristica, gli algoritmi di hash aiutano a garantire l'integrità del messaggio. Una volta inviato il messaggio assieme al corrispondente digest, è infatti sufficiente per il ricevente applicare lo stesso algoritmo di hashing e confrontare il digest così prodotto con quello inviato assieme al messaggio: se corrispondono, vuol dire che il messaggio non è stato manipolato durante la comunicazione.

È importante precisare che un algoritmo di hashing, di per sé, non garantisce la confidenzialità della comunicazione, poiché il messaggio è inviato in chiaro. Inoltre, dato un digest, è impossibile risalire al messaggio originario. Gli algoritmi di hash, insomma, funzionano solo in una direzione: vengono infatti definiti anche One-way alghoritm.

In un'app Windows Store, per produrre un digest a partire da un messaggio di testo è possibile sfruttare la classe HashAlgorithmProvider, come mostrato nel prossimo snippet:

public void HashMessage_Click(object sender, RoutedEventArgs args)
{
    String message1 = PlainText1.Text;
    String message2 = PlainText2.Text;
    IBuffer binaryMessage1 = CryptographicBuffer.ConvertStringToBinary(message1, BinaryStringEncoding.Utf8);
    IBuffer binaryMessage2 = CryptographicBuffer.ConvertStringToBinary(message2, BinaryStringEncoding.Utf8);
    String hashAlgorithmName = HashAlgorithmNames.Sha512;
    HashAlgorithmProvider hashProvider = HashAlgorithmProvider.OpenAlgorithm(hashAlgorithmName);
    IBuffer hashedMessage1 = hashProvider.HashData(binaryMessage1);
    IBuffer hashedMessage2 = hashProvider.HashData(binaryMessage2);
    if (hashedMessage1.Length != hashProvider.HashLength || hashedMessage2.Length != hashProvider.HashLength)
    {
        HashedMessage1.Text = "Si è verificato un errore nella produzione dell'hash";
    }
    HashedMessage1.Text = String.Format("Digest messaggio 1: {0}", CryptographicBuffer.EncodeToBase64String(hashedMessage1));
    HashedMessage2.Text = String.Format("Digest messaggio 2: {0}", CryptographicBuffer.EncodeToBase64String(hashedMessage2));
}

Questo esempio utilizza due messaggi di testo quasi identici (l'unica differenza è il punto esclamativo nella prima stringa) per mostrare come anche minime variazioni nella base di partenza produca digest differenti.

Il codice per prima cosa converte i due messaggi nella relative rappresentazione binaria tramite il metodo ConvertStringToBinary, uno dei numerosi metodi messi a disposizione dalla classe CryptographicBuffer per eseguire conversioni tra i vari tipi coinvolti nelle operazioni criptografiche (come i metodi EncodeToBase64String/DecodeToBase64String e EncodeToHexString/DecodeToHexString).

Il secondo passaggio consiste nella scelta dell'algoritmo da utilizzare per produrre il digest. Per questa operazione possiamo sfruttare l'enum HashAlgorithmNames, che elenca i vari algoritmi supportati (Md5, Sha1, Sha256, Sha384, e Sha512):

String hashAlgorithmName = HashAlgorithmNames.Sha512;

Il nome dell'algoritmo prescelto (Sha512, nel nostro caso) viene quindi passato come parametro al metodo HashAlgorithmProvider.OpenAlgorithm, ottenendo come risultato un oggetto di tipo HashAlgorithmProvider, il quale incapsula la logica interna relativa a quel particolare algoritmo.

A questo punto, per produrre l'hash corrispondente è sufficiente invocare il metodo HashData.

IBuffer hashedMessage1 = hashProvider.HashData(binaryMessage1);
IBuffer hashedMessage2 = hashProvider.HashData(binaryMessage2);

Il blocco if successivo controlla che la lunghezza dell'hash così prodotto coincide con quella specifica di quel particolare algoritmo (indicatadalla proprietà HashLength dell'oggetto HashAlgorithmProvider).

Per testare questo codice, possiamo usare la seguente definizione XAML come riferimento per la pagina MainPage.xaml dell'applicazione.

<Page
    x:Class="Demo.Html.it.CryptoSample.CS.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Demo.Html.it.CryptoSample.CS"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">
    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <StackPanel Orientation="Vertical">
            <TextBlock x:Name="PlainText1"  Margin="5" FontSize="18" >Ciao da Html.it!</TextBlock>
            <TextBlock x:Name="PlainText2"  Margin="5" FontSize="18" >Ciao da Html.it</TextBlock>
            <Button Content="Genera Hash" VerticalAlignment="Center" Margin="5" Click="HashMessage_Click" />
            <TextBlock x:Name="HashedMessage1"  Margin="5" FontSize="18" />
            <TextBlock x:Name="HashedMessage2"  Margin="5" FontSize="18" />
        </StackPanel>
    </Grid>
</Page>

La prossima immagine mostra il risultato dell'operazione di hashing (da notare come i due digest prodotti siano decisamente diversi l'uno dall'altro).

Per verificare che due digest coincidano (e quindi che il messaggio non sia stato alterato) possiamo sfruttare il metodo statico Compare, esposto dalla classe HashAlgorithmProvider, come illustrato nel prossimo snippet:

public void CompareDigest(String originalMessage, IBuffer digest)
{
    IBuffer binaryMessage = CryptographicBuffer.ConvertStringToBinary(originalMessage, BinaryStringEncoding.Utf8);
    String hashAlgorithmName = HashAlgorithmNames.Sha512;
    HashAlgorithmProvider hashProvider = HashAlgorithmProvider.OpenAlgorithm(hashAlgorithmName);
    CryptographicHash reusableHash = hashProvider.CreateHash();
    reusableHash.Append(binaryMessage);
    IBuffer hashedOriginalMessage = reusableHash.GetValueAndReset();
    if (!CryptographicBuffer.Compare(hashedOriginalMessage, digest))
    {
        // il messaggio è stato alterato
    }
}

Il metodo CreateHash della classe HashAlgorithmProvider consente di creare un oggetto di tipo CryptographicHash, ossiaun container che permette di operare più trasformazioni in sequenza. Per creare un digest, è sufficiente passare il testo (sotto forma di un oggetto IBuffer) al metodo Append, quindi invocare il metodo GetValueAndReset, il quale restituisce il relativo digest e resetta l'oggetto CryptographicHash affinché sia pronto a ricevere un nuovo messaggio (e conseguentemente a produrre un nuovo digest).

Infine, il metodo Compare mette a confronto il digest ricevuto assieme al messaggio con quello prodotto applicando al messaggio originale lo stesso algoritmo usato per produrre il primo digest. Se i due digest coincidono, il messaggio non può essere stato manipolato fra le due operazioni.

Generare numeri e dati random

La generazione di numeri random rappresenta un momento importante in molte operazioni criptografiche, come ad esempio nella generazione di chiavi criptografiche o di password. La classe CryptographicBuffer espone un metodo GenerateRandomNumber che rende questa operazione particolarmente semplice:

UInt32 rndNumber = CryptographicBuffer.GenerateRandomNumber();

Questo metodo restituisce un numero intero a 32-bit (tieni presente che un UInt32 non è un tipo CLS –compliant.) Questo numero può quindi essere utilizzato per successive operazioni criptografiche (ne vedremo un esempio più avanti).

Oltre alla generazione di numeri casuali, la classe CryptographicBuffer espone anche un metodo GenerateRandom che può essere utilizzato per generare un buffer di dati random della lunghezza desiderata. Il prossimo snippet mostra un esempio di generazione di numeri e dati random:

private void GenerateRandom_Click(object sender, RoutedEventArgs e)
{
    tbRandomNumber.Text = String.Format("Numero random: {0}", CryptographicBuffer.GenerateRandomNumber());
    UInt32 length = 32;
    IBuffer rndData = CryptographicBuffer.GenerateRandom(length);
    tbRandomData.Text = String.Format("Dati random: {0}", CryptographicBuffer.EncodeToHexString(rndData));
}

Per testare questo codice, aggiungiamo le seguenti righe alla pagina principale dell'app all'interno del controllo Grid:

<StackPanel>
    <Button Content="Genera numeri e dati random" VerticalAlignment="Center" FontSize="18" Margin="5" Click="GenerateRandom_Click" />
    <TextBlock x:Name="tbRandomNumber" Margin="5" FontSize="18" />
    <TextBlock x:Name="tbRandomData" Margin="5" FontSize="18" />
</StackPanel>

La prossima immagine mostra un esempio di numeri e dati random generate tramite i metodi sopra descritti:

Criptare un messaggio con algoritmi MAC

Una forma particolare di algoritmo di hash è rappresentato dal Message Authentication Code (MAC), una famiglia di algoritmi anche noti come "keyed hashing algorithm". Un MAC consiste in un breve set di dati che viene utilizzato per assicurare non solo l'integrità del messaggio, ma anche la sua autenticità, grazie al fatto che l'algoritmo si basa su una chiave segreta che è conosciuta sia da mittente che dal ricevente (criptografia simmetrica).

Nei casi più comuni, la chiave segreta viene utilizzata in combinazione con un algoritmo di hashing (detto hash-based message authentication code, or HMAC) per produrre un digest criptato che può essere decriptato unicamente da coloro in possesso della chiave segreta. Talvolta, invece, la chiave segreta viene utilizzata per criptare l'intero messaggio, e gli ultimi bit di questo sono usati come codice hash (anche se nessun algoritmo di hash è stato effettivamente applicator), mentre il resto dei dati viene scartato.

Per criptare un messaggio tramite un algoritmo MAC, è possibile sfruttare la classe MacAlgorithmProvider, la cui logica di funzionamento è molto simile a quella della classe HashAlgorithmProvider vista in precedenza. La differenza principale sta nel fatto che, per produrre il digest, viene impiegata una chiave segreta. Il prossimo snippet mostra questo punto:

private IBuffer _macSignature = null;
private String _message = "Ciao da Html.it!";
private IBuffer _key = null;
private void GenerateMacSignature_Click(object sender, RoutedEventArgs e)
{
    String macAlgorithmName = MacAlgorithmNames.HmacSha256;
    MacAlgorithmProvider macProvider = MacAlgorithmProvider.OpenAlgorithm(macAlgorithmName);
    this._key = CryptographicBuffer.GenerateRandom(macProvider.MacLength);
    CryptographicKey hmacKey = macProvider.CreateKey(this._key);
    var binaryMessage = CryptographicBuffer.ConvertStringToBinary(this._message, BinaryStringEncoding.Utf8);
    this._macSignature = CryptographicEngine.Sign(hmacKey, binaryMessage);
    tbMacSignature.Text += String.Format("MAC Signature: {0}", CryptographicBuffer.EncodeToHexString(this._macSignature));
}

Le prime due righe di codice sono molto simili a quelle viste in relazione alla classe HashAlgorithmProvider: per prima cosa, il codice recupera il nome dell'algoritmo MAC da utilizzare (gli algoritmi supportati sono AesCmac, HmacMd5, HmacSha1, HmacSha256, HmacSha384, e HmacSha512) attraverso la classe statica MacAlgorithmNames, quindi invoca il metodo OpenAlgorithm per recuperare il relativo provider, passando come parametro il nome dell'algoritmo da utilizzare (HmacSha256, in questo esempio).

Il passo successivo consiste nel creare la chiave segreta per firmare il messaggio. Per far questo, il codice utilizza il metodo GenerateRandom descritto in precedenza. Questo metodo restituisce un buffer di dati random che viene quindi passato come parametro al metodo CreateKey del provider MAC. Infine, il codice invoca il metodo Sign della classe CryptographicEngine per produrre un codice hash criptato basato sulla chiave privata.

L'autenticità del digest così criptato può essere verificata tramite il metodo VerifySignature, il quale accetta i seguenti parametri: la chiave segreta usata per criptare il digest, il messaggio e la firma da verificare. Questo metodo decripta la firma tramite la chiave fornita e quindi compara i due digest. Se questi coincidono, significa non solo che il messagggio non è stato alterato durante il trasporto, ma anche che vi è una relativa certezza circa l'identità del mittente. Il prossimo snippet mostra un esempio di verifica:

private void VerifyMacSignature_Click(object sender, RoutedEventArgs e)
{
    String macAlgorithmName = MacAlgorithmNames.HmacSha256;
    MacAlgorithmProvider macProvider = MacAlgorithmProvider.OpenAlgorithm(macAlgorithmName);
    CryptographicKey hmacKey = macProvider.CreateKey(this._key);
    var binaryMessage = CryptographicBuffer.ConvertStringToBinary(this._message, BinaryStringEncoding.Utf8);
    bool IsAuthenticated = CryptographicEngine.VerifySignature(
        hmacKey,
        binaryMessage,
        this._macSignature);
    if (!IsAuthenticated)
        VerificationTextBlock.Text += "Attenzione! Le firme non coincidono.";
    else
        VerificationTextBlock.Text += "Ok, le firme coincidono!";
}

Per prima cosa, il codice ottiene una reference all'algoritmo MAC utilizzato per firmare il digest e crea un nuovo oggetto CryptographicKey basato sulla stessa chiave utilizzata per criptare l'hash. Quindi, il codice invoca il metodo VerifySignature per accertarsi che, data la chiave segreta, il messaggio e la firma a questo associate, la firma sia valida.

Per testare questo codice, possiamo usare la seguente definizione XML come riferimento per la pagina MainPage.xaml dell'app.

<StackPanel>
    <StackPanel Orientation="Horizontal">
        <Button Content="Genera firma MAC" Margin="5" FontSize="18" Click="GenerateMacSignature_Click" />
        <TextBlock x:Name="tbMacSignature" Margin="5" VerticalAlignment="Center" FontSize="18"/>
    </StackPanel>
    <StackPanel Orientation="Horizontal">
        <Button Content="Validate MAC signature" Margin="5" FontSize="18" Click="VerifyMacSignature_Click" />
        <TextBlock x:Name="VerificationTextBlock" Margin="5" VerticalAlignment="Center" FontSize="18"/>
    </StackPanel>
</StackPanel>

Se lanciamo l'applicazione, generiamo una firma MAX e poi validiamo la firma generata, il risultato dovrebbe essere simile a questo:

La firma digitale

Una firma digitale è concettualmente simile a quanto appena visto a proposito dell'autenticazione MAC. La differenza è che mentre quest'ultima si basa su una chiave segreta condivisa (o chiave simmetrica), la firma digitale utilizza una chiave asimmetrica per raggiungere lo stesso risultato.

Più nel dettaglio, la criptografia asimmetrica si basa su una coppia di chiavi generate matematicamente a partire da numeri random: una chiave privata, che deve essere tenuta segreta, e una chiave pubblica che può essere liberamente diffusa. I dati criptati utilizzando la chiave pubblica possono essere decriptati unicamente con la chiave privata e viceversa.

Per firmare digitalmente un messaggio, per prima cosa il mittente applica un algoritmo di hash al messaggio, creando un digest. Questo digest viene criptato utilizzato la chiave privata. Il ricevente, dal canto suo, utilizzerà la corrispondente chiave pubblica per decriptare il digest associato al messaggio, calcolare il codice di hash di quest'ultimo e quindi confrontare i due digest: se questi coincidono, significa che il messaggio arriva effettivamente dal possessore della chiave privata e che il messaggio non è stato alterato durante il trasporto.

È anche possibile l'operazione inversa, ossia firmare digitalmente un messaggio utilizzando la chiave pubblica del destinatario. Il messaggio potrà essere decriptato unicamente dal possessore della chiave privata, assicurando così la confidenzialità del messaggio.

Il prossimo snippet mostra un esempio di creazione di una firma digitale:.

private void GenerateDigitalSignature_Click(object sender, RoutedEventArgs e)
{
    CryptographicKey keyPair;
    UInt32 keySize = 256;
    String message = "Ciao da Html.it";
    String asymmetricAlgorithmName = AsymmetricAlgorithmNames.EcdsaP256Sha256;
    var asymmetricKeyProvider = AsymmetricKeyAlgorithmProvider.OpenAlgorithm(asymmetricAlgorithmName);
    try
    {
        keyPair = asymmetricKeyProvider.CreateKeyPair(keySize);
    }
    catch (ArgumentException ex)
    {
        // errore nella generazione delle chiavi
        return;
    }
    IBuffer binaryMessage = CryptographicBuffer.ConvertStringToBinary(message, BinaryStringEncoding.Utf8);
    IBuffer signature = CryptographicEngine.Sign(keyPair, binaryMessage);
    IBuffer publicKeyBuffer = keyPair.ExportPublicKey();
    IBuffer keyPairBuffer = keyPair.Export();
    CryptographicKey keyPublic = asymmetricKeyProvider.ImportPublicKey(publicKeyBuffer);
    if (keyPublic.KeySize != keyPair.KeySize)
    {
        // importazione fallita
        return;
    }
    keyPair = asymmetricKeyProvider.ImportKeyPair(keyPairBuffer);
    if (keyPublic.KeySize != keyPair.KeySize)
    {
        // importazione fallita
        return;
    }
    if (!CryptographicEngine.VerifySignature(keyPublic, binaryMessage, signature))
    {
        tbDigitalSignature.Text = "Verifica fallita!";
        return;
    }
    tbDigitalSignature.Text = String.Format("Firma verificata: {0}",
        CryptographicBuffer.EncodeToBase64String(signature));
}

La differenza principale rispetto al codice visto a proposito degli algoritmi MAC è che, in questo caso, per firmare il messaggio viene utilizzata una coppia di chiavi, anziché una chiave segreta condivisa.

Anche in questo caso, il primo passo consiste nella scelta del provider che incapsula la logica dell'algoritmo; poi, una volta recuperato il nome dell'algoritmo tramite l'enum AsymmetricalAlgorithmNames, per creare la coppia chiave pubblica/chiave privata il codice invoca il metodo CreateKeyPair della classe AsymmetricKeyProvider (per l'elenco degli algoritmi supportati si rinvia alla documentazione su MSDN).

String asymmetricAlgorithmName = AsymmetricAlgorithmNames.EcdsaP256Sha256;
var asymmetricKeyProvider = AsymmetricKeyAlgorithmProvider.OpenAlgorithm(asymmetricAlgorithmName);
try
{
    keyPair = asymmetricKeyProvider.CreateKeyPair(keySize);
}
catch (ArgumentException ex)
{
    // errore nella generazione delle chiavi
    return;
}

La chiave privata è quindi usata per firmare digitalmente il messaggio tramite il metodo CryptographicEngine.Sign.

IBuffer signature = CryptographicEngine.Sign(keyPair, binaryMessage);

Dopo aver firmato il messaggio con la chiave privata, è possibile esportare (sotto forma di array di byte) l'intera coppia di chiavi, o anche solo la chiave pubblica, tramite i metodi Export e ExportPublicKey della classe CryptographicKey.

IBuffer publicKeyBuffer = keyPair.ExportPublicKey();
IBuffer keyPairBuffer = keyPair.Export();

Analogamente, è possibile importare una o entrambe le chiavi tramite i metodi ImportKeyPair e ImportPublicKey (sempre sotto forma di buffer).

CryptographicKey keyPublic = asymmetricKeyProvider.ImportPublicKey(publicKeyBuffer);

Infine, per verificare la firma, la classe CryptographicEngine espone il metodo VerifySignature, il quale accetta come parametri la chiave pubblica importata, il messaggio e il digest firmato con la chiave privata.

CryptographicEngine.VerifySignature(keyPublic, binaryMessage, signature);

Per testare il codice qui presentato, si può usare la seguente definizione XAML come riferimento per l'app:

<Page
    x:Class="Demo.Html.it.CryptoSample.CS.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Demo.Html.it.CryptoSample.CS"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">
    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <StackPanel>
            <Button Click="GenerateDigitalSignature_Click" Content="Crea firma digitale" Margin="5" />
            <TextBlock x:Name="tbDigitalSignature" Margin="5" VerticalAlignment="Center" FontSize="18" TextWrapping="Wrap"/>
        </StackPanel>
    </Grid>
</Page>

La prossima immagine mostra la firma generata dal codice:

Emettere e richiedere certificati

Nella vita reale, la nostra identità è comprovata da un documento emesso da una autorità di certificazione. Questa autorità può essere riconosciuta solo a livello locale (si pensi al comune di residenza per le carte di identità, la cui validità è nazionale) oppure riconosciuta a livello internazionale (la questura per i passaporti), per indentificare in modo sicuro un "individuo" informatico, occorre un documento emesso da una autorità di certificazione. In pratica la verifica di un documento parte dal riconoscimento dell'autorità di certificazione che lo ha emesso. Ad esempio, la nostra carta di identità non è valida all'estero in quanto non viene riconosciuta l'autorità di ogni singolo comune italiano al di fuori del nostro paese, mentre il passaporto è riconosciuto come documento valido in quanto esiste una relazione di "fiducia" fra i vari stati del globo.

Il documento di identità per utenti, computer, aziende, etc prende il nome di certificato digitale.

Come si è detto, la criptografia asimmetrica si basa su una coppia di chiavi, di cui una pubblica e una privata, che viene utilizzata per criptare/decriptare i dati. Mentre la chiave privata deve rimanere segreta, la chiave pubblica è generalmente incorporata in un certificato che collega quella chiave a una particolare persona, computer o organizzazione.

X.509 PKI è uno standard che identifica i requisiti per un certificato a chiave pubblica. In base a questo standard, un certificato contiene informazioni relative a un soggetto, inclusa la sua chiave pubblica. Un'autorità di certificazione (Certification Authority, o "CA") ha il compito di emette certificati e le parti coinvolte nella comunicazione si affidano alla CA per verificare l'identità del soggetto.

La struttura di un certificato X.509 è la seguente:

---------------------------------------------------------------------
-- X.509 signed certificate
---------------------------------------------------------------------
SignedContent ::= SEQUENCE
{
  certificate         CertificateToBeSigned,
  algorithm           Object Identifier,
  signature           BITSTRING
}
---------------------------------------------------------------------
-- X.509 certificate to be signed
---------------------------------------------------------------------
CertificateToBeSigned ::= SEQUENCE
{
  version                 [0] CertificateVersion DEFAULT v1,
  serialNumber            CertificateSerialNumber,
  signature               AlgorithmIdentifier,
  issuer                  Name
  validity                Validity,
  subject                 Name
  subjectPublicKeyInfo    SubjectPublicKeyInfo,
  issuerUniqueIdentifier  [1] IMPLICIT UniqueIdentifier OPTIONAL,
  subjectUniqueIdentifier [2] IMPLICIT UniqueIdentifier OPTIONAL,
  extensions              [3] Extensions OPTIONAL
}

Il namespace Windows.Security.Cryptography.Certificates contiene tipi e metodi che consentono di richiedere, installare o importare certificati digitali. Il seguente snippet mostra come creare la richiesta per un certificato.

private async Task CreateRequestAsync()
{
    CertificateRequestProperties crp = new CertificateRequestProperties();
    crp.FriendlyName = "MyCertificate";
    crp.Subject = "Html.it";
    crp.KeyProtectionLevel = KeyProtectionLevel.NoConsent;
    crp.KeyUsages = EnrollKeyUsages.All;
    crp.Exportable = ExportOption.Exportable;
    crp.KeySize = 2048;
    crp.KeyStorageProviderName = KeyStorageProviderNames.SoftwareKeyStorageProvider;
    crp.KeyAlgorithmName = "SHA256";
    String request = await CertificateEnrollmentManager.CreateRequestAsync(crp);
    return request;
}

Per prima cosa, il codice istanzia un oggetto di tipo CertificateRequestProperties con le proprietà della richiesta. In particolare, l'enum KeyProtectionLevel indica il livello di protezione e accetta uno dei seguenti valori:

  • NoConsent: Nessuna particolare protezione (questo è il valore di default)
  • ConsentOnly: l'utente viene notificato tramite un dialog quando la chiave privata viene creata o utilizzata.
  • ConsentWithPassword: l'utente viene richiesto di inserire una password per la chiave, non appena questa viene create o viene uutilizzata.

La prossima immagine mostra un esempio di dialog per la creazione di una password da associare al certificato:

La proprietà KeyUsages (di tipo EnrollKeyUsage) specifica il tipo di operazione che può essere eseguito con la chiave privata associata al certificato richiesto. I valori possibili sono i seguenti:

  • None: nessun uso specifico indicato.
  • Decryption: la chiave può essere usata solo per decriptare un messaggio.
  • Signing: la chiave può essere usata per firmare (questo rappresenta il valore di default).
  • KeyAgreement: la chiave può essere usata per lo scambio di chiavi di sessione.
  • All: la chiave può essere usata per uno qualunque degli scopi sopra indicate.

La proprietà Exportable specifica se la chiave privata possa essere o meno esportata (di default, non è esportabile per ragioni di sicurezza), mentre la proprietà KeySize permette di indicare la lunghezza della chiave da generare (per gli algoritmi RSA e DSA, il valore di default è di 2048 bit).

Un'altra proprietà da menzionare è KeyStorageProviderName, la quale indica il key storage provider (KSP) da usare per generare la chiave privata. Un'applicazione Windows Store può accedere a tre KSP:

  • PlatformKeyStorageProvider: corrisponde al Microsoft Platform Key Storage Provider
  • SmartcardKeyStorageProvider: corrisponde al Microsoft Smart Card Key Storage Provider
  • SoftwareKeyStorageProvider: corrisponde al Microsoft Software Key Storage Provider (questo è il valore di default)

Infine, la proprietà KeyAlgorithmName specifica il tipo di algoritmo da usare per generare la chiave pubblica. Il valore di default è RSA.

La classe CertificateEnrollmentManager espone anche un metodo asincrono, InstallCertificateAsync, che permette di installare un certificato nell'app container del computer. Il seguente snippet illustra questo punto.

private async void InstallCertificate_Click(object sender, RoutedEventArgs e)
{
    String response = "";
    String request = await this.CreateRequestAsync();
    if (String.IsNullOrEmpty(request))
    {
        // qualcosa è andato storto
    }
    try
    {
        response = await SubmitCertificateRequestAndGetResponseAsync(request, "http://www.contoso.org/");
        if (!String.IsNullOrEmpty(response))
        {
            await CertificateEnrollmentManager.InstallCertificateAsync(response, InstallOptions.None);
        }
    }
    catch (Exception ex)
    {
        // gestire l’eccezione
    }
}
private Task<string> SubmitCertificateRequestAndGetResponseAsync(String certificateRequest, String url)
{
    // invia la richiesta alla CA e attende la risposta
}

Dopo aver richiesto un certificato tramite il metodo CreateRequestAsync, il codice invoca il metodo SubmitCertificateRequestAndGetResponseAsync per creare una richiesta HTTP, inviare la richiesta di certificato al server indicato come secondo parametro, e quindi restituisce il certificato proveniente dal server.

Una volta recuperato il certificato, il codice chiama il metodo InstallCertificateAsync passando come parametro il certificato stesso, assieme a un'istanza dell'enum InstallOption che specifica le opzioni di installazione.

In Windows, i certificati rilasciati così come le richieste di certificati sono memorizzati nel Microsoft Certificate Store: per l'elenco e la descrizione dei vari store, si rinvia alla documentazione su MSDN.

I certificati sono normalmente salvati per singolo utente e per singola applicazione. Un'applicazione Windows Store può accedere in scrittura unicamente al proprio storage di certificati, mentre in lettura può accedere, oltre che ai certificati nello storage applicativo, anche a quelli presenti sulla macchina locale. I certificati aggiunti da un'app non possono essere letti da altre applicazioni Windows Store e quando l'app viene disinstallata, anche i relativi certificati vengono rimossi.

Infine, per importare in modo asincrono un certificato a partire da un messaggio Personal Information Exchange (PFX), puoi sfruttare il metodo ImportPfxDataAsync della classe CertificateEnrollmentManager. Il prossimo snippet ne mostra un esempio:

private async void ImportCertificate_Click(object sender, RoutedEventArgs e)
{
    try
    {
        string pfxCertificate = new ResourceLoader().GetString("MyCertificate");
        string password = "password";
        string friendlyName = "Pfx Certificate Sample";
        await CertificateEnrollmentManager.ImportPfxDataAsync(
            pfxCertificate,
            password,
            ExportOption.NotExportable,
            KeyProtectionLevel.NoConsent,
            InstallOptions.None,
            friendlyName);
    }
    catch (Exception ex)
    {
        //
    }
}

È possibile esportare da un device per importarlo successivamente su altri device.

Proteggere i tuoi dati con la classe DataProtectionProvider

La classe DataProtectionProvider, contenuta nel namespace Windows.Security.Cryptography.DataProtection), espone metodi che permetttono di proteggere dati sensibili tramite criptografia. Questa classe ha due costruttori: un costruttore di default e un costruttore con che accetta una stringa che descrive il tipo di protezione da utilizzare (descriptor). È importante ricordare che il costruttore di default deve essere usato prima di iniziare un'operazione di decriptazione, mentre l'altro prima di iniziare a criptare i dati (e non viceversa).

Il codice che segue mostra come usare il metodo ProtectAsync della classe DataProtectionProvider per criptare un buffer di dati (da notare l'uso della versione overload del costruttore):

private IBuffer _protectedBuffer = null;
private async void ProtectButton_Click(object sender, RoutedEventArgs e)
{
    String descriptor = "LOCAL=user";
    DataProtectionProvider dpp = new DataProtectionProvider(descriptor);
    IBuffer binaryMessage = CryptographicBuffer.ConvertStringToBinary(tbPlainText.Text, BinaryStringEncoding.Utf8);
    this._protectedBuffer = await dpp.ProtectAsync(binaryMessage);
    tbEncryptedText.Text = CryptographicBuffer.EncodeToBase64String(this._protectedBuffer);
}

Per decriptare i dati precedentemente protetti è possibile sfruttare il metodo UnprotectAsync, il quale accetta come parametro un buffer di byte che rappresenta il messaggio criptato, come mostrato nel seguente snippet (si noti l'uso del costruttore di default):

private async void UnprotectButton_Click(object sender, RoutedEventArgs e)
{
    if (this._protectedBuffer != null)
    {
        DataProtectionProvider dpp = new DataProtectionProvider();
        IBuffer unprotectedBuffer = await dpp.UnprotectAsync(this._protectedBuffer);
        tbUnprotectedText.Text = CryptographicBuffer.ConvertBinaryToString(BinaryStringEncoding.Utf8, unprotectedBuffer);
    }
}

Per testare questo codice, possiamo usare la seguente definizione XAML per la pagina principale dell'app:

<Page
    x:Class="Demo.Html.it.CryptoSample.CS.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Demo.Html.it.CryptoSample.CS"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">
    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <StackPanel>
            <TextBlock x:Name="tbPlainText" Text="Messaggio da proteggere" Margin="5" FontSize="20" />
            <Button x:Name="ProtectButton" Margin="5" Content="Cripta il messaggio" Click="ProtectButton_Click" />
            <TextBlock x:Name="tbEncryptedText" Margin="5" FontSize="20" TextWrapping="Wrap" />
            <Button x:Name="UnprotectButton" Margin="5" Content="Decripta il messaggio" Click="UnprotectButton_Click" />
            <TextBlock x:Name="tbUnprotectedText" Margin="5" FontSize="20" TextWrapping="Wrap" />
        </StackPanel>
    </Grid>
</Page>

Se ora eseguiamo l'applicazione, il risultato dovrebbe essere simile a quello mostrato nella prossima immagine.

È anche possibile proteggere stream di dati tramite il metodo ProtectStreamAsync. Questo metodo accetta due parametri: un oggetto IInputStream che rappresenta lo stream da proteggere, e un oggetto IOutputStream che conterrà lo stream criptato. Analogamente, per decriptare uno stream è sufficiente chiamare il metodo UnprotectStreamAsync, che accetta sempre due parametri: un oggetto IInputStream che rappresenta lo stream protetto, e un oggetto IOutputStream contenente lo stream decriptato.

Infine, per proteggere la password dell'utente, è possibile sfruttare anche la classe PasswordVault contenuta nel namespace Windows.Security.Credentials.


Ti consigliamo anche