2021-12-04 / Bartłomiej Kurek
Tworzenie certyfikatów SSL w Python (#1 - CA)

Pakiety: pyOpenSSL, cryptography

Do przetwarzania certyfikatów w języku Python możemy użyć bibliotek pyOpenSSL oraz cryptography.
Bilioteka pyOpenSSL jest zestawem "niskopoziomowych" funkcji wspierających podzbiór funkcjonalności OpenSSL, a cryptography jest interfejsem wyższego poziomu.

Standard X.509: RFC 5280

Wytyczne standardu internetowych certyfikatów i oraz listy certyfikatów unieważnionych znajdziemy
w dokumencie rfc5280. Oficjalna nazwa tego standardu to:
Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile.
Dokumenacja ta zawiera szczegółowe założenia infrastruktury dla certyfikatów opartych o klucze (PKI - Public Key Infrastructure).

Tworzenie certyfikatu urzędu (CA)

Zacznijmy od zaprezentowania skryptu, który tworzy parę kluczy (prywatny/ publiczny) oraz certyfikat RSA dla nowego urzędu, a następnie zapisuje dane klucza i certyfikatu do jednego wspólnego pliku pem.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import OpenSSL


key = OpenSSL.crypto.PKey()
key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048)

ca = OpenSSL.crypto.X509()
ca.set_version(2)
ca.set_serial_number(1)
ca.set_pubkey(key)

ca.gmtime_adj_notBefore(0)
ca.gmtime_adj_notAfter(3600 * 24)

subject = ca.get_subject()
subject.CN = "MyCommonName"
subject.C = "XX"  # country code
subject.ST = "MyState"
subject.L = "MyLocation"
subject.O = "MyOrganization"  # noqa: E741
subject.OU = "MyUnitName"
subject.emailAddress = "me@localhost"

ca.set_issuer(subject)

ca.sign(key, "sha256")

ca.add_extensions([
    OpenSSL.crypto.X509Extension(
        b"basicConstraints",
        True,
        b"CA:TRUE, pathlen:0"
    ),
    OpenSSL.crypto.X509Extension(
        b"keyUsage",
        True,
        b"keyCertSign, cRLSign"
    ),
    OpenSSL.crypto.X509Extension(
        b"subjectKeyIdentifier",
        False,
        b"hash",
        subject=ca
    ),
])

ca.add_extensions([
    OpenSSL.crypto.X509Extension(
        b"authorityKeyIdentifier",
        False,
        b"keyid:always",
        issuer=ca
    ),
])

pem_binary = OpenSSL.crypto.dump_certificate(
    OpenSSL.crypto.FILETYPE_PEM, ca
)

passphrase = "secret"

key_binary = OpenSSL.crypto.dump_privatekey(
    OpenSSL.crypto.FILETYPE_PEM,
    key,
    "AES-256-CBC",
    passphrase.encode(),
)

with open("ca.pem", "w") as fh:
    fh.write(pem_binary.decode())
    fh.write(key_binary.decode())

Omówienie skryptu

Moduł pyOpenSSL

Na początku musimy oczywiście zaimportować moduł OpenSSL.
Zainstalować możemy go komendą:

pip install pyOpenSSL

Tworzenie obiektu klucza publicznego

W skrypcie tworzymy najpierw parę kluczy (key-pairs). Powyższy skrypt tworzy klucz typu RSA o długości 2048 bajtów.
Tworzymy obiekt klasy PKey ("public key").

key = OpenSSL.crypto.PKey()
key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048)

Klasa PKey implementuje kilka metod, którymi w razie potrzeby możemy się posłużyć w celu
uzyskania informacji o kluczu, czy jego przekształcenia na obiekt zgodny z biblioteką cryptography.

>>> key = OpenSSL.crypto.PKey()
>>> key.bits()
0
>>> key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048)
>>> key.bits()
2048
>>> key.type()
6
>>> key.check()
True
>>> key.to_cryptography_key()
<cryptography.hazmat.backends.openssl.rsa._RSAPrivateKey object at 0x7f60cb3042b0>

Tworzenie obiektu certyfikatu

