PKI – budowa certyfikatu (2) – rozszerzenia

Przedstawione poprzednio certyfikaty w wersji 1 oraz 2 są dosyć ubogie biorąc pod uwagę informacje w nich zapisane. Tak na prawdę znajdziemy tam tylko daty ważności oraz nazwy podmiotu i jednostki, która podpisała certyfikat.

W wersji 3 certyfikatów znajduje się pole z rozszerzeniami. Są to dodatkowe atrybuty. Mogą zawierać np. informacje ułatwiające identyfikację podmiotu, do którego należy certyfikat albo informacje o możliwościach kryptograficznych. Ten wpis będzie właśnie o nich.

Rozszerzeniami w X.509 v3 rządzi kilka zasad:

  • każde z rozszerzeń musi być oznaczone jako krytyczne bądź niekrytyczne;
  • każde rozszerzenie ma swój numer OID i posiada strukturę ASN.1;
  • certyfikat nie może zawierać więcej niż jedno rozszerzenie o konkretnym OID.
Typowe rozszerzenia certyfikatów
Podstawowe ograniczenia (Basic Constraints)
Informuje czy certyfikat jest certyfikatem CA i jaka jest dopuszczalna „głębokość” ścieżki certyfikacji.
Alternatywne nazwy podmiotu (Subject Alternative Name)
Pozwala na podanie alternatywnych nazw jak np. adresu email, adresu DNS albo IP.
Użycie klucza (Key Usage)
Definiuje cel klucza zapisanego w certyfikacie. Możliwe są: digitalSignature, nonRepudiation/contentCommitment, keyEncipherment, dataEncipherment, keyAgreement, keyCertSign, cRLSign, encipherOnly, decipherOnly. Niektóre z nich się wykluczają.
Rozszerzone użycie klucza (Extended Key Usage)
Definiuje cel certyfikatu (oprócz lub zamiast podstawowego użycia klucza). Przykłady: id-kp-serverAuth, id-kp-clientAuth, id-kp-codeSigning, id-kp-emailProtection, id-kp-timeStamping, id-kp-OCSPSigning. W praktyce to rozszerzenie znajduje się tylko w certyfikatach końcowych.
Punkty dystrybucji list CRL (CRL Distribution Points)
Rozszerzenie definiuje gdzie należy szukać list odwołanych certyfikatów wystawcy. Listy CRL wykorzystywane są do weryfikacji czy dany certyfikat nie został z jakiegoś powodu wycofany z użycia.
Programowy dostęp do rozszerzeń

Na początek proste wylistowanie rozszerzeń – ich numerów OID i nazw (o ile .dot je rozpoznaje).

using System.Security.Cryptography.X509Certificates;

var store = new X509Store(StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly);
foreach (var crt in store.Certificates)
{
    Console.WriteLine("-----------------------------");
    Console.WriteLine($"        Version: {crt.Version}");
    Console.WriteLine($"        Subject: {crt.Subject}");
    Console.WriteLine($"         Issuer: {crt.Issuer}");
    Console.WriteLine($"          Valid: {crt.NotBefore} - {crt.NotAfter}");
    Console.WriteLine($"     Thumbprint: {crt.Thumbprint}");
    Console.WriteLine($"  Serial number: {crt.SerialNumber}");
    Console.WriteLine($"     Extensions:");
    foreach (var ext in crt.Extensions)
    {
        Console.WriteLine($"\t\t({ext.Oid?.Value}) {ext.Oid?.FriendlyName}, isCritical: {ext.Critical}");
    }
}
store.Close();

Przykładowy certyfikat wyglądał będzie jak niżej.

Jak widać, zawiera on wspomniane wcześniej rozszerzenia z podstawowymi ograniczeniami, użyciem klucza i alternatywnymi nazwami podmiotu.

Jest jeszcze jedno rozszerzenie, którego .net nie umie nazwać – OID 1.3.6.1.4.1.311.84.1.1. Jest to informacja o tym, że certyfikat jest certyfikatem developerskim utworzonym przez narzędzia dotnet.

