Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
287 changes: 287 additions & 0 deletions test-resources/regen_certs.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
require "fileutils"
require 'openssl'
require 'ostruct'
require 'tmpdir'
require 'securerandom'

require 'puppetserver/ca/action/generate'
require 'puppetserver/ca/action/setup'
require 'puppetserver/ca/host'
require 'puppetserver/ca/logger'
require 'puppetserver/ca/utils/file_system'

module PuppetserverSpec
module Ca
class Pki
include Puppetserver::Ca::Utils
attr :ca_cert, :ca_crl, :ca_key, :root_cert, :root_crl, :root_key, :settings

SETTINGS_TEMPLATE = {
confdir: '%<confdir>s',
ssldir: '%<confdir>s/ssl',
cadir: '%<confdir>s/ca',
certdir: '%<confdir>s/ssl/certs',
privatekeydir: '%<confdir>s/ssl/private_keys',
publickeydir: '%<confdir>s/ssl/public_keys',
hostpubkey: '%<confdir>s/ssl/public_keys/localhost.pem',
hostprivkey: '%<confdir>s/ssl/private_keys/localhost.pem',
hostcert: '%<confdir>s/ssl/certs/localhost.pem',
hostcrl: '%<confdir>s/ssl/crl.pem',
localcacert: '%<confdir>s/ssl/certs/ca.pem',
csrdir: '%<confdir>s/ca/requests',
signeddir: '%<confdir>s/ca/signed',
cakey: '%<confdir>s/ca/ca_key.pem',
capub: '%<confdir>s/ca/ca_pub.pem',
cacert: '%<confdir>s/ca/ca_crt.pem',
cacrl: '%<confdir>s/ca/ca_crl.pem',
serial: '%<confdir>s/ca/serial',
rootkey: '%<confdir>s/ca/root_key.pem',
cert_inventory: '%<confdir>s/ca/inventory.txt',
keylength: 2048,
certname: 'localhost',
ca_name: 'Puppet CA: localhost',
root_ca_name: 'Puppet Root CA: %<root_ca_name_rand>s',
ca_ttl: 157_680_000,
subject_alt_names: '',
csr_attributes: ''
}.freeze

def initialize(settings = {}, intermediate_cert: true)
settings_vars = {
confdir: (Dir.mktmpdir 'puppetca-', ENV['TMPDIR'] || '/tmp'),
Comment thread
nmburgan marked this conversation as resolved.
root_ca_name_rand: SecureRandom.hex(7)
}
@settings = SETTINGS_TEMPLATE.merge(settings).transform_values { |v| (v.is_a? String) ? v % settings_vars : v }

signer = SigningDigest.new
@digest = signer.digest
@logger = Puppetserver::Ca::Logger.new(:warning, $stdout, $stderr)
@ca = Puppetserver::Ca::LocalCertificateAuthority.new(@digest, @settings)

@root_key, @root_cert, @root_crl = @ca.create_root_cert

if intermediate_cert
@ca.create_intermediate_cert(@root_key, @root_cert)
ca_cert_setting = [@ca.cert, @root_cert]
ca_crl_setting = [@ca.crl, @root_crl]
else
ca_ssl = OpenStruct.new(
{
cert: root_cert,
certs: root_cert,
crl: root_crl,
crls: root_crl,
key: root_key
}
)
ca_cert_setting = @root_cert
ca_crl_setting = @root_crl
@ca.load_ssl_components(ca_ssl)
end

@ca_crl = @ca.crl
@ca_cert = @ca.cert
@ca_key = @ca.key

FileSystem.ensure_dirs([@settings[:ssldir],
@settings[:cadir],
@settings[:certdir],
@settings[:privatekeydir],
@settings[:publickeydir],
@settings[:signeddir]])

@ca.update_serial_file(2)
server_key, server_cert = @ca.create_server_cert
inventory = @ca.inventory_entry(@ca.cert) + "\n" + @ca.inventory_entry(server_cert)

[
[@settings[:cacert], ca_cert_setting],
[@settings[:cacrl], ca_crl_setting],
[@settings[:cadir] + '/infra_crl.pem', ca_crl_setting],
[@settings[:hostcert], server_cert],
[@settings[:localcacert], ca_cert_setting],
[@settings[:hostcrl], ca_crl_setting],
[@settings[:hostpubkey], server_key.public_key],
[@settings[:capub], @ca.key.public_key],
[@settings[:cert_inventory], inventory],
[@settings[:cadir] + '/infra_inventory.txt', ''],
[@settings[:cadir] + '/infra_serials', ''],
[File.join(@settings[:signeddir], "#{@settings[:certname]}.pem"), server_cert],
[@settings[:hostprivkey], server_key],
[@settings[:rootkey], @root_key],
[@settings[:cakey], @ca.key]
].each do |location, content|
FileSystem.write_file(location, content, 0644)
end