Do stworzenia obiektu certyfikatu używamy klasy X509.

ca = OpenSSL.crypto.X509()

Kiedy mamy już obiekt certyfikatu, używamy jego metod do uzupełnienia danych.
Wersje certyfikatu numeruje się od 0, a zatem aby uzyskać certyfikat w wersji 3 (v3) używamy
liczby 2:

ca.set_version(2)

Ustawiamy numer seryjny certyfikatu i przypisujemy do niego wcześniej wygenerowany klucz publiczny:

ca.set_serial_number(1)
ca.set_pubkey(key)

Certyfikaty SSL wystawiane są na określony przedział czasu. Określamy zatem (w sekundach) od kiedy i przez jak długi czas certyfikat będzie ważny. Służą do tego metody "gmtime_adj_notBefore" oraz "gmtime_adj_notAfter". Pierwszej z nich możemy przekazać 0, jeśli chcemy aby certyfikat był ważny "od teraz", od momentu generowania. Drugiej metodzie przekazujemy liczbę sekund odpowiadającej ważności certyfkatu, przykładowo 365 dni to: 3600 * 24 * 365.

ca.gmtime_adj_notBefore(0)
ca.gmtime_adj_notAfter(3600 * 24)

W następnym kroku uzupełniamy dane podmiotu. Obiekt certyfikatu posiada metodę get_subject(), która zwraca
nam obiekt struktury subject. W nim uzupełniamy kolejne pola odpowiadające nazwie podmiotu, jego kraju, itd.

subject = ca.get_subject()
subject.CN = "MyCommonName"
subject.C = "XX"  # country code
subject.ST = "MyState"
subject.L = "MyLocation"
subject.O = "MyOrganization"  # noqa: E741
subject.OU = "MyUnitName"
subject.emailAddress = "me@localhost"

Pole "C" oznacza kraj, umieszczamy tam dwuliterowy kod kraju (np. PL).
Pole "O" zawiera nazwę organizacji. Użyłem tam komentarza "noqa" wyłączającego w linterze błąd E741 w tej linii, gdyż standard PEP8 wymienia użycie zmiennych o nazwach "I", "O", "l" jako nieczytelne.

Następnie ustawiamy dane wystawcy certyfikatu używając wcześniej przygotowanego obiektu subject.

ca.set_issuer(subject)

Certyfikat podpisujemy stworzonym kluczem publicznym podając jako drugi argument żądany algorytm.

ca.sign(key, "sha256")

Składowe certyfikatu oznaczające zakres jego wykorzystania to tzw. rozszerzenia (extensions).
Biblioteka pyOpenSSL posiada klasę X509Extension, której przekazujemy listę właściwych rozszerzeń.
W Python3 musimy przekazać wszystkie ciągi znakowe jako bajty.

Dla certyfikatu urzędu (CA), musimy wskazać w polu basicConstraints wartość CA:TRUE.
"pathlen:0" oznacza, że certyfikaty poświadczone tym podpisem urzędowym nie mogą służyć do certyfikowania kolejnych. W tym przypadku nasz urząd nie wystawia certyfikatów urzędów pośrednich.
Pominięcie tego pola (pathlen) oznacza, że w całym łańcuchu poprawnej certyfikacji nie byłoby limitu urzędów pośrednich. Użycie liczby większej od zera oznaczałoby maksymalną długość takiego łańcucha urzędow. Zatem nasze pierwsze rozszerzenie ma postać:

OpenSSL.crypto.X509Extension(
    b"basicConstraints",
    True,
    b"CA:TRUE, pathlen:0"
)

Dane certyfikatu zawierają w sobie również informację o możliwym zakresie zastosowania certyfikatu.
Dla certyfkatu urzędu ograniczam zatem zakres do podpisywania innych certyfikatów (keyCertSign) oraz podpisywania listy certyfikatów unieważnionych (cRLSign).
Drugie rozszerzenie ma zatem postać:

OpenSSL.crypto.X509Extension(
    b"keyUsage",
    True,
    b"keyCertSign, cRLSign"
)

