Skip to content

Instantly share code, notes, and snippets.

@Magisus
Created May 21, 2024 19:04
Show Gist options
  • Save Magisus/231c9799da83241e9417f3fe9d01b871 to your computer and use it in GitHub Desktop.
Save Magisus/231c9799da83241e9417f3fe9d01b871 to your computer and use it in GitHub Desktop.
Generate a CA certificate chain
require 'openssl'
require 'resolv'
# The minimum recommended length is 2048, 4096 is more future-proof
PRIVATE_KEY_LENGTH = 2048
# Used for cert expiration periods. 15 years is chosen arbitrarily,
# long enough that users won't have to deal with refreshing for
# a while. The `regen_certificates` plan can be used to refresh
# certs if needed.
FIFTEEN_YEARS_SECONDS = 15 * 365 * 24 * 60 * 60
# Indicates that a certificate can be used as an SSL server certificate
# http://oidref.com/1.3.6.1.5.5.7.3.1
SSL_SERVER_CERT_OID = "1.3.6.1.5.5.7.3.1"
CA_EXTENSIONS = [
# Marks the cert as a certificate authority, rather than
# an end entity certificate, used for identifying a server
["basicConstraints", "CA:TRUE", true],
# Defines the purpose of the public key contained in the cert,
# in this case signing other certs and CRLs
["keyUsage", "keyCertSign, cRLSign", true],
# Used as an ID for this cert
["subjectKeyIdentifier", "hash", false],
# Used to identify the cert used to sign this cert
["authorityKeyIdentifier", "keyid:always", false]
]
SERVER_EXTENSIONS = [
# Defines the purpose of the public key contained in the cert,
# in this case encryption and signing things (not other certs)
["keyUsage", "digitalSignature,keyEncipherment", true],
# Used as an ID for this cert
["subjectKeyIdentifier", "hash", false],
# Used to identify the cert used to sign this cert.
# Should match the subjectKeyIdentifier of the CA cert
["authorityKeyIdentifier", "keyid:always", false],
]
# Recommended signing algorithm as of 2024
DEFAULT_SIGNING_DIGEST = OpenSSL::Digest::SHA256.new
def create_private_key
OpenSSL::PKey::RSA.new(PRIVATE_KEY_LENGTH)
end
def create_self_signed_ca(key, name)
cert = OpenSSL::X509::Certificate.new
cert.public_key = key.public_key
cert.subject = OpenSSL::X509::Name.parse(name)
cert.issuer = cert.subject
cert.version = 2
cert.serial = rand(2**128)
not_before = just_now
cert.not_before = not_before
cert.not_after = not_before + FIFTEEN_YEARS_SECONDS
ext_factory = extension_factory_for(cert, cert)
CA_EXTENSIONS.each do |ext|
extension = ext_factory.create_extension(*ext)
cert.add_extension(extension)
end
cert.sign(key, DEFAULT_SIGNING_DIGEST)
cert
end
def create_intermediate_ca(key, name, ca_key, ca_cert)
csr = OpenSSL::X509::Request.new
csr.public_key = key.public_key
csr.subject = OpenSSL::X509::Name.parse(name)
csr.version = 2
csr.sign(key, DEFAULT_SIGNING_DIGEST)
cert = OpenSSL::X509::Certificate.new
cert.public_key = csr.public_key
cert.subject = csr.subject
cert.issuer = ca_cert.subject
cert.version = 2
cert.serial = rand(2**128)
not_before = just_now
cert.not_before = not_before
cert.not_after = not_before + FIFTEEN_YEARS_SECONDS
ext_factory = extension_factory_for(ca_cert, cert)
CA_EXTENSIONS.each do |ext|
extension = ext_factory.create_extension(*ext)
cert.add_extension(extension)
end
cert.sign(ca_key, DEFAULT_SIGNING_DIGEST)
cert
end
def create_csr(key, name)
csr = OpenSSL::X509::Request.new
csr.public_key = key.public_key
csr.subject = OpenSSL::X509::Name.parse(name)
csr.version = 2
csr.sign(key, DEFAULT_SIGNING_DIGEST)
csr
end
def sign(hostname, ca_key, ca_cert, csr)
cert = OpenSSL::X509::Certificate.new
cert.public_key = csr.public_key
cert.subject = csr.subject
cert.issuer = ca_cert.subject
cert.version = 2
cert.serial = rand(2**128)
not_before = just_now
cert.not_before = not_before
cert.not_after = not_before + FIFTEEN_YEARS_SECONDS
ext_factory = extension_factory_for(ca_cert, cert)
SERVER_EXTENSIONS.each do |ext|
extension = ext_factory.create_extension(*ext)
cert.add_extension(extension)
end
if (hostname =~ Resolv::IPv4::Regex) || (hostname =~ Resolv::IPv6::Regex)
type_string = 'IP'
else
type_string = 'DNS'
end
alt_names_ext = ext_factory.create_extension("subjectAltName", "#{type_string}:#{hostname}", false)
cert.add_extension(alt_names_ext)
cert.sign(ca_key, DEFAULT_SIGNING_DIGEST)
cert
end
# Returns a Time object for one minute ago,
# to give some tolerance for clock skew
def just_now
Time.now - 60
end
def create_crl_for(cert, key)
crl = OpenSSL::X509::CRL.new
crl.version = 1
crl.issuer = cert.subject
ef = extension_factory_for(cert)
crl.add_extension(ef.create_extension(["authorityKeyIdentifier", "keyid:always", false]))
crl.add_extension(OpenSSL::X509::Extension.new("crlNumber", OpenSSL::ASN1::Integer(0)))
crl.last_update = just_now
crl.next_update = just_now + FIFTEEN_YEARS_SECONDS
crl.sign(key, DEFAULT_SIGNING_DIGEST)
crl
end
def extension_factory_for(ca, cert = nil)
ef = OpenSSL::X509::ExtensionFactory.new
ef.issuer_certificate = ca
ef.subject_certificate = cert if cert
ef
end
def generate(hostname)
ca_key = create_private_key
ca_cert = create_self_signed_ca(ca_key, "/CN=mitmproxy CA: #{hostname}")
int_key = create_private_key
int_cert = create_intermediate_ca(int_key, "/CN=mitmproxy Intermediate CA", ca_key, ca_cert)
File.write('private_key.pem', int_key)
File.write('proxy_certs.pem', "#{int_cert}#{ca_cert}")
end
generate('mitmproxy')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment