Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port certificates generating command from secure-forward #1818

Merged
merged 8 commits into from
Jan 17, 2018
Merged
Show file tree
Hide file tree
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
6 changes: 6 additions & 0 deletions bin/fluent-ca-generate
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env ruby

$LOAD_PATH.unshift(File.join(__dir__, 'lib'))
require 'fluent/command/ca_generate'

Fluent::CaGenerate.new.call
179 changes: 179 additions & 0 deletions lib/fluent/command/ca_generate.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
require 'openssl'
require 'optparse'
require 'fileutils'

module Fluent
class CaGenerate
DEFAULT_OPTIONS = {
private_key_length: 2048,
cert_country: 'US',
cert_state: 'CA',
cert_locality: 'Mountain View',
cert_common_name: 'Fluentd Forward CA',
}
HELP_TEXT = <<HELP
Usage: fluent-ca-genrate DIR_PATH PRIVATE_KEY_PASSPHRASE [--country COUNTRY] [--state STATE] [--locality LOCALITY] [--common-name COMMON_NAME]
HELP

def initialize(argv = ARGV)
@argv = argv
@options = {}
@opt_parser = OptionParser.new
configure_option_parser
@options.merge!(DEFAULT_OPTIONS)
parse_options!
end

def usage(msg = nil)
puts HELP_TEXT
puts "Error: #{msg}" if msg
exit 1
end

def call
ca_dir, passphrase, = @argv[0..1]

unless ca_dir && passphrase
puts "#{HELP_TEXT}"
puts ''
exit 1
end

FileUtils.mkdir_p(ca_dir)

cert, key = Fluent::CaGenerate.generate_ca_pair(@options)

key_data = key.export(OpenSSL::Cipher.new('aes256'), passphrase)
File.open(File.join(ca_dir, 'ca_key.pem'), 'w') do |file|
file.write key_data
end
File.open(File.join(ca_dir, 'ca_cert.pem'), 'w') do |file|
file.write cert.to_pem
end

puts "successfully generated: ca_key.pem, ca_cert.pem"
puts "copy and use ca_cert.pem to client(out_forward)"
end

def self.certificates_from_file(path)
data = File.read(path)
pattern = Regexp.compile('-+BEGIN CERTIFICATE-+\n(?:[^-]*\n)+-+END CERTIFICATE-+\n', Regexp::MULTILINE)
list = []
data.scan(pattern){|match| list << OpenSSL::X509::Certificate.new(match)}
list
end

def self.generate_ca_pair(opts={})
key = OpenSSL::PKey::RSA.generate(opts[:private_key_length])

issuer = subject = OpenSSL::X509::Name.new
subject.add_entry('C', opts[:cert_country])
subject.add_entry('ST', opts[:cert_state])
subject.add_entry('L', opts[:cert_locality])
subject.add_entry('CN', opts[:cert_common_name])

digest = OpenSSL::Digest::SHA256.new

cert = OpenSSL::X509::Certificate.new
cert.not_before = Time.at(0)
cert.not_after = Time.now + 5 * 365 * 86400 # 5 years after
cert.public_key = key
cert.serial = 1
cert.issuer = issuer
cert.subject = subject
cert.add_extension OpenSSL::X509::Extension.new('basicConstraints', OpenSSL::ASN1.Sequence([OpenSSL::ASN1::Boolean(true)]))
cert.sign(key, digest)

return cert, key
end

def self.generate_server_pair(opts={})
key = OpenSSL::PKey::RSA.generate(opts[:private_key_length])

ca_key = OpenSSL::PKey::RSA.new(File.read(opts[:ca_key_path]), opts[:ca_key_passphrase])
ca_cert = OpenSSL::X509::Certificate.new(File.read(opts[:ca_cert_path]))
issuer = ca_cert.issuer

subject = OpenSSL::X509::Name.new
subject.add_entry('C', opts[:country])
subject.add_entry('ST', opts[:state])
subject.add_entry('L', opts[:locality])
subject.add_entry('CN', opts[:common_name])

digest = OpenSSL::Digest::SHA256.new

cert = OpenSSL::X509::Certificate.new
cert.not_before = Time.at(0)
cert.not_after = Time.now + 5 * 365 * 86400 # 5 years after
cert.public_key = key
cert.serial = 2
cert.issuer = issuer
cert.subject = subject

cert.add_extension OpenSSL::X509::Extension.new('basicConstraints', OpenSSL::ASN1.Sequence([OpenSSL::ASN1::Boolean(false)]))
cert.add_extension OpenSSL::X509::Extension.new('nsCertType', 'server')

cert.sign ca_key, digest

return cert, key
end

def self.generate_self_signed_server_pair(opts={})
key = OpenSSL::PKey::RSA.generate(opts[:private_key_length])

issuer = subject = OpenSSL::X509::Name.new
subject.add_entry('C', opts[:country])
subject.add_entry('ST', opts[:state])
subject.add_entry('L', opts[:locality])
subject.add_entry('CN', opts[:common_name])

digest = OpenSSL::Digest::SHA256.new

cert = OpenSSL::X509::Certificate.new
cert.not_before = Time.at(0)
cert.not_after = Time.now + 5 * 365 * 86400 # 5 years after
cert.public_key = key
cert.serial = 1
cert.issuer = issuer
cert.subject = subject
cert.sign(key, digest)

return cert, key
end

private

def configure_option_parser
@opt_parser.banner = HELP_TEXT

@opt_parser.on('--key-length [KEY_LENGTH]',
"configure key length. (default: #{DEFAULT_OPTIONS[:private_key_length]})") do |v|
@options[:private_key_length] = v.to_i
end

@opt_parser.on('--country [COUNTRY]',
"configure country. (default: #{DEFAULT_OPTIONS[:cert_country]})") do |v|
@options[:cert_country] = v.upcase
end

@opt_parser.on('--state [STATE]',
"configure state. (default: #{DEFAULT_OPTIONS[:cert_state]})") do |v|
@options[:cert_state] = v
end

@opt_parser.on('--locality [LOCALITY]',
"configure locality. (default: #{DEFAULT_OPTIONS[:cert_locality]})") do |v|
@options[:cert_locality] = v
end

@opt_parser.on('--common-name [COMMON_NAME]',
"configure common name (default: #{DEFAULT_OPTIONS[:cert_common_name]})") do |v|
@options[:cert_common_name] = v
end
end

def parse_options!
@opt_parser.parse!(@argv)
end
end
end
70 changes: 70 additions & 0 deletions test/command/test_ca_generate.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
require_relative '../helper'

require 'yajl'
require 'flexmock/test_unit'
require 'tmpdir'

require 'fluent/command/ca_generate'
require 'fluent/event'

class TestFluentCaGenerate < ::Test::Unit::TestCase
def test_generate_ca_pair
cert, key = Fluent::CaGenerate.generate_ca_pair(Fluent::CaGenerate::DEFAULT_OPTIONS)
assert_equal(OpenSSL::X509::Certificate, cert.class)
assert_true(key.private?)
end

def test_ca_generate
dumped_output = capture_stdout do
Dir.mktmpdir do |dir|
Fluent::CaGenerate.new([dir, "fluentd"]).call
assert_true(File.exist?(File.join(dir, "ca_key.pem")))
assert_true(File.exist?(File.join(dir, "ca_cert.pem")))
end
end
expected = <<TEXT
successfully generated: ca_key.pem, ca_cert.pem
copy and use ca_cert.pem to client(out_forward)
TEXT
assert_equal(expected, dumped_output)
end

sub_test_case "configure options" do
test "should respond multiple options" do
dumped_output = capture_stdout do
Dir.mktmpdir do |dir|
Fluent::CaGenerate.new([dir, "fluentd",
"--country", "JP", "--key-length", "4096",
"--state", "Tokyo", "--locality", "Chiyoda-ku",
"--common-name", "Forward CA"]).call
assert_true(File.exist?(File.join(dir, "ca_key.pem")))
assert_true(File.exist?(File.join(dir, "ca_cert.pem")))
end
end
expected = <<TEXT
successfully generated: ca_key.pem, ca_cert.pem
copy and use ca_cert.pem to client(out_forward)
TEXT
assert_equal(expected, dumped_output)
end

test "invalid options" do
Dir.mktmpdir do |dir|
assert_raise(OptionParser::InvalidOption) do
Fluent::CaGenerate.new([dir, "fluentd",
"--invalid"]).call
end
assert_false(File.exist?(File.join(dir, "ca_key.pem")))
assert_false(File.exist?(File.join(dir, "ca_cert.pem")))
end
end

test "empty options" do
assert_raise(SystemExit) do
capture_stdout do
Fluent::CaGenerate.new([]).call
end
end
end
end
end