Rozszerzenie trzecie dotyczy subjectKeyIdentifier. subjectKeyIdentifier (SKID) to identyfikator klucza podmiotu zawartego w certyfikacie. RFC stanowi, że to pole NIE MOŻE być oznaczone jako "critical", zatem drugi arument to False. Jako podmiot (subject) wskazujemy ten właśnie certyfikat.

OpenSSL.crypto.X509Extension(
    b"subjectKeyIdentifier",
    False,
    b"hash",
    subject=ca
)

Następnie - mając już "subjectKeyIdentifier" wśród rozszerzeń certyfikatu, ustawiamy identyfikator klucza wystawcy (urzędu) - w przypadku głównego certyfikatu urzędowego ten identyfikator jest taki sam jak identyfikator klucza podmiotu (subjectKeyIdentifier).
Rozszerzenie to dodajemy w osobnym wywołaniu metody "add_extensions", kiedy już ustawimy wcześniej "subjectKeyIdentifier". Rozszerzenie "authorityKeyIdentifier" również NIE MOŻE być oznaczone jako critical w przypadku certyfikwatów urzędowych. Certyfikat tego urzędu nie ma nadrzędnego certyfikatu, a zatem jako wystawcę wskazujemy nasz własny urząd.

ca.add_extensions([
    OpenSSL.crypto.X509Extension(
        b"authorityKeyIdentifier",
        False,
        b"keyid:always",
        issuer=ca
    ),
])

Na koniec przygotowujemy sobie dane certyfikatu w formacie PEM:

pem_binary = OpenSSL.crypto.dump_certificate(
    OpenSSL.crypto.FILETYPE_PEM, ca
)

Przygotowujemy również dane samego klucza zaszyfrowanego hasłem:

passphrase = "secret"

key_binary = OpenSSL.crypto.dump_privatekey(
    OpenSSL.crypto.FILETYPE_PEM,
    key,
    "AES-256-CBC",
    passphrase.encode(),
)

a całość umieszczamy w pliku. Możemy umieścić certyfikat i klucz w osobnych plikach lub zbiorczo w jednym wspólnym w formacie PEM:

with open("ca.pem", "w") as fh:
    fh.write(pem_binary.decode())
    fh.write(key_binary.decode())

Sprawdzamy dane certyfikatu (openssl)

Mając już certyfikat umieszczony w pliku (ca.pem), możemy użyć np. komend openssl do sprawdzenia i wyświetlenia danych w formie tekstowej.
W celu wyświetlenia certyfikatu użyjemy openssl x509, a informacje o kluczu uzyskamy używając openssl rsa.

Certyfikat