@ca.update_serial_file(server_cert.serial + 1)
end

def generate_cert(certnames, alt_names = [])
certnames = certnames.is_a?(Array) ? certnames : [certnames]

generate = Puppetserver::Ca::Action::Generate.new(@logger)
generate.generate_authorized_certs(certnames, alt_names, @settings, @digest)

certnames.each do |certname|
add_inventory_cert("#{@settings[:signeddir]}/#{certname}.pem")
end
end

def add_inventory_cert(certfile)
cert = OpenSSL::X509::Certificate.new(File.read(certfile))
File.open(@settings[:cert_inventory], 'a') do |f|
f.puts(@ca.inventory_entry(cert))
end
end

def generate_key_csr(certname, alt_names = [])
generate = Puppetserver::Ca::Action::Generate.new(@logger)
_key, csr = generate.generate_key_csr(certname, @settings, @digest, alt_names)

Dir.mkdir @settings[:csrdir] unless Dir.exist? @settings[:csrdir]
generate.save_file(csr, certname, @settings[:csrdir], 'Certificate request')
end

def revoke_cert(certname)
crl = OpenSSL::X509::CRL.new(File.read(@settings[:cacrl]))
cert = OpenSSL::X509::Certificate.new(File.read("#{@settings[:certdir]}/#{certname}.pem"))

revocation = OpenSSL::X509::Revoked.new
revocation.serial = cert.serial
revocation.time = Time.now
crl.add_revoked(revocation)

ca_key = OpenSSL::PKey::RSA.new(File.read(@settings[:cakey]))
crl.sign(ca_key, @digest)

File.write(@settings[:cacrl], crl.to_pem)
end

def create_intermediate_cert(certname, ca_key = @root_key, ca_cert = @root_cert)
host = Puppetserver::Ca::Host.new(@digest)

key = host.create_private_key(@settings[:keylength])
int_csr = host.create_csr(name: certname, key: key)
cert = @ca.sign_intermediate(ca_key, ca_cert, int_csr)

[cert, key]
end

def create_crl(cert: @ca.cert, key: @ca.key, serial: 0, deltaserial: nil, akid: true, last_update: just_now, next_update: valid_until)
crl = OpenSSL::X509::CRL.new
crl.issuer = cert.subject
crl.version = 1

ef = @ca.extension_factory_for(cert)
crl.add_extension(ef.create_extension(['authorityKeyIdentifier', 'keyid:always', false])) if akid
crl.add_extension(OpenSSL::X509::Extension.new('crlNumber', OpenSSL::ASN1::Integer(serial)))
crl.add_extension(OpenSSL::X509::Extension.new('2.5.29.27', OpenSSL::ASN1::Integer(deltaserial), true)) \
unless deltaserial.nil?

crl.last_update = last_update
crl.next_update = next_update
crl.sign(key, @digest)

crl
end

def create_root_crl(serial, deltaserial = nil, akid: true)
create_crl(cert: @root_cert, key: @root_key, serial: serial, deltaserial: deltaserial, akid: akid)
end

def just_now
Time.now - 1
end

def valid_until
Time.now + @settings[:ca_ttl]
end

def cleanup
FileUtils.rm_rf @settings[:confdir]
end
end
end
end

def regen_crl_tests_pki
puts 'Regenerating PKI for puppetlabs.ssl-utils.core-test/validate-cert-chain-and-crls-test ...'

settings = {
'root_ca_name': 'Root CA',
'ca_name': 'Intermediate CA 1'
}

pki = PuppetserverSpec::Ca::Pki.new(settings)
ica2_cert, ica2_key = pki.create_intermediate_cert('Intermediate CA 2', pki.ca_key, pki.ca_cert)
ica3_cert, ica3_key = pki.create_intermediate_cert('Intermediate CA 3', pki.ca_key, pki.ca_cert)
root_crl = pki.create_root_crl(0)
ica2_crl = pki.create_crl(cert: ica2_cert, key: ica2_key)
ica3_crl = pki.create_crl(cert: ica3_cert, key: ica3_key)
unrelated_pki = PuppetserverSpec::Ca::Pki.new(settings)

