From 9783d7f21c4ddc43b845a6c551d954adaab57a7e Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Wed, 19 Feb 2020 04:44:31 +0000 Subject: [PATCH 1/6] config: remove deprecated methods Remove 4 deprecated methods. The following two methods have been marked as deprecated since 2003, by r4531 (ruby.git commit 78ff3833fb67c8005a9b851037e74b3eea940aa3). - OpenSSL::Config#value - OpenSSL::Config#section Other two methods are removed because the corresponding functions disappeared in OpenSSL 1.1.0. - OpenSSL::Config#add_value - OpenSSL::Config#[]= --- lib/openssl/config.rb | 90 ------------------------- test/openssl/test_config.rb | 128 +++++------------------------------- 2 files changed, 16 insertions(+), 202 deletions(-) diff --git a/lib/openssl/config.rb b/lib/openssl/config.rb index 9a0b78742..46e1711d2 100644 --- a/lib/openssl/config.rb +++ b/lib/openssl/config.rb @@ -297,50 +297,6 @@ def get_value(section, key) get_key_string(section, key) end - ## - # - # *Deprecated* - # - # Use #get_value instead - def value(arg1, arg2 = nil) # :nodoc: - warn('Config#value is deprecated; use Config#get_value') - if arg2.nil? - section, key = 'default', arg1 - else - section, key = arg1, arg2 - end - section ||= 'default' - section = 'default' if section.empty? - get_key_string(section, key) - end - - ## - # *Deprecated in v2.2.0*. This method will be removed in a future release. - # - # Set the target _key_ with a given _value_ under a specific _section_. - # - # Given the following configurating file being loaded: - # - # config = OpenSSL::Config.load('foo.cnf') - # #=> # - # puts config.to_s - # #=> [ default ] - # # foo=bar - # - # You can set the value of _foo_ under the _default_ section to a new - # value: - # - # config.add_value('default', 'foo', 'buzz') - # #=> "buzz" - # puts config.to_s - # #=> [ default ] - # # foo=buzz - # - def add_value(section, key, value) - check_modify - (@data[section] ||= {})[key] = value - end - ## # Get a specific _section_ from the current configuration # @@ -361,46 +317,6 @@ def [](section) @data[section] || {} end - ## - # Deprecated - # - # Use #[] instead - def section(name) # :nodoc: - warn('Config#section is deprecated; use Config#[]') - @data[name] || {} - end - - ## - # *Deprecated in v2.2.0*. This method will be removed in a future release. - # - # Sets a specific _section_ name with a Hash _pairs_. - # - # Given the following configuration being created: - # - # config = OpenSSL::Config.new - # #=> # - # config['default'] = {"foo"=>"bar","baz"=>"buz"} - # #=> {"foo"=>"bar", "baz"=>"buz"} - # puts config.to_s - # #=> [ default ] - # # foo=bar - # # baz=buz - # - # It's important to note that this will essentially merge any of the keys - # in _pairs_ with the existing _section_. For example: - # - # config['default'] - # #=> {"foo"=>"bar", "baz"=>"buz"} - # config['default'] = {"foo" => "changed"} - # #=> {"foo"=>"changed"} - # config['default'] - # #=> {"foo"=>"changed", "baz"=>"buz"} - # - def []=(section, pairs) - check_modify - set_section(section, pairs) - end - def set_section(section, pairs) # :nodoc: hash = @data[section] ||= {} pairs.each do |key, value| @@ -488,12 +404,6 @@ def initialize_copy(other) @data = other.data.dup end - def check_modify - warn "#{caller(2, 1)[0]}: warning: do not modify OpenSSL::Config; this " \ - "method is deprecated and will be removed in a future release." - raise TypeError.new("Insecure: can't modify OpenSSL config") if frozen? - end - def get_key_string(section, key) Config.get_key_string(@data, section, key) end diff --git a/test/openssl/test_config.rb b/test/openssl/test_config.rb index f65392c18..984a1150e 100644 --- a/test/openssl/test_config.rb +++ b/test/openssl/test_config.rb @@ -214,28 +214,6 @@ def test_get_value_ENV assert_equal(ENV[key], @it.get_value('ENV', key)) end - def test_value - # suppress deprecation warnings - EnvUtil.suppress_warning do - assert_equal('CA_default', @it.value('ca', 'default_ca')) - assert_equal(nil, @it.value('ca', 'no such key')) - assert_equal(nil, @it.value('no such section', 'no such key')) - assert_equal('.', @it.value('', 'HOME')) - assert_equal('.', @it.value(nil, 'HOME')) - assert_equal('.', @it.value('HOME')) - # fallback to 'default' ugly... - assert_equal('.', @it.value('unknown', 'HOME')) - end - end - - def test_value_ENV - EnvUtil.suppress_warning do - key = ENV.keys.first - assert_not_nil(key) # make sure we have at least one ENV var. - assert_equal(ENV[key], @it.value('ENV', key)) - end - end - def test_aref assert_equal({'HOME' => '.'}, @it['default']) assert_equal({'dir' => './demoCA', 'certs' => './certs'}, @it['CA_default']) @@ -243,65 +221,19 @@ def test_aref assert_equal({}, @it['']) end - def test_section - EnvUtil.suppress_warning do - assert_equal({'HOME' => '.'}, @it.section('default')) - assert_equal({'dir' => './demoCA', 'certs' => './certs'}, @it.section('CA_default')) - assert_equal({}, @it.section('no_such_section')) - assert_equal({}, @it.section('')) - end - end - def test_sections assert_equal(['CA_default', 'ca', 'default'], @it.sections.sort) - # OpenSSL::Config#[]= is deprecated - EnvUtil.suppress_warning do - @it['new_section'] = {'foo' => 'bar'} - assert_equal(['CA_default', 'ca', 'default', 'new_section'], @it.sections.sort) - @it['new_section'] = {} - assert_equal(['CA_default', 'ca', 'default', 'new_section'], @it.sections.sort) - end - end - - def test_add_value - # OpenSSL::Config#add_value is deprecated - EnvUtil.suppress_warning do - c = OpenSSL::Config.new - assert_equal("", c.to_s) - # add key - c.add_value('default', 'foo', 'bar') - assert_equal("[ default ]\nfoo=bar\n\n", c.to_s) - # add another key - c.add_value('default', 'baz', 'qux') - assert_equal('bar', c['default']['foo']) - assert_equal('qux', c['default']['baz']) - # update the value - c.add_value('default', 'baz', 'quxxx') - assert_equal('bar', c['default']['foo']) - assert_equal('quxxx', c['default']['baz']) - # add section and key - c.add_value('section', 'foo', 'bar') - assert_equal('bar', c['default']['foo']) - assert_equal('quxxx', c['default']['baz']) - assert_equal('bar', c['section']['foo']) - end - end - - def test_aset - # OpenSSL::Config#[]= is deprecated - EnvUtil.suppress_warning do - @it['foo'] = {'bar' => 'baz'} - assert_equal({'bar' => 'baz'}, @it['foo']) - @it['foo'] = {'bar' => 'qux', 'baz' => 'quxx'} - assert_equal({'bar' => 'qux', 'baz' => 'quxx'}, @it['foo']) - - # OpenSSL::Config is add only for now. - @it['foo'] = {'foo' => 'foo'} - assert_equal({'foo' => 'foo', 'bar' => 'qux', 'baz' => 'quxx'}, @it['foo']) - # you cannot override or remove any section and key. - @it['foo'] = {} - assert_equal({'foo' => 'foo', 'bar' => 'qux', 'baz' => 'quxx'}, @it['foo']) - end + Tempfile.create("openssl.cnf") { |f| + f.write File.read(@tmpfile.path) + f.puts "[ new_section ]" + f.puts "foo = bar" + f.puts "[ empty_section ]" + f.close + + c = OpenSSL::Config.new(f.path) + assert_equal(['CA_default', 'ca', 'default', 'empty_section', 'new_section'], + c.sections.sort) + } end def test_each @@ -323,40 +255,12 @@ def test_inspect assert_match(/#/, @it.inspect) end - def test_freeze - @it.freeze - - # Modifying OpenSSL::Config produces a warning - EnvUtil.suppress_warning do - bug = '[ruby-core:18377]' - # RuntimeError for 1.9, TypeError for 1.8 - e = assert_raise(TypeError, bug) do - @it['foo'] = [['key', 'wrong']] - end - assert_match(/can't modify/, e.message, bug) - end - end - def test_dup - assert(!@it.sections.empty?) - c = @it.dup - assert_equal(@it.sections.sort, c.sections.sort) - # OpenSSL::Config#[]= is deprecated - EnvUtil.suppress_warning do - @it['newsection'] = {'a' => 'b'} - assert_not_equal(@it.sections.sort, c.sections.sort) - end - end - - def test_clone - assert(!@it.sections.empty?) - c = @it.clone - assert_equal(@it.sections.sort, c.sections.sort) - # OpenSSL::Config#[]= is deprecated - EnvUtil.suppress_warning do - @it['newsection'] = {'a' => 'b'} - assert_not_equal(@it.sections.sort, c.sections.sort) - end + assert_equal(['CA_default', 'ca', 'default'], @it.sections.sort) + c1 = @it.dup + assert_equal(@it.sections.sort, c1.sections.sort) + c2 = @it.clone + assert_equal(@it.sections.sort, c2.sections.sort) end private From 9ce2ccf36d6bb06e4031d8773522d96ca0ed7971 Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Wed, 19 Feb 2020 05:05:41 +0000 Subject: [PATCH 2/6] test/openssl/test_config: add missing test case for Config.parse_config --- test/openssl/test_config.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/openssl/test_config.rb b/test/openssl/test_config.rb index 984a1150e..9a3a6a804 100644 --- a/test/openssl/test_config.rb +++ b/test/openssl/test_config.rb @@ -177,6 +177,12 @@ def test_s_load } end + def test_s_parse_config + ret = OpenSSL::Config.parse_config(@it.to_s) + assert_equal(@it.sections.sort, ret.keys.sort) + assert_equal(@it["default"], ret["default"]) + end + def test_initialize c = OpenSSL::Config.new assert_equal("", c.to_s) From 259e6fd2dcdf624627f95071460fdc17b98d4a39 Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Wed, 22 Apr 2020 21:46:39 +0900 Subject: [PATCH 3/6] test/openssl/test_config: fix non-deterministic test case Sort keys of a section before comparing. The ordering is not part of the API. This can cause a test failure if we use OpenSSL's C implementation. Fixes: 2ad65b5f673f ("config: support .include directive", 2018-08-16) --- test/openssl/test_config.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/openssl/test_config.rb b/test/openssl/test_config.rb index 9a3a6a804..a725add26 100644 --- a/test/openssl/test_config.rb +++ b/test/openssl/test_config.rb @@ -151,7 +151,7 @@ def test_s_parse_include # Include a file by relative path c1 = OpenSSL::Config.parse(include_file) assert_equal(["default", "sec-a", "sec-b", "sec-main"], c1.sections.sort) - assert_equal(["file-main", "file-a", "file-b"], c1["default"].keys) + assert_equal(["file-a", "file-b", "file-main"], c1["default"].keys.sort) assert_equal({"a" => "123"}, c1["sec-a"]) assert_equal({"b" => "123"}, c1["sec-b"]) assert_equal({"main" => "123", "key_outside_section" => "value_a"}, c1["sec-main"]) From b70817faec1ca45cebe533a02617e7ac04d5ecbc Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Sat, 22 Feb 2020 04:58:08 +0900 Subject: [PATCH 4/6] test/openssl/test_config: skip test_get_value_ENV on LibreSSL LibreSSL has removed the feature to map environment variables onto the "ENV" section. --- test/openssl/test_config.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/openssl/test_config.rb b/test/openssl/test_config.rb index a725add26..01be28164 100644 --- a/test/openssl/test_config.rb +++ b/test/openssl/test_config.rb @@ -215,6 +215,9 @@ def test_get_value end def test_get_value_ENV + # LibreSSL removed support for NCONF_get_string(conf, "ENV", str) + return if libressl? + key = ENV.keys.first assert_not_nil(key) # make sure we have at least one ENV var. assert_equal(ENV[key], @it.get_value('ENV', key)) From c891e0ea8951230c19919c6960d88b8d038395e9 Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Wed, 19 Feb 2020 05:06:09 +0000 Subject: [PATCH 5/6] config: revert to C implementation of OpenSSL::Config Revert OpenSSL::Config to using the OpenSSL API and remove our own parser implementation for the config file syntax. OpenSSL::Config now wraps a CONF object. Accessor methods deal with the object directly rather than Ruby-level internal state. This work is based on the old C code we used before 2010. --- ext/openssl/ossl_config.c | 453 ++++++++++++++++++++++++++++++++++-- ext/openssl/ossl_config.h | 11 +- lib/openssl.rb | 1 - lib/openssl/config.rb | 411 -------------------------------- test/openssl/test_config.rb | 10 +- 5 files changed, 441 insertions(+), 445 deletions(-) delete mode 100644 lib/openssl/config.rb diff --git a/ext/openssl/ossl_config.c b/ext/openssl/ossl_config.c index 28392e208..52d96e1ed 100644 --- a/ext/openssl/ossl_config.c +++ b/ext/openssl/ossl_config.c @@ -9,21 +9,46 @@ */ #include "ossl.h" +static VALUE cConfig, eConfigError; -/* - * Classes - */ -VALUE cConfig; -/* Document-class: OpenSSL::ConfigError - * - * General error for openssl library configuration files. Including formatting, - * parsing errors, etc. - */ -VALUE eConfigError; +static void +nconf_free(void *conf) +{ + NCONF_free(conf); +} -/* - * Public - */ +static const rb_data_type_t ossl_config_type = { + "OpenSSL/CONF", + { + 0, nconf_free, + }, + 0, 0, RUBY_TYPED_FREE_IMMEDIATELY, +}; + +static CONF * +GetConfig(VALUE obj) +{ + CONF *conf; + + TypedData_Get_Struct(obj, CONF, &ossl_config_type, conf); + if (!conf) + rb_raise(rb_eRuntimeError, "CONF is not initialized"); + return conf; +} + +static VALUE +config_s_alloc(VALUE klass) +{ + VALUE obj; + CONF *conf; + + obj = TypedData_Wrap_Struct(klass, &ossl_config_type, 0); + conf = NCONF_new(NULL); + if (!conf) + ossl_raise(eConfigError, "NCONF_new"); + RTYPEDDATA_DATA(obj) = conf; + return obj; +} /* * DupConfigPtr is a public C-level function for getting OpenSSL CONF struct @@ -60,30 +85,412 @@ DupConfigPtr(VALUE obj) return conf; } -/* Document-const: DEFAULT_CONFIG_FILE + +static void +config_load_bio(CONF *conf, BIO *bio) +{ + long eline = -1; + + if (!NCONF_load_bio(conf, bio, &eline)) { + BIO_free(bio); + if (eline <= 0) + ossl_raise(eConfigError, "wrong config format"); + else + ossl_raise(eConfigError, "error in line %d", eline); + } + BIO_free(bio); + + /* + * Clear the error queue even if it is parsed successfully. + * Particularly, when the .include directive refers to a non-existent file, + * it is only reported in the error queue. + */ + ossl_clear_error(); +} + +/* + * call-seq: + * Config.parse(string) -> OpenSSL::Config + * + * Parses a given _string_ as a blob that contains configuration for OpenSSL. + */ +static VALUE +config_s_parse(VALUE klass, VALUE str) +{ + VALUE obj = config_s_alloc(klass); + CONF *conf = GetConfig(obj); + BIO *bio; + + bio = ossl_obj2bio(&str); + config_load_bio(conf, bio); /* Consumes BIO */ + return obj; +} + +static VALUE config_get_sections(VALUE self); +static VALUE config_get_section(VALUE self, VALUE section); + +/* + * call-seq: + * Config.parse_config(io) -> hash + * + * Parses the configuration data read from _io_ and returns the whole content + * as a Hash. + */ +static VALUE +config_s_parse_config(VALUE klass, VALUE io) +{ + VALUE obj, sections, ret; + long i; + + obj = config_s_parse(klass, io); + sections = config_get_sections(obj); + ret = rb_hash_new(); + for (i = 0; i < RARRAY_LEN(sections); i++) { + VALUE section = rb_ary_entry(sections, i); + rb_hash_aset(ret, section, config_get_section(obj, section)); + } + return ret; +} + +/* + * call-seq: + * Config.new(filename) -> OpenSSL::Config + * + * Creates an instance of OpenSSL::Config from the content of the file + * specified by _filename_. + * + * This can be used in contexts like OpenSSL::X509::ExtensionFactory.config= + * + * This can raise IO exceptions based on the access, or availability of the + * file. A ConfigError exception may be raised depending on the validity of + * the data being configured. + */ +static VALUE +config_initialize(int argc, VALUE *argv, VALUE self) +{ + CONF *conf = GetConfig(self); + VALUE filename; + + /* 0-arguments call has no use-case, but is kept for compatibility */ + rb_scan_args(argc, argv, "01", &filename); + rb_check_frozen(self); + if (!NIL_P(filename)) { + BIO *bio = BIO_new_file(StringValueCStr(filename), "rb"); + if (!bio) + ossl_raise(eConfigError, "BIO_new_file"); + config_load_bio(conf, bio); /* Consumes BIO */ + } + return self; +} + +static VALUE +config_initialize_copy(VALUE self, VALUE other) +{ + CONF *conf = GetConfig(self); + VALUE str; + BIO *bio; + + str = rb_funcall(other, rb_intern("to_s"), 0); + rb_check_frozen(self); + bio = ossl_obj2bio(&str); + config_load_bio(conf, bio); /* Consumes BIO */ + return self; +} + +/* + * call-seq: + * config.get_value(section, key) -> string + * + * Gets the value of _key_ from the given _section_. + * + * Given the following configurating file being loaded: + * + * config = OpenSSL::Config.load('foo.cnf') + * #=> # + * puts config.to_s + * #=> [ default ] + * # foo=bar + * + * You can get a specific value from the config if you know the _section_ + * and _key_ like so: + * + * config.get_value('default','foo') + * #=> "bar" + */ +static VALUE +config_get_value(VALUE self, VALUE section, VALUE key) +{ + CONF *conf = GetConfig(self); + const char *str, *sectionp; + + StringValueCStr(section); + StringValueCStr(key); + /* For compatibility; NULL means "default". */ + sectionp = RSTRING_LEN(section) ? RSTRING_PTR(section) : NULL; + str = NCONF_get_string(conf, sectionp, RSTRING_PTR(key)); + if (!str) { + ossl_clear_error(); + return Qnil; + } + return rb_str_new_cstr(str); +} + +/* + * call-seq: + * config[section] -> hash + * + * Gets all key-value pairs in a specific _section_ from the current + * configuration. + * + * Given the following configurating file being loaded: + * + * config = OpenSSL::Config.load('foo.cnf') + * #=> # + * puts config.to_s + * #=> [ default ] + * # foo=bar + * + * You can get a hash of the specific section like so: + * + * config['default'] + * #=> {"foo"=>"bar"} * - * The default system configuration file for openssl */ +static VALUE +config_get_section(VALUE self, VALUE section) +{ + CONF *conf = GetConfig(self); + STACK_OF(CONF_VALUE) *sk; + int i, entries; + VALUE hash; + + hash = rb_hash_new(); + StringValueCStr(section); + if (!(sk = NCONF_get_section(conf, RSTRING_PTR(section)))) { + ossl_clear_error(); + return hash; + } + entries = sk_CONF_VALUE_num(sk); + for (i = 0; i < entries; i++) { + CONF_VALUE *entry = sk_CONF_VALUE_value(sk, i); + rb_hash_aset(hash, rb_str_new_cstr(entry->name), + rb_str_new_cstr(entry->value)); + } + return hash; +} + +static void +get_conf_section_doall_arg(CONF_VALUE *cv, VALUE *aryp) +{ + if (cv->name) + return; + rb_ary_push(*aryp, rb_str_new_cstr(cv->section)); +} + +/* IMPLEMENT_LHASH_DOALL_ARG_CONST() requires >= OpenSSL 1.1.0 */ +static IMPLEMENT_LHASH_DOALL_ARG_FN(get_conf_section, CONF_VALUE, VALUE) /* - * INIT + * call-seq: + * config.sections -> array of string + * + * Get the names of all sections in the current configuration. */ +static VALUE +config_get_sections(VALUE self) +{ + CONF *conf = GetConfig(self); + VALUE ary; + + ary = rb_ary_new(); + lh_doall_arg((_LHASH *)conf->data, LHASH_DOALL_ARG_FN(get_conf_section), + &ary); + return ary; +} + +static void +dump_conf_value_doall_arg(CONF_VALUE *cv, VALUE *strp) +{ + VALUE str = *strp; + STACK_OF(CONF_VALUE) *sk; + int i, num; + + if (cv->name) + return; + sk = (STACK_OF(CONF_VALUE) *)cv->value; + num = sk_CONF_VALUE_num(sk); + rb_str_cat_cstr(str, "[ "); + rb_str_cat_cstr(str, cv->section); + rb_str_cat_cstr(str, " ]\n"); + for (i = 0; i < num; i++){ + CONF_VALUE *v = sk_CONF_VALUE_value(sk, i); + rb_str_cat_cstr(str, v->name ? v->name : "None"); + rb_str_cat_cstr(str, "="); + rb_str_cat_cstr(str, v->value ? v->value : "None"); + rb_str_cat_cstr(str, "\n"); + } + rb_str_cat_cstr(str, "\n"); +} + +static IMPLEMENT_LHASH_DOALL_ARG_FN(dump_conf_value, CONF_VALUE, VALUE) + +/* + * call-seq: + * config.to_s -> string + * + * + * Gets the parsable form of the current configuration. + * + * Given the following configuration being created: + * + * config = OpenSSL::Config.new + * #=> # + * config['default'] = {"foo"=>"bar","baz"=>"buz"} + * #=> {"foo"=>"bar", "baz"=>"buz"} + * puts config.to_s + * #=> [ default ] + * # foo=bar + * # baz=buz + * + * You can parse get the serialized configuration using #to_s and then parse + * it later: + * + * serialized_config = config.to_s + * # much later... + * new_config = OpenSSL::Config.parse(serialized_config) + * #=> # + * puts new_config + * #=> [ default ] + * foo=bar + * baz=buz + */ +static VALUE +config_to_s(VALUE self) +{ + CONF *conf = GetConfig(self); + VALUE str; + + str = rb_str_new(NULL, 0); + lh_doall_arg((_LHASH *)conf->data, LHASH_DOALL_ARG_FN(dump_conf_value), + &str); + return str; +} + +static void +each_conf_value_doall_arg(CONF_VALUE *cv, void *unused) +{ + STACK_OF(CONF_VALUE) *sk; + VALUE section; + int i, num; + + if (cv->name) + return; + sk = (STACK_OF(CONF_VALUE) *)cv->value; + num = sk_CONF_VALUE_num(sk); + section = rb_str_new_cstr(cv->section); + for (i = 0; i < num; i++){ + CONF_VALUE *v = sk_CONF_VALUE_value(sk, i); + VALUE name = v->name ? rb_str_new_cstr(v->name) : Qnil; + VALUE value = v->value ? rb_str_new_cstr(v->value) : Qnil; + rb_yield(rb_ary_new3(3, section, name, value)); + } +} + +static IMPLEMENT_LHASH_DOALL_ARG_FN(each_conf_value, CONF_VALUE, void) + +/* + * call-seq: + * config.each { |section, key, value| } + * + * Retrieves the section and its pairs for the current configuration. + * + * config.each do |section, key, value| + * # ... + * end + */ +static VALUE +config_each(VALUE self) +{ + CONF *conf = GetConfig(self); + + RETURN_ENUMERATOR(self, 0, 0); + + lh_doall_arg((_LHASH *)conf->data, LHASH_DOALL_ARG_FN(each_conf_value), + NULL); + return self; +} + +/* + * call-seq: + * config.inspect -> string + * + * String representation of this configuration object, including the class + * name and its sections. + */ +static VALUE +config_inspect(VALUE self) +{ + VALUE str, ary = config_get_sections(self); + const char *cname = rb_class2name(rb_obj_class(self)); + + str = rb_str_new_cstr("#<"); + rb_str_cat_cstr(str, cname); + rb_str_cat_cstr(str, " sections="); + rb_str_append(str, rb_inspect(ary)); + rb_str_cat_cstr(str, ">"); + + return str; +} + void Init_ossl_config(void) { - char *default_config_file; + char *path; + VALUE path_str; #if 0 mOSSL = rb_define_module("OpenSSL"); eOSSLError = rb_define_class_under(mOSSL, "OpenSSLError", rb_eStandardError); #endif - eConfigError = rb_define_class_under(mOSSL, "ConfigError", eOSSLError); + /* Document-class: OpenSSL::Config + * + * Configuration for the openssl library. + * + * Many system's installation of openssl library will depend on your system + * configuration. See the value of OpenSSL::Config::DEFAULT_CONFIG_FILE for + * the location of the file for your host. + * + * See also http://www.openssl.org/docs/apps/config.html + */ cConfig = rb_define_class_under(mOSSL, "Config", rb_cObject); - default_config_file = CONF_get1_default_config_file(); - rb_define_const(cConfig, "DEFAULT_CONFIG_FILE", - rb_str_new2(default_config_file)); - OPENSSL_free(default_config_file); - /* methods are defined by openssl/config.rb */ + /* Document-class: OpenSSL::ConfigError + * + * General error for openssl library configuration files. Including formatting, + * parsing errors, etc. + */ + eConfigError = rb_define_class_under(mOSSL, "ConfigError", eOSSLError); + + rb_include_module(cConfig, rb_mEnumerable); + rb_define_singleton_method(cConfig, "parse", config_s_parse, 1); + rb_define_singleton_method(cConfig, "parse_config", config_s_parse_config, 1); + rb_define_alias(CLASS_OF(cConfig), "load", "new"); + rb_define_alloc_func(cConfig, config_s_alloc); + rb_define_method(cConfig, "initialize", config_initialize, -1); + rb_define_method(cConfig, "initialize_copy", config_initialize_copy, 1); + rb_define_method(cConfig, "get_value", config_get_value, 2); + rb_define_method(cConfig, "[]", config_get_section, 1); + rb_define_method(cConfig, "sections", config_get_sections, 0); + rb_define_method(cConfig, "to_s", config_to_s, 0); + rb_define_method(cConfig, "each", config_each, 0); + rb_define_method(cConfig, "inspect", config_inspect, 0); + + /* Document-const: DEFAULT_CONFIG_FILE + * + * The default system configuration file for OpenSSL. + */ + path = CONF_get1_default_config_file(); + path_str = ossl_buf2str(path, rb_long2int(strlen(path))); + rb_define_const(cConfig, "DEFAULT_CONFIG_FILE", path_str); } diff --git a/ext/openssl/ossl_config.h b/ext/openssl/ossl_config.h index 627d297ba..c96a00f70 100644 --- a/ext/openssl/ossl_config.h +++ b/ext/openssl/ossl_config.h @@ -7,13 +7,10 @@ * This program is licensed under the same licence as Ruby. * (See the file 'LICENCE'.) */ -#if !defined(_OSSL_CONFIG_H_) -#define _OSSL_CONFIG_H_ +#ifndef OSSL_CONFIG_H +#define OSSL_CONFIG_H -extern VALUE cConfig; -extern VALUE eConfigError; - -CONF* DupConfigPtr(VALUE obj); +CONF *DupConfigPtr(VALUE obj); void Init_ossl_config(void); -#endif /* _OSSL_CONFIG_H_ */ +#endif /* OSSL_CONFIG_H */ diff --git a/lib/openssl.rb b/lib/openssl.rb index b04748578..8a342f15b 100644 --- a/lib/openssl.rb +++ b/lib/openssl.rb @@ -15,7 +15,6 @@ require_relative 'openssl/bn' require_relative 'openssl/pkey' require_relative 'openssl/cipher' -require_relative 'openssl/config' require_relative 'openssl/digest' require_relative 'openssl/hmac' require_relative 'openssl/x509' diff --git a/lib/openssl/config.rb b/lib/openssl/config.rb deleted file mode 100644 index 46e1711d2..000000000 --- a/lib/openssl/config.rb +++ /dev/null @@ -1,411 +0,0 @@ -# frozen_string_literal: true -=begin -= Ruby-space definitions that completes C-space funcs for Config - -= Info - Copyright (C) 2010 Hiroshi Nakamura - -= Licence - This program is licensed under the same licence as Ruby. - (See the file 'LICENCE'.) - -=end - -require 'stringio' - -module OpenSSL - ## - # = OpenSSL::Config - # - # Configuration for the openssl library. - # - # Many system's installation of openssl library will depend on your system - # configuration. See the value of OpenSSL::Config::DEFAULT_CONFIG_FILE for - # the location of the file for your host. - # - # See also http://www.openssl.org/docs/apps/config.html - class Config - include Enumerable - - class << self - - ## - # Parses a given _string_ as a blob that contains configuration for - # OpenSSL. - # - # If the source of the IO is a file, then consider using #parse_config. - def parse(string) - c = new() - parse_config(StringIO.new(string)).each do |section, hash| - c.set_section(section, hash) - end - c - end - - ## - # load is an alias to ::new - alias load new - - ## - # Parses the configuration data read from _io_, see also #parse. - # - # Raises a ConfigError on invalid configuration data. - def parse_config(io) - begin - parse_config_lines(io) - rescue => error - raise ConfigError, "error in line #{io.lineno}: " + error.message - end - end - - def get_key_string(data, section, key) # :nodoc: - if v = data[section] && data[section][key] - return v - elsif section == 'ENV' - if v = ENV[key] - return v - end - end - if v = data['default'] && data['default'][key] - return v - end - end - - private - - def parse_config_lines(io) - section = 'default' - data = {section => {}} - io_stack = [io] - while definition = get_definition(io_stack) - definition = clear_comments(definition) - next if definition.empty? - case definition - when /\A\[/ - if /\[([^\]]*)\]/ =~ definition - section = $1.strip - data[section] ||= {} - else - raise ConfigError, "missing close square bracket" - end - when /\A\.include (\s*=\s*)?(.+)\z/ - path = $2 - if File.directory?(path) - files = Dir.glob(File.join(path, "*.{cnf,conf}"), File::FNM_EXTGLOB) - else - files = [path] - end - - files.each do |filename| - begin - io_stack << StringIO.new(File.read(filename)) - rescue - raise ConfigError, "could not include file '%s'" % filename - end - end - when /\A([^:\s]*)(?:::([^:\s]*))?\s*=(.*)\z/ - if $2 - section = $1 - key = $2 - else - key = $1 - end - value = unescape_value(data, section, $3) - (data[section] ||= {})[key] = value.strip - else - raise ConfigError, "missing equal sign" - end - end - data - end - - # escape with backslash - QUOTE_REGEXP_SQ = /\A([^'\\]*(?:\\.[^'\\]*)*)'/ - # escape with backslash and doubled dq - QUOTE_REGEXP_DQ = /\A([^"\\]*(?:""[^"\\]*|\\.[^"\\]*)*)"/ - # escaped char map - ESCAPE_MAP = { - "r" => "\r", - "n" => "\n", - "b" => "\b", - "t" => "\t", - } - - def unescape_value(data, section, value) - scanned = [] - while m = value.match(/['"\\$]/) - scanned << m.pre_match - c = m[0] - value = m.post_match - case c - when "'" - if m = value.match(QUOTE_REGEXP_SQ) - scanned << m[1].gsub(/\\(.)/, '\\1') - value = m.post_match - else - break - end - when '"' - if m = value.match(QUOTE_REGEXP_DQ) - scanned << m[1].gsub(/""/, '').gsub(/\\(.)/, '\\1') - value = m.post_match - else - break - end - when "\\" - c = value.slice!(0, 1) - scanned << (ESCAPE_MAP[c] || c) - when "$" - ref, value = extract_reference(value) - refsec = section - if ref.index('::') - refsec, ref = ref.split('::', 2) - end - if v = get_key_string(data, refsec, ref) - scanned << v - else - raise ConfigError, "variable has no value" - end - else - raise 'must not reaced' - end - end - scanned << value - scanned.join - end - - def extract_reference(value) - rest = '' - if m = value.match(/\(([^)]*)\)|\{([^}]*)\}/) - value = m[1] || m[2] - rest = m.post_match - elsif [?(, ?{].include?(value[0]) - raise ConfigError, "no close brace" - end - if m = value.match(/[a-zA-Z0-9_]*(?:::[a-zA-Z0-9_]*)?/) - return m[0], m.post_match + rest - else - raise - end - end - - def clear_comments(line) - # FCOMMENT - if m = line.match(/\A([\t\n\f ]*);.*\z/) - return m[1] - end - # COMMENT - scanned = [] - while m = line.match(/[#'"\\]/) - scanned << m.pre_match - c = m[0] - line = m.post_match - case c - when '#' - line = nil - break - when "'", '"' - regexp = (c == "'") ? QUOTE_REGEXP_SQ : QUOTE_REGEXP_DQ - scanned << c - if m = line.match(regexp) - scanned << m[0] - line = m.post_match - else - scanned << line - line = nil - break - end - when "\\" - scanned << c - scanned << line.slice!(0, 1) - else - raise 'must not reaced' - end - end - scanned << line - scanned.join - end - - def get_definition(io_stack) - if line = get_line(io_stack) - while /[^\\]\\\z/ =~ line - if extra = get_line(io_stack) - line += extra - else - break - end - end - return line.strip - end - end - - def get_line(io_stack) - while io = io_stack.last - if line = io.gets - return line.gsub(/[\r\n]*/, '') - end - io_stack.pop - end - end - end - - ## - # Creates an instance of OpenSSL's configuration class. - # - # This can be used in contexts like OpenSSL::X509::ExtensionFactory.config= - # - # If the optional _filename_ parameter is provided, then it is read in and - # parsed via #parse_config. - # - # This can raise IO exceptions based on the access, or availability of the - # file. A ConfigError exception may be raised depending on the validity of - # the data being configured. - # - def initialize(filename = nil) - @data = {} - if filename - File.open(filename.to_s) do |file| - Config.parse_config(file).each do |section, hash| - set_section(section, hash) - end - end - end - end - - ## - # Gets the value of _key_ from the given _section_ - # - # Given the following configurating file being loaded: - # - # config = OpenSSL::Config.load('foo.cnf') - # #=> # - # puts config.to_s - # #=> [ default ] - # # foo=bar - # - # You can get a specific value from the config if you know the _section_ - # and _key_ like so: - # - # config.get_value('default','foo') - # #=> "bar" - # - def get_value(section, key) - if section.nil? - raise TypeError.new('nil not allowed') - end - section = 'default' if section.empty? - get_key_string(section, key) - end - - ## - # Get a specific _section_ from the current configuration - # - # Given the following configurating file being loaded: - # - # config = OpenSSL::Config.load('foo.cnf') - # #=> # - # puts config.to_s - # #=> [ default ] - # # foo=bar - # - # You can get a hash of the specific section like so: - # - # config['default'] - # #=> {"foo"=>"bar"} - # - def [](section) - @data[section] || {} - end - - def set_section(section, pairs) # :nodoc: - hash = @data[section] ||= {} - pairs.each do |key, value| - hash[key] = value - end - end - - ## - # Get the names of all sections in the current configuration - def sections - @data.keys - end - - ## - # Get the parsable form of the current configuration - # - # Given the following configuration being created: - # - # config = OpenSSL::Config.new - # #=> # - # config['default'] = {"foo"=>"bar","baz"=>"buz"} - # #=> {"foo"=>"bar", "baz"=>"buz"} - # puts config.to_s - # #=> [ default ] - # # foo=bar - # # baz=buz - # - # You can parse get the serialized configuration using #to_s and then parse - # it later: - # - # serialized_config = config.to_s - # # much later... - # new_config = OpenSSL::Config.parse(serialized_config) - # #=> # - # puts new_config - # #=> [ default ] - # foo=bar - # baz=buz - # - def to_s - ary = [] - @data.keys.sort.each do |section| - ary << "[ #{section} ]\n" - @data[section].keys.each do |key| - ary << "#{key}=#{@data[section][key]}\n" - end - ary << "\n" - end - ary.join - end - - ## - # For a block. - # - # Receive the section and its pairs for the current configuration. - # - # config.each do |section, key, value| - # # ... - # end - # - def each - @data.each do |section, hash| - hash.each do |key, value| - yield [section, key, value] - end - end - end - - ## - # String representation of this configuration object, including the class - # name and its sections. - def inspect - "#<#{self.class.name} sections=#{sections.inspect}>" - end - - protected - - def data # :nodoc: - @data - end - - private - - def initialize_copy(other) - @data = other.data.dup - end - - def get_key_string(section, key) - Config.get_key_string(@data, section, key) - end - end -end diff --git a/test/openssl/test_config.rb b/test/openssl/test_config.rb index 01be28164..3af9923d2 100644 --- a/test/openssl/test_config.rb +++ b/test/openssl/test_config.rb @@ -150,6 +150,10 @@ def test_s_parse_include # Include a file by relative path c1 = OpenSSL::Config.parse(include_file) + if c1["sec-main"][".include"] + # OpenSSL < 1.1.1 parses '.include =' as a normal assignment + pend ".include directive is not supported" + end assert_equal(["default", "sec-a", "sec-b", "sec-main"], c1.sections.sort) assert_equal(["file-a", "file-b", "file-main"], c1["default"].keys.sort) assert_equal({"a" => "123"}, c1["sec-a"]) @@ -157,9 +161,9 @@ def test_s_parse_include assert_equal({"main" => "123", "key_outside_section" => "value_a"}, c1["sec-main"]) # Relative paths are from the working directory - assert_raise(OpenSSL::ConfigError) do - Dir.chdir("child") { OpenSSL::Config.parse(include_file) } - end + # Inclusion fails, but the error is ignored silently + c2 = Dir.chdir("child") { OpenSSL::Config.parse(include_file) } + assert_equal(["default", "sec-main"], c2.sections.sort) end end From d9064190ca892708f71da500a6e24fb2615c6ccb Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Wed, 19 Feb 2020 05:11:54 +0000 Subject: [PATCH 6/6] config: replace DupConfigPtr() with GetConfig() Now that OpenSSL::Config wraps a real CONF object, the caller can just borrow it rather than creating a new temporary CONF object. CONF object is usually treated as immutable. DupConfigPtr() is now removed, and GetConfig() is exported instead. --- ext/openssl/ossl_config.c | 38 +------------------------------------- ext/openssl/ossl_config.h | 2 +- ext/openssl/ossl_x509ext.c | 3 +-- 3 files changed, 3 insertions(+), 40 deletions(-) diff --git a/ext/openssl/ossl_config.c b/ext/openssl/ossl_config.c index 52d96e1ed..21c327b26 100644 --- a/ext/openssl/ossl_config.c +++ b/ext/openssl/ossl_config.c @@ -25,7 +25,7 @@ static const rb_data_type_t ossl_config_type = { 0, 0, RUBY_TYPED_FREE_IMMEDIATELY, }; -static CONF * +CONF * GetConfig(VALUE obj) { CONF *conf; @@ -50,42 +50,6 @@ config_s_alloc(VALUE klass) return obj; } -/* - * DupConfigPtr is a public C-level function for getting OpenSSL CONF struct - * from an OpenSSL::Config(eConfig) instance. We decided to implement - * OpenSSL::Config in Ruby level but we need to pass native CONF struct for - * some OpenSSL features such as X509V3_EXT_*. - */ -CONF * -DupConfigPtr(VALUE obj) -{ - CONF *conf; - VALUE str; - BIO *bio; - long eline = -1; - - OSSL_Check_Kind(obj, cConfig); - str = rb_funcall(obj, rb_intern("to_s"), 0); - bio = ossl_obj2bio(&str); - conf = NCONF_new(NULL); - if(!conf){ - BIO_free(bio); - ossl_raise(eConfigError, NULL); - } - if(!NCONF_load_bio(conf, bio, &eline)){ - BIO_free(bio); - NCONF_free(conf); - if (eline <= 0) - ossl_raise(eConfigError, "wrong config format"); - else - ossl_raise(eConfigError, "error in line %d", eline); - } - BIO_free(bio); - - return conf; -} - - static void config_load_bio(CONF *conf, BIO *bio) { diff --git a/ext/openssl/ossl_config.h b/ext/openssl/ossl_config.h index c96a00f70..4e604f1ae 100644 --- a/ext/openssl/ossl_config.h +++ b/ext/openssl/ossl_config.h @@ -10,7 +10,7 @@ #ifndef OSSL_CONFIG_H #define OSSL_CONFIG_H -CONF *DupConfigPtr(VALUE obj); +CONF *GetConfig(VALUE obj); void Init_ossl_config(void); #endif /* OSSL_CONFIG_H */ diff --git a/ext/openssl/ossl_x509ext.c b/ext/openssl/ossl_x509ext.c index 5eb9bd759..e54102c77 100644 --- a/ext/openssl/ossl_x509ext.c +++ b/ext/openssl/ossl_x509ext.c @@ -226,11 +226,10 @@ ossl_x509extfactory_create_ext(int argc, VALUE *argv, VALUE self) GetX509ExtFactory(self, ctx); obj = NewX509Ext(cX509Ext); rconf = rb_iv_get(self, "@config"); - conf = NIL_P(rconf) ? NULL : DupConfigPtr(rconf); + conf = NIL_P(rconf) ? NULL : GetConfig(rconf); X509V3_set_nconf(ctx, conf); ext = X509V3_EXT_nconf_nid(conf, ctx, nid, RSTRING_PTR(valstr)); X509V3_set_ctx_nodb(ctx); - NCONF_free(conf); if (!ext){ ossl_raise(eX509ExtError, "%"PRIsVALUE" = %"PRIsVALUE, oid, valstr); }