$ openssl x509 -text -in ca.pem
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 1 (0x1)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN = MyCommonName, C = XX, ST = MyState, L = MyLocation, O = MyOrganization, OU = MyUnitName, emailAddress = me@localhost
        Validity
            Not Before: Dec  4 16:12:00 2021 GMT
            Not After : Dec  5 16:12:00 2021 GMT
        Subject: CN = MyCommonName, C = XX, ST = MyState, L = MyLocation, O = MyOrganization, OU = MyUnitName, emailAddress = me@localhost
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:bb:33:03:d8:15:9a:32:59:28:9f:bc:d9:cf:30:
                    a1:7a:5b:e0:15:c3:ed:49:8f:0b:e1:95:b9:04:38:
                    26:46:1e:ad:88:32:66:53:2c:0c:fd:fc:4e:d4:38:
                    a9:b2:da:ed:d8:c6:62:27:1f:64:d6:74:2f:a0:31:
                    06:d9:31:6f:44:ed:d5:7a:e8:e9:8f:49:ec:2b:99:
                    98:e0:a2:e7:ec:fc:44:9d:47:de:02:9c:2f:c0:6d:
                    0d:6d:da:a4:1d:36:07:b5:55:0d:85:de:40:dd:06:
                    49:f4:94:29:98:72:14:f5:ee:7b:45:77:c9:b8:61:
                    b9:2b:49:63:ea:7c:34:81:9c:9e:83:69:37:b3:e5:
                    5e:3c:50:0e:ff:7f:fe:2e:97:a8:5c:e6:4e:a8:01:
                    bd:8e:ca:56:29:38:df:2f:e7:78:95:7d:ad:f7:65:
                    45:ec:48:55:af:be:89:13:c6:58:76:94:d6:14:1d:
                    d6:73:ab:ce:55:78:b5:49:7e:14:0d:c0:a2:f9:c2:
                    fd:7c:55:87:1f:0a:35:79:a5:1c:44:d6:1d:8e:63:
                    04:d1:47:0e:9b:12:10:98:0c:8a:d8:e4:81:92:5f:
                    ea:81:8f:43:55:fc:7e:97:27:95:36:eb:ba:ea:6a:
                    14:69:77:f5:84:86:b8:36:84:a5:b5:97:9e:bd:c9:
                    48:51
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Basic Constraints: critical
                CA:TRUE, pathlen:0
            X509v3 Key Usage: critical
                Certificate Sign, CRL Sign
            X509v3 Subject Key Identifier: 
                B2:1F:10:F1:88:A7:96:85:3F:38:99:6C:48:63:E9:EB:44:3E:C7:68
            X509v3 Authority Key Identifier: 
                keyid:B2:1F:10:F1:88:A7:96:85:3F:38:99:6C:48:63:E9:EB:44:3E:C7:68

    Signature Algorithm: sha256WithRSAEncryption
         3f:ea:7b:39:37:b0:ab:8d:5a:2b:00:93:69:f4:11:ba:32:4b:
         e1:8f:d9:e5:4e:89:d0:74:22:67:9e:27:e2:87:35:39:2e:59:
         d4:d4:b7:e7:49:23:30:59:32:76:71:9b:bb:db:34:71:f7:95:
         d9:cd:1f:12:ff:ce:f5:8f:40:76:eb:80:66:d5:85:35:a1:a4:
         0e:3a:c8:4c:b8:54:52:75:9b:e0:7b:1b:45:dd:82:4d:71:5b:
         ed:bc:25:ae:7a:78:8f:77:02:f1:a9:09:7d:10:0c:c9:9e:6e:
         0d:09:48:4c:33:d3:32:2b:4a:e6:94:99:e5:8a:72:cf:20:1b:
         49:8b:c0:20:e0:37:27:29:0b:01:06:c2:67:e3:5a:8c:39:79:
         a4:3b:2e:da:c4:73:f7:43:d1:58:0b:10:e5:69:1a:3a:af:ff:
         de:33:85:76:36:be:eb:64:1e:7d:e2:bf:1a:60:11:56:42:d0:
         14:bc:87:75:bf:68:9d:42:39:67:0a:54:40:c4:fe:3f:fe:1d:
         f6:fe:32:57:bb:3d:c9:e2:f9:98:12:5c:66:c5:e8:e3:c5:7a:
         7f:29:f9:8b:19:4b:63:85:6e:d9:2f:ef:f9:5e:eb:fe:91:7b:
         3d:27:b6:17:75:f6:3b:62:36:4e:15:f2:cf:97:bf:48:58:a1:
         0b:15:52:68
