Design for a X509 Certificate Authority

Current state and shortcomings

Import/export in Ganeti have a need for many unique X509 certificates. So far these were all self-signed, but with the new design for import/export they need to be signed by a Certificate Authority (CA).

Proposed changes

The plan is to implement a simple CA in Ganeti.

Interacting with an external CA is too difficult or impossible for automated processes like exporting instances, so each Ganeti cluster will have its own CA. The public key will be stored in …/lib/ganeti/ca/cert.pem, the private key (only readable by the master daemon) in …/lib/ganeti/ca/key.pem.

Similar to the RAPI certificate, a new CA certificate can be installed using the gnt-cluster renew-crypto command. Such a CA could be an intermediate of a third-party CA. By default a self-signed CA is generated and used.

Each certificate signed by the CA is required to have a unique serial number. The serial number is stored in the file …/lib/ganeti/ca/serial, replicated to all master candidates and never reset, even when a new CA is installed.

The threat model is expected to be the same as with self-signed certificates. To reinforce this, all certificates signed by the CA must be valid for less than one week (168 hours).

Implementing support for Certificate Revocation Lists (CRL) using OpenSSL is non-trivial. Lighttpd doesn’t support them at all and apparently never will in version 1.4.x. Some CRL-related parts have only been added in the most recent version of pyOpenSSL (0.11). Instead of a CRL, Ganeti will gain a new cluster configuration property defining the minimum accepted serial number. In case of a lost or compromised private key this property can be set to the most recently generated serial number.

While possible to implement in the future, other X509 certificates used by the cluster (e.g. RAPI or inter-node communication) will not be automatically signed by the per-cluster CA.

The commonName attribute of signed certificates must be set to the the cluster name or the name of a node in the cluster.

Software requirements

  • pyOpenSSL 0.10 or above (lower versions can’t set the X509v3 extension subjectKeyIdentifier recommended for certificate authority certificates by RFC 3280, section 4.2.1.2)

Code samples

Generating X509 CA using pyOpenSSL

The following code sample shows how to generate a CA certificate using pyOpenSSL:

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

ca = OpenSSL.crypto.X509()
ca.set_version(3)
ca.set_serial_number(1)
ca.get_subject().CN = "ca.example.com"
ca.gmtime_adj_notBefore(0)
ca.gmtime_adj_notAfter(24 * 60 * 60)
ca.set_issuer(ca.get_subject())
ca.set_pubkey(key)
ca.add_extensions([
  OpenSSL.crypto.X509Extension("basicConstraints", True,
                               "CA:TRUE, pathlen:0"),
  OpenSSL.crypto.X509Extension("keyUsage", True,
                               "keyCertSign, cRLSign"),
  OpenSSL.crypto.X509Extension("subjectKeyIdentifier", False, "hash",
                               subject=ca),
  ])
ca.sign(key, "sha1")

Signing X509 certificate using CA

The following code sample shows how to sign an X509 certificate using a CA:

ca_cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
                                          "ca.pem")
ca_key = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM,
                                        "ca.pem")

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

cert = OpenSSL.crypto.X509()
cert.get_subject().CN = "node1.example.com"
cert.set_serial_number(1)
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(24 * 60 * 60)
cert.set_issuer(ca_cert.get_subject())
cert.set_pubkey(key)
cert.sign(ca_key, "sha1")

How to generate Certificate Signing Request

The following code sample shows how to generate an X509 Certificate Request (CSR):

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

req = OpenSSL.crypto.X509Req()
req.get_subject().CN = "node1.example.com"
req.set_pubkey(key)
req.sign(key, "sha1")

# Write private key
print OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)

# Write request
print OpenSSL.crypto.dump_certificate_request(OpenSSL.crypto.FILETYPE_PEM, req)

X509 certificate from Certificate Signing Request

The following code sample shows how to create an X509 certificate from a Certificate Signing Request and sign it with a CA:

ca_cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
                                          "ca.pem")
ca_key = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM,
                                        "ca.pem")
req = OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM,
                                              open("req.csr").read())

cert = OpenSSL.crypto.X509()
cert.set_subject(req.get_subject())
cert.set_serial_number(1)
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(24 * 60 * 60)
cert.set_issuer(ca_cert.get_subject())
cert.set_pubkey(req.get_pubkey())
cert.sign(ca_key, "sha1")

print OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)

Verify whether X509 certificate matches private key

The code sample below shows how to check whether a certificate matches with a certain private key. OpenSSL has a function for this, X509_check_private_key, but pyOpenSSL provides no access to it.

ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_METHOD)
ctx.use_privatekey(key)
ctx.use_certificate(cert)
try:
  ctx.check_privatekey()
except OpenSSL.SSL.Error:
  print "Incorrect key"
else:
  print "Key matches certificate"