Created
May 21, 2024 19:04
-
-
Save Magisus/231c9799da83241e9417f3fe9d01b871 to your computer and use it in GitHub Desktop.
Generate a CA certificate chain
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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