-----BEGIN CERTIFICATE-----
MIIEDzCCAvegAwIBAgIBATANBgkqhkiG9w0BAQsFADCBljEVMBMGA1UEAwwMTXlD
b21tb25OYW1lMQswCQYDVQQGEwJYWDEQMA4GA1UECAwHTXlTdGF0ZTETMBEGA1UE
BwwKTXlMb2NhdGlvbjEXMBUGA1UECgwOTXlPcmdhbml6YXRpb24xEzARBgNVBAsM
Ck15VW5pdE5hbWUxGzAZBgkqhkiG9w0BCQEWDG1lQGxvY2FsaG9zdDAeFw0yMTEy
MDQxNjEyMDBaFw0yMTEyMDUxNjEyMDBaMIGWMRUwEwYDVQQDDAxNeUNvbW1vbk5h
bWUxCzAJBgNVBAYTAlhYMRAwDgYDVQQIDAdNeVN0YXRlMRMwEQYDVQQHDApNeUxv
Y2F0aW9uMRcwFQYDVQQKDA5NeU9yZ2FuaXphdGlvbjETMBEGA1UECwwKTXlVbml0
TmFtZTEbMBkGCSqGSIb3DQEJARYMbWVAbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B
AQEFAAOCAQ8AMIIBCgKCAQEAuzMD2BWaMlkon7zZzzChelvgFcPtSY8L4ZW5BDgm
Rh6tiDJmUywM/fxO1Dipstrt2MZiJx9k1nQvoDEG2TFvRO3Veujpj0nsK5mY4KLn
7PxEnUfeApwvwG0NbdqkHTYHtVUNhd5A3QZJ9JQpmHIU9e57RXfJuGG5K0lj6nw0
gZyeg2k3s+VePFAO/3/+LpeoXOZOqAG9jspWKTjfL+d4lX2t92VF7EhVr76JE8ZY
dpTWFB3Wc6vOVXi1SX4UDcCi+cL9fFWHHwo1eaUcRNYdjmME0UcOmxIQmAyK2OSB
kl/qgY9DVfx+lyeVNuu66moUaXf1hIa4NoSltZeevclIUQIDAQABo2YwZDASBgNV
HRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUsh8Q8Yin
loU/OJlsSGPp60Q+x2gwHwYDVR0jBBgwFoAUsh8Q8YinloU/OJlsSGPp60Q+x2gw
DQYJKoZIhvcNAQELBQADggEBAD/qezk3sKuNWisAk2n0EboyS+GP2eVOidB0Imee
J+KHNTkuWdTUt+dJIzBZMnZxm7vbNHH3ldnNHxL/zvWPQHbrgGbVhTWhpA46yEy4
VFJ1m+B7G0Xdgk1xW+28Ja56eI93AvGpCX0QDMmebg0JSEwz0zIrSuaUmeWKcs8g
G0mLwCDgNycpCwEGwmfjWow5eaQ7LtrEc/dD0VgLEOVpGjqv/94zhXY2vutkHn3i
vxpgEVZC0BS8h3W/aJ1COWcKVEDE/j/+Hfb+Mle7Pcni+ZgSXGbF6OPFen8p+YsZ
S2OFbtkv7/le6/6Rez0nthd19jtiNk4V8s+Xv0hYoQsVUmg=
-----END CERTIFICATE-----

Powyżej widzimy m.in zawartość zdefiniowanych przez nas rozszerzeń. Jest to certyfikat urzędu (CA:TRUE), użycie ograniczone jest do podpisywania certyfikatów i listy CRL, a Subject Key Identifier jest taki sam jak Authority Key Identifier:

        X509v3 extensions:
            X509v3 Basic Constraints: critical
                CA:TRUE, pathlen:0
            X509v3 Key Usage: critical
                Certificate Sign, CRL Sign
            X509v3 Subject Key Identifier: 
                B2:1F:10:F1:88:A7:96:85:3F:38:99:6C:48:63:E9:EB:44:3E:C7:68
            X509v3 Authority Key Identifier: 
                keyid:B2:1F:10:F1:88:A7:96:85:3F:38:99:6C:48:63:E9:EB:44:3E:C7:68

Trochę wyżej widzimy również dane wystawcy (pole Issuer), dane certyfikowanego podmiotu (Subject), które w przypadku tego urzędu są takie same (nie jest to urząd pośredni).
Czas ważności (Validity) tego certyfikatu wyznaczony jest na jeden pełny dzień od momentu jego wygenerowania:

        Validity
            Not Before: Dec  4 16:12:00 2021 GMT
            Not After : Dec  5 16:12:00 2021 GMT

Klucz prywatny

Do uzyskania informacji o kluczu będziemy musieli podać hasło. Poniżej zamieszczam komendę, która wyświetla informacje o kluczu oraz sam klucz RSA.

$ openssl rsa -text -in ca.pem 
Enter pass phrase for ca.pem:
RSA Private-Key: (2048 bit, 2 primes)
modulus: [...]

Dla zwięzłości pomijam całe wyjście komendy "openssl rsa".