dest_dir = "#{__dir__}/puppetlabs/ssl_utils/examples/ssl/crl_validation"
File.open("#{dest_dir}/1-cert-chain.pem", 'w') do |f|
f.puts(pki.root_cert)
end
File.open("#{dest_dir}/1-crl-chain.pem", 'w') do |f|
f.puts(pki.create_root_crl(0))
end
File.open("#{dest_dir}/2-crl-chain.pem", 'w') do |f|
f.puts(pki.ca_crl, ica2_crl)
end
File.open("#{dest_dir}/3-cert-chain.pem", 'w') do |f|
f.puts(ica2_cert, pki.ca_cert, pki.root_cert)
end
File.open("#{dest_dir}/3-crl-chain.pem", 'w') do |f|
f.puts(root_crl, pki.ca_crl, ica2_crl)
end
File.open("#{dest_dir}/4-crl-chain.pem", 'w') do |f|
f.puts(root_crl, pki.ca_crl, ica3_crl, ica2_crl)
end
File.open("#{dest_dir}/cert-with-expired-crl.pem", 'w') do |f|
f.puts(pki.root_cert)
end
File.open("#{dest_dir}/expired-crl.pem", 'w') do |f|
f.puts(pki.create_crl(cert: pki.root_cert,
key: pki.root_key,
last_update: Time.now - pki.settings[:ca_ttl] * 2,
next_update: Time.now - pki.settings[:ca_ttl]))
end
File.open("#{dest_dir}/cert-with-crl-bad-sig.pem", 'w') do |f|
f.puts(pki.root_cert)
end
File.open("#{dest_dir}/crl-with-bad-signature.pem", 'w') do |f|
f.puts(pki.create_crl(cert: unrelated_pki.root_cert,
key: unrelated_pki.root_key))
end
File.open("#{dest_dir}/cert-with-not-valid-crl.pem", 'w') do |f|
f.puts(pki.root_cert)
end
File.open("#{dest_dir}/not-yet-valid-crl.pem", 'w') do |f|
f.puts(pki.create_crl(cert: pki.root_cert,
key: pki.root_key,
last_update: Time.now + pki.settings[:ca_ttl]))
end
File.open("#{dest_dir}/cert-chain-with-revoked-cert.pem", 'w') do |f|
f.puts(ica2_cert, pki.ca_cert, pki.root_cert)
end
File.open("#{dest_dir}/crl-chain-with-cert-revoked.pem", 'w') do |f|

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's a subtle bug here, but I'm not 100% sure. When the root CA is generated via Pki.new, it's creating a root CA and also an intermediate CA off of it automatically (let's call this ica1). This attempts to revoke ica2_cert.serial on the Root CA's CRL via root_crl.add_revoked, but ica2_cert is issued by ica1, so that entry can't revoke it. I think it works today only because the openvoxserver-ca gem's sign_intermediate hardcodes cert.serial = 2 for every intermediate, so the entry accidentally revokes Intermediate CA 1 I think? And I think the underlying test does a weak check that a cert is revoked, but not which cert.

I'm fine merging this as-is since we're just upstreaming your script, but might be worth fixing up at some point (if I'm thinking about this correctly).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd lean towards merging this bit as-is and perhaps revisiting the assumptions those tests are supposed to be making, and then adjusting the openvoxserver-ca gem and/or regen_cert.rb accordingly.

It does seem strange to hardcode the serial in this way, and I'm really not sure if it was intended this way or it's a really terrible bug meaning you can't ever revoke an intermediate cert and generate a new one that isn't revoked at birth... 😅

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's the latter 😬 But yeah, I'm fine merging this as-is for now.

revocation = OpenSSL::X509::Revoked.new
revocation.serial = ica2_cert.serial
revocation.time = Time.now
root_crl.add_revoked(revocation)
signer = Puppetserver::Ca::Utils::SigningDigest.new
root_crl.sign(pki.root_key, signer.digest)
f.puts(root_crl, pki.ca_crl, ica2_crl)
end
File.open("#{dest_dir}/other-3-cert-chain.pem", 'w') do |f|
f.puts(ica2_cert, pki.ca_cert, pki.root_cert)
end

pki.cleanup
unrelated_pki.cleanup
end

regen_crl_tests_pki