From cc73f9b71d7ad393edad79c83e923a3ed30c94be Mon Sep 17 00:00:00 2001 From: Hiroshi Hatake Date: Fri, 12 Jan 2018 14:06:29 +0900 Subject: [PATCH 1/8] Add fluent-ca-generate command for generating CA certificates --- bin/fluent-ca-generate | 35 ++++++++++++ lib/fluent/command/ca_generate.rb | 91 +++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100755 bin/fluent-ca-generate create mode 100644 lib/fluent/command/ca_generate.rb diff --git a/bin/fluent-ca-generate b/bin/fluent-ca-generate new file mode 100755 index 0000000000..105c932c33 --- /dev/null +++ b/bin/fluent-ca-generate @@ -0,0 +1,35 @@ +#!/usr/bin/env ruby + +$LOAD_PATH.unshift(File.join(__dir__, 'lib')) +require 'fileutils' +require 'fluent/command/ca_generate' + +ca_dir, passphrase = ARGV + +unless ca_dir && passphrase + puts 'USAGE: fluent-ca-generate DIR_PATH PRIVATE_KEY_PASSPHRASE' + puts '' + exit 0 +end + +FileUtils.mkdir_p(ca_dir) + +opt = { + private_key_length: 2048, + cert_country: 'US', + cert_state: 'CA', + cert_locality: 'Mountain View', + cert_common_name: 'Fluentd Forward CA', +} +cert, key = Fluent::CaGenerate.generate_ca_pair(opt) + +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)" diff --git a/lib/fluent/command/ca_generate.rb b/lib/fluent/command/ca_generate.rb new file mode 100644 index 0000000000..bf0271ef7b --- /dev/null +++ b/lib/fluent/command/ca_generate.rb @@ -0,0 +1,91 @@ +require 'openssl' + +module Fluent + module CaGenerate + 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 + end +end From b7eaa0dd3fc6cb4dca9451ed1403f8f492459c1e Mon Sep 17 00:00:00 2001 From: Hiroshi Hatake Date: Mon, 15 Jan 2018 11:51:45 +0900 Subject: [PATCH 2/8] Add generate_ca_pair unit-test --- test/command/test_ca_generate.rb | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 test/command/test_ca_generate.rb diff --git a/test/command/test_ca_generate.rb b/test/command/test_ca_generate.rb new file mode 100644 index 0000000000..5ec65801cb --- /dev/null +++ b/test/command/test_ca_generate.rb @@ -0,0 +1,23 @@ +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 + opt = { + private_key_length: 2048, + cert_country: 'US', + cert_state: 'CA', + cert_locality: 'Mountain View', + cert_common_name: 'Fluentd Forward CA', + } + cert, key = Fluent::CaGenerate.generate_ca_pair(opt) + assert_equal(OpenSSL::X509::Certificate, cert.class) + assert_true(key.private?) + end +end From a8c4f7303e248b17865b2998a3e9fa3f5b22192e Mon Sep 17 00:00:00 2001 From: Hiroshi Hatake Date: Mon, 15 Jan 2018 13:09:04 +0900 Subject: [PATCH 3/8] Migrate Fluentd's internal command style --- bin/fluent-ca-generate | 30 +----------------------- lib/fluent/command/ca_generate.rb | 38 ++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 30 deletions(-) diff --git a/bin/fluent-ca-generate b/bin/fluent-ca-generate index 105c932c33..c2d80c546f 100755 --- a/bin/fluent-ca-generate +++ b/bin/fluent-ca-generate @@ -4,32 +4,4 @@ $LOAD_PATH.unshift(File.join(__dir__, 'lib')) require 'fileutils' require 'fluent/command/ca_generate' -ca_dir, passphrase = ARGV - -unless ca_dir && passphrase - puts 'USAGE: fluent-ca-generate DIR_PATH PRIVATE_KEY_PASSPHRASE' - puts '' - exit 0 -end - -FileUtils.mkdir_p(ca_dir) - -opt = { - private_key_length: 2048, - cert_country: 'US', - cert_state: 'CA', - cert_locality: 'Mountain View', - cert_common_name: 'Fluentd Forward CA', -} -cert, key = Fluent::CaGenerate.generate_ca_pair(opt) - -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)" +Fluent::CaGenerate.new.call diff --git a/lib/fluent/command/ca_generate.rb b/lib/fluent/command/ca_generate.rb index bf0271ef7b..d4cd1d5cea 100644 --- a/lib/fluent/command/ca_generate.rb +++ b/lib/fluent/command/ca_generate.rb @@ -1,7 +1,43 @@ require 'openssl' module Fluent - module CaGenerate + class CaGenerate + def initialize(argv = ARGV) + @argv = argv + end + + def call + ca_dir, passphrase = @argv + + unless ca_dir && passphrase + puts 'USAGE: fluent-ca-generate DIR_PATH PRIVATE_KEY_PASSPHRASE' + puts '' + exit 0 + end + + FileUtils.mkdir_p(ca_dir) + + opt = { + private_key_length: 2048, + cert_country: 'US', + cert_state: 'CA', + cert_locality: 'Mountain View', + cert_common_name: 'Fluentd Forward CA', + } + cert, key = Fluent::CaGenerate.generate_ca_pair(opt) + + 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) From 31f2f1c6345da477eb86bdc46fb9af44407db8c5 Mon Sep 17 00:00:00 2001 From: Hiroshi Hatake Date: Mon, 15 Jan 2018 13:24:20 +0900 Subject: [PATCH 4/8] Add ca_generate internal command test --- test/command/test_ca_generate.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/command/test_ca_generate.rb b/test/command/test_ca_generate.rb index 5ec65801cb..a247376182 100644 --- a/test/command/test_ca_generate.rb +++ b/test/command/test_ca_generate.rb @@ -20,4 +20,19 @@ def test_generate_ca_pair 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 = < Date: Tue, 16 Jan 2018 13:52:59 +0900 Subject: [PATCH 5/8] Add certificates configuration parameters --- lib/fluent/command/ca_generate.rb | 73 ++++++++++++++++++++++++++----- test/command/test_ca_generate.rb | 20 +++++++++ 2 files changed, 82 insertions(+), 11 deletions(-) diff --git a/lib/fluent/command/ca_generate.rb b/lib/fluent/command/ca_generate.rb index d4cd1d5cea..c805ba7483 100644 --- a/lib/fluent/command/ca_generate.rb +++ b/lib/fluent/command/ca_generate.rb @@ -1,30 +1,46 @@ require 'openssl' +require 'optparse' 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 = < Date: Tue, 16 Jan 2018 14:10:50 +0900 Subject: [PATCH 6/8] Use DEFAULT_OPTIONS constant in test case --- test/command/test_ca_generate.rb | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/test/command/test_ca_generate.rb b/test/command/test_ca_generate.rb index f47f737142..ee807294de 100644 --- a/test/command/test_ca_generate.rb +++ b/test/command/test_ca_generate.rb @@ -9,14 +9,7 @@ class TestFluentCaGenerate < ::Test::Unit::TestCase def test_generate_ca_pair - opt = { - private_key_length: 2048, - cert_country: 'US', - cert_state: 'CA', - cert_locality: 'Mountain View', - cert_common_name: 'Fluentd Forward CA', - } - cert, key = Fluent::CaGenerate.generate_ca_pair(opt) + cert, key = Fluent::CaGenerate.generate_ca_pair(Fluent::CaGenerate::DEFAULT_OPTIONS) assert_equal(OpenSSL::X509::Certificate, cert.class) assert_true(key.private?) end From 45721a901b919d8f17b8901f97e2cbdeef64a775 Mon Sep 17 00:00:00 2001 From: Hiroshi Hatake Date: Tue, 16 Jan 2018 14:15:34 +0900 Subject: [PATCH 7/8] Add failure test cases --- test/command/test_ca_generate.rb | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/command/test_ca_generate.rb b/test/command/test_ca_generate.rb index ee807294de..30fc9a1ec8 100644 --- a/test/command/test_ca_generate.rb +++ b/test/command/test_ca_generate.rb @@ -47,5 +47,24 @@ def test_ca_generate 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 From 3ce4c6e297cf7191c97b4d86a1ebf7fe1330fab0 Mon Sep 17 00:00:00 2001 From: Hiroshi Hatake Date: Tue, 16 Jan 2018 14:18:34 +0900 Subject: [PATCH 8/8] Move fileutils require directive into near needed place --- bin/fluent-ca-generate | 1 - lib/fluent/command/ca_generate.rb | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/fluent-ca-generate b/bin/fluent-ca-generate index c2d80c546f..6037c7fce3 100755 --- a/bin/fluent-ca-generate +++ b/bin/fluent-ca-generate @@ -1,7 +1,6 @@ #!/usr/bin/env ruby $LOAD_PATH.unshift(File.join(__dir__, 'lib')) -require 'fileutils' require 'fluent/command/ca_generate' Fluent::CaGenerate.new.call diff --git a/lib/fluent/command/ca_generate.rb b/lib/fluent/command/ca_generate.rb index c805ba7483..74b67afc3f 100644 --- a/lib/fluent/command/ca_generate.rb +++ b/lib/fluent/command/ca_generate.rb @@ -1,5 +1,6 @@ require 'openssl' require 'optparse' +require 'fileutils' module Fluent class CaGenerate