W przypadku X509Certificate2 wszystkie rozszerzenia są pochodnymi klasy bazowej X509Extension. Dostępne klasy pochodne:

  • X509BasicConstraintsExtension,
  • X509KeyUsageExtension,
  • X509EnhancedKeyUsageExtension,
  • X509SubjectAlternativeNameExtension,
  • X509SubjectKeyIdentifierExtension, 
  • X509AuthorityInformationAccessExtension,
  • X509AuthorityKeyIdentifierExtension.
X509v3 Podstawowe ograniczenia
string BasicConstraintsDetails(X509BasicConstraintsExtension ext)
{
    var builder = new StringBuilder();
    builder.Append($"CA: {ext.CertificateAuthority}");
    if (ext.HasPathLengthConstraint)
    {
        builder.Append($", PathLengthConstraint: {ext.PathLengthConstraint}");
    }
    return builder.ToString();
}
(2.5.29.19) X509v3 Basic Constraints, isCritical: True
       CA: True, PathLengthConstraint: 0
X509v3 Użycie klucza

string KeyUsageDetails(X509KeyUsageExtension ext)
{
    return ext.KeyUsages.ToString();
}
(2.5.29.15) X509v3 Key Usage, isCritical: True
       DataEncipherment, KeyEncipherment, NonRepudiation, DigitalSignature

Przyznaję, poszedłem tutaj nieco na łatwiznę 😉

KeyUsages zawiera flagi odpowiadające użyciom klucza. Można je weryfikować za pomocą procedury HasFlag. Dostępne flagi:

  • X509KeyUsageFlags.EncipherOnly – klucz może być używany tylko do szyfrowania,
  • X509KeyUsageFlags.CrlSign – klucz może służyć do podpisywania listy odwołania certyfikatów,
  • X509KeyUsageFlags.KeyCertSign – klucz może służyć do podpisywania certyfikatów,
  • X509KeyUsageFlags.KeyAgreement,
  • X509KeyUsageFlags.DataEncipherment – klucz może służyć do szyfrowania danych,
  • X509KeyUsageFlags.KeyEncipherment – klucz może służyć do szyfrowania klucza,
  • X509KeyUsageFlags.NonRepudiation – klucz może służyć do uwierzytelniania,
  • X509KeyUsageFlags.DigitalSignature – klucz może być używany jako podpis cyfrowy,
  • X509KeyUsageFlags.DecipherOnly – klucz może być używany tylko do odszyfrowywania.
X509.v3 Rozszerzone użycie klucza
string EnhancedKeyUsageDetails(X509EnhancedKeyUsageExtension ext)
{
    var usages = new List<string>();
    foreach (var u in ext.EnhancedKeyUsages)
    {
        usages.Add(EnhancedKeyUsageToFrendlyName(u));
    }
    return string.Join(", ", usages);
}

