Eine Certificate Authority in Python (Update)

Für eines meiner iOS-Projekte wurde es nötig, dass sich die App mit einem Webservice verbindet und Daten speichert. Bei der Überlegung, wie ich nun die Daten im Webservice am sichersten einem Gerät zuordne, blieb ich bei einer Lösung mittels TLS Client Authentication hängen.

Der schwierige Teil ist nun einerseits die Client Zertifikate in dem Python Backend zu erstellen (Da der OpenSSL Wrapper M2Crypto ein paar Bugs hat) und andererseits das Zertifikat auf dem Gerät zu benutzen (Da Apple leider kaum etwas bietet, wodurch ich auf OpenSSL zurückgreifen musste). In dem ersten Teil dieses Zweiteilers möchte ich mich zunächst dem Python Backend zuwenden, da dieses mir die meisten Probleme bereitet hat.

Um das Zertifikat zu erstellen, habe ich mir eine kleine Helferfunktion gebastelt (da ich diese später nochmals weiterverwenden muss), welche man im folgenden bestaunen kann. Aber keine Angst ich werde darunter erklären was diese macht.

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
72
def generate_certificate(type, ca_cert, ca_key, ca_stub, pubkey_pem,
                         common_name, expiration_date, comment=None):
    '''
        Generate a certificate of the given type and with the
        given parameters.
    '''

    assert issubclass(type, DeclarativeBase)

    cert = X509.X509()

    name = X509.X509_Name()
    name.CN = common_name
    cert.set_subject_name(name)

    # Set the public key of the certificate
    pubKey = EVP.PKey()
    pubKey.assign_rsa(RSA.load_pub_key_bio(BIO.MemoryBuffer(pubkey_pem)))
    cert.set_pubkey(pubKey)

    # Set the X509 extenstions
    cert.add_ext(X509.new_extension('nsCertType', 'client'))
    if comment is not None:
        cert.add_ext(X509.new_extension('nsComment', comment))
    cert.add_ext(X509.new_extension('extendedKeyUsage', 'clientAuth',
                 critical=1))
    cert.add_ext(X509.new_extension('keyUsage', 'digitalSignature',
                 critical=1))
    cert.add_ext(X509.new_extension('basicConstraints', 'CA:FALSE',
                 critical=1))

    # Create the subject key identifier
    modulus = cert.get_pubkey().get_modulus()
    sha_hash = hashlib.sha1(modulus).digest()
    sub_key_id = ":".join(["%02X"%ord(byte) for byte in sha_hash])

    cert.add_ext(X509.new_extension('subjectKeyIdentifier', sub_key_id))

    # Authority Identifier
    cert.add_ext(ca_stub.get_ext('authorityKeyIdentifier'))

    # Certificate is valid from now
    notBefore = ASN1_UTCTIME(m2.x509_get_not_before(cert.x509))
    notBefore.set_datetime(datetime.now())

    # Certificate is valid for expires_in seconds
    notAfter  = ASN1_UTCTIME(m2.x509_get_not_after(cert.x509))
    notAfter.set_datetime(expiration_date)

    # Interact with the db and get the serial number

    certificate_entry = type()

    certificate_entry.subject = str(name) # Will be /CN=common_name
    certificate_entry.expiration_date = expiration_date
    certificate_entry.revoked = False

    Session.add(certificate_entry)
    Session.commit()

    # Now we do have a valid serial number
    cert.set_serial_number(certificate_entry.serial_number)

    """
        Sign the certificate with our
        intermediate CA and make it valid
    """
    cert.set_issuer(ca_cert.get_subject())
    cert.sign(ca_key, 'sha1')

    # Return the certificate in PEM format
    return cert.as_pem()

Fangen wir mit den Parametern der Funktion an:

  • type …ist eine Klasse die von der DeclarativeBase von SQLAlchemy abgeleitet wurde. Diese wird benutzt um das Zertifikat in der Datenbank zu verewigen und gleichzeitig eine Serialnummer für es zu bekommen.
  • ca_cert, ca_key …sind ein X509 und ein EVP Objekt die einmal das Zertifikat bzw. den privaten Schlüssel der CA enthalten, die das Zertifikat erstellen soll.
  • pubkey_pem …ist ein String, welcher den öffentlichen Schlüssel im PEM Format enthält, der in das Zertifikat eingebettet werden soll.
  • common_name …ist ein String welcher den Allgemeinen Namen enthält für das Zertifikat. Man kann noch viel mehr in das Subject eines Zertifikates einbringen, aber für meine Zwecke reicht der Allgemeine Name.

Nach dem das geklärt wurde, sollte der Rest des Codes ziemlich einleuchtend sein. Kopfzerbechen haben mir allerdings der subjectKeyIdentifier und authorityKeyIdentifier bereitet. Im normlen OpenSSL Kommandozeilen Programm gibt es einfache Optionen wie “hash”, welche M2Crypto leider nicht unterstützt.

Daher muss man den subjectKeyIdentifer wie folgt berechnen:

1
2
3
4
5
6
# Create the subject key identifier
modulus = cert.get_pubkey().get_modulus()
sha_hash = hashlib.sha1(modulus).digest()
sub_key_id = ":".join(["%02X"%ord(byte) for byte in sha_hash])

cert.add_ext(X509.new_extension('subjectKeyIdentifier', sub_key_id))

Noch gemeiner war der authorityKeyIdentifier. Diesen kann man zwar auch selbst berechnen, aber durch einen Bug in M2Crypto stürzt das Programm immer ab, wenn man diesen dann setzt. Daher muss man sich hier einen Trick bedienen. Man nimmt ein bereits signirtes Zertifikat von der CA (Zum Beispiel mit dem openssl ca Kommando erstellt) und holt sich aus diesem einfach die authorityKeyIdentifier Extension und setzt diese:

1
2
# Authority Identifier
cert.add_ext(ca_stub.get_ext('authorityKeyIdentifier'))

Nun funktioniert’s!

(Update: das Setzen der Gültigkeitszeitspanne des Zertifikats funktioniert nun immer richtig.)