string EnhancedKeyUsageToFrendlyName(Oid oid)
{
    switch (oid.Value)
    {
    case "1.3.6.1.5.5.7.3.1": return "serverAuth";
    case "1.3.6.1.5.5.7.3.2": return "clientAuth";
    case "1.3.6.1.5.5.7.3.3": return "codeSigning";
    case "1.3.6.1.5.5.7.3.4": return "emailProtection";
    case "1.3.6.1.5.5.7.3.5": return "ipsecEndSystem";	
    case "1.3.6.1.5.5.7.3.6": return "ipsecTunnel";
    case "1.3.6.1.5.5.7.3.7": return "ipsecUser";
    case "1.3.6.1.5.5.7.3.8": return "timeStamping";
    case "1.3.6.1.5.5.7.3.9": return "ocspSigning";
    case "1.3.6.1.5.5.7.3.10": return "dvcs";
    case "1.3.6.1.5.5.7.3.11": return "sbgpCertAAServerAuth";
    case "1.3.6.1.5.5.7.3.12": return "id-kp-scvp-responder";
    case "1.3.6.1.5.5.7.3.13": return "id-kp-eapOverPPP";
    case "1.3.6.1.5.5.7.3.14": return "id-kp-eapOverLAN";
    case "1.3.6.1.5.5.7.3.15": return "id-kp-scvpServer";
    case "1.3.6.1.5.5.7.3.16": return "id-kp-scvpClient";
    case "1.3.6.1.5.5.7.3.17": return "id-kp-ipsecIKE";
    case "1.3.6.1.5.5.7.3.18": return "id-kp-capwapAC";
    case "1.3.6.1.5.5.7.3.19": return "id-kp-capwapWTP";
    case "1.3.6.1.5.5.7.3.20": return "id-kp-sipDomain";
    case "1.3.6.1.5.5.7.3.21": return "secureShellClient";
    case "1.3.6.1.5.5.7.3.22": return "secureShellServer";
    case "1.3.6.1.5.5.7.3.23": return "id-kp-sendRouter";
    case "1.3.6.1.5.5.7.3.24": return "id-kp-sendProxy";
    case "1.3.6.1.5.5.7.3.25": return "id-kp-sendOwner";
    case "1.3.6.1.5.5.7.3.26": return "id-kp-sendProxiedOwner";
    case "1.3.6.1.5.5.7.3.27": return "id-kp-cmcCA";
    case "1.3.6.1.5.5.7.3.28": return "id-kp-cmcRA";
    case "1.3.6.1.5.5.7.3.29": return "id-kp-cmcArchive";
    default: return $"unknown({oid.Value})";
    }
}
(2.5.29.37) X509v3 Extended Key Usage, isCritical: False
       clientAuth, emailProtection

W tym przypadku, mimo iż Oid posiada atrybut FriendlyName, nazwy były zawsze puste. ;-( Mapowanie identyfikatora na nazwę musiałem zrobić ręcznie na podstawie dokumentacji.

X509.v3 Alternatywne nazwy podmiotu

string SubjectAlternativeDetails(X509SubjectAlternativeNameExtension ext)
{
    var names = new List<string>();
    names.AddRange(ext.EnumerateDnsNames().Select(p => $"DNS:{p}"));
    names.AddRange(ext.EnumerateIPAddresses().Select(p => $"IP:{p}"));
    return string.Join(", ", names);
}
(2.5.29.17) X509v3 Subject Alternative Name, isCritical: True
       DNS:localhost

Pojawia się tu jednak jeden problem – standard przewiduje tu nieco więcej niż tylko adresy dns i ip. Może się tu trafić np adres email.

string SubjectAlternativeDetails(X509SubjectAlternativeNameExtension ext)
{
    return ext.Format(false);
}
(2.5.29.17) X509v3 Subject Alternative Name, isCritical: False
       email:pawel.worwag@gmail.com
X509.v3 Identyfikator klucza podmiotu
string SubjectKeyIdentifierDetails(X509SubjectKeyIdentifierExtension ext)
{
    return ext.SubjectKeyIdentifier ?? string.Empty;
}
(2.5.29.14) X509v3 Subject Key Identifier, isCritical: False
       9F2208AD37B103E4842D7568FE84BEE37A4C7ADA
X509.v3 Dostęp do informacji o urzędzie
string AuthorityInformationAccessDetails(X509AuthorityInformationAccessExtension ext)
{
    var list = new List<string>();
    list.AddRange(ext.EnumerateOcspUris().Select(p => $"OCSP:{p}"));
    list.AddRange(ext.EnumerateCAIssuersUris().Select(p => $"CA:{p}"));
    return string.Join(", ",list);
}
(1.3.6.1.5.5.7.1.1) Authority Information Access, isCritical: False
       OCSP:http://subca.ocsp-certum.com, CA:http://repository.certum.pl/ctrca.cer
Wnioski i spostrzeżenia

Powyższe przykłady pokazują, że do części danych w certyfikacie łatwo się dostać. Przynajmniej z pozoru. Gdy przyjrzymy się bliżej, okaże się że klasom rozszerzeń niestety sporo brakuje, w efekcie czego nie są wg mnie jakoś mocno przydatne.

Największą bolączką jest brak klasy dla rozszerzenia zawierającego punkty dystrybucji list CRL (CRL Distribution Point). Trzeba posiłkować się zewnętrznymi bibliotekami albo napisać parser samemu ;-(