ruby-changes:46966
From: rhe <ko1@a...>
Date: Wed, 14 Jun 2017 18:49:16 +0900 (JST)
Subject: [ruby-changes:46966] rhe:r59081 (trunk): openssl: import v2.0.4
rhe 2017-06-14 18:49:09 +0900 (Wed, 14 Jun 2017) New Revision: 59081 https://svn.ruby-lang.org/cgi-bin/viewvc.cgi?view=revision&revision=59081 Log: openssl: import v2.0.4 Import Ruby/OpenSSL 2.0.4. Only bug (and typo) fixes. The full commit history since v2.0.3 (imported at r57482) can be found at: https://github.com/ruby/openssl/compare/v2.0.3...v2.0.4 This contains the fix for [Bug #11033]. ---------------------------------------------------------------- Jun Aruga (1): Update .travis.yml and Dockerfile Kazuki Yamaguchi (9): test/test_pkey_ec: do not use dummy 0 order test/test_ssl: fix typo in test_sysread_and_syswrite ssl: check return value of SSL_set_fd() Fix typos test/test_x509store: skip OpenSSL::TestX509Store#test_set_errors tool/sync-with-trunk: 'LASY' -> 'LAST' x509store: clear error queue after calling X509_LOOKUP_load_file() extconf.rb: simplify searching libraries logic Ruby/OpenSSL 2.0.4 SHIBATA Hiroshi (1): Fix typos Vladimir Rybas (1): Fix documentation for OpenSSL::Cipher#final nobu (2): openssl: fix broken openssl check openssl: fix broken openssl check usa (1): Search SSL libraries by testing various filename patterns Modified files: trunk/ext/openssl/History.md trunk/ext/openssl/extconf.rb trunk/ext/openssl/openssl.gemspec trunk/ext/openssl/ossl_bn.c trunk/ext/openssl/ossl_cipher.c trunk/ext/openssl/ossl_ns_spki.c trunk/ext/openssl/ossl_pkey_ec.c trunk/ext/openssl/ossl_pkey_rsa.c trunk/ext/openssl/ossl_ssl.c trunk/ext/openssl/ossl_version.h trunk/ext/openssl/ossl_x509store.c trunk/test/openssl/test_pkcs7.rb trunk/test/openssl/test_ssl.rb trunk/test/openssl/test_x509store.rb Index: test/openssl/test_ssl.rb =================================================================== --- test/openssl/test_ssl.rb (revision 59080) +++ test/openssl/test_ssl.rb (revision 59081) @@ -66,7 +66,7 @@ class OpenSSL::TestSSL < OpenSSL::SSLTes https://github.com/ruby/ruby/blob/trunk/test/openssl/test_ssl.rb#L66 buf = "" ssl.syswrite(str) assert_same buf, ssl.sysread(str.size, buf) - assert_equal(str, newstr) + assert_equal(str, buf) } } end Index: test/openssl/test_x509store.rb =================================================================== --- test/openssl/test_x509store.rb (revision 59080) +++ test/openssl/test_x509store.rb (revision 59081) @@ -34,6 +34,29 @@ class OpenSSL::TestX509Store < OpenSSL:: https://github.com/ruby/ruby/blob/trunk/test/openssl/test_x509store.rb#L34 OpenSSL::TestUtils.issue_crl(*args) end + def test_add_file + ca_exts = [ + ["basicConstraints", "CA:TRUE", true], + ["keyUsage", "cRLSign,keyCertSign", true], + ] + cert1 = issue_cert(@ca1, @rsa1024, 1, ca_exts, nil, nil) + cert2 = issue_cert(@ca2, @rsa2048, 1, ca_exts, nil, nil) + tmpfile = Tempfile.open { |f| f << cert1.to_pem << cert2.to_pem; f } + + store = OpenSSL::X509::Store.new + assert_equal false, store.verify(cert1) + assert_equal false, store.verify(cert2) + store.add_file(tmpfile.path) + assert_equal true, store.verify(cert1) + assert_equal true, store.verify(cert2) + + # OpenSSL < 1.1.1 leaks an error on a duplicate certificate + assert_nothing_raised { store.add_file(tmpfile.path) } + assert_equal [], OpenSSL.errors + ensure + tmpfile and tmpfile.close! + end + def test_verify # OpenSSL uses time(2) while Time.now uses clock_gettime(CLOCK_REALTIME), # and there may be difference. @@ -194,6 +217,7 @@ class OpenSSL::TestX509Store < OpenSSL:: https://github.com/ruby/ruby/blob/trunk/test/openssl/test_x509store.rb#L217 end def test_set_errors + return if OpenSSL::OPENSSL_VERSION_NUMBER >= 0x10100000 now = Time.now ca1_cert = issue_cert(@ca1, @rsa2048, 1, [], nil, nil) store = OpenSSL::X509::Store.new Index: test/openssl/test_pkcs7.rb =================================================================== --- test/openssl/test_pkcs7.rb (revision 59080) +++ test/openssl/test_pkcs7.rb (revision 59081) @@ -51,7 +51,7 @@ class OpenSSL::TestPKCS7 < OpenSSL::Test https://github.com/ruby/ruby/blob/trunk/test/openssl/test_pkcs7.rb#L51 assert_equal(@ee1_cert.serial, signers[0].serial) assert_equal(@ee1_cert.issuer.to_s, signers[0].issuer.to_s) - # Normaly OpenSSL tries to translate the supplied content into canonical + # Normally OpenSSL tries to translate the supplied content into canonical # MIME format (e.g. a newline character is converted into CR+LF). # If the content is a binary, PKCS7::BINARY flag should be used. Index: ext/openssl/ossl_x509store.c =================================================================== --- ext/openssl/ossl_x509store.c (revision 59080) +++ ext/openssl/ossl_x509store.c (revision 59081) @@ -342,6 +342,15 @@ ossl_x509store_add_file(VALUE self, VALU https://github.com/ruby/ruby/blob/trunk/ext/openssl/ossl_x509store.c#L342 if(X509_LOOKUP_load_file(lookup, path, X509_FILETYPE_PEM) != 1){ ossl_raise(eX509StoreError, NULL); } +#if OPENSSL_VERSION_NUMBER < 0x10101000 || defined(LIBRESSL_VERSION_NUMBER) + /* + * X509_load_cert_crl_file() which is called from X509_LOOKUP_load_file() + * did not check the return value of X509_STORE_add_{cert,crl}(), leaking + * "cert already in hash table" errors on the error queue, if duplicate + * certificates are found. This will be fixed by OpenSSL 1.1.1. + */ + ossl_clear_error(); +#endif return self; } Index: ext/openssl/openssl.gemspec =================================================================== --- ext/openssl/openssl.gemspec (revision 59080) +++ ext/openssl/openssl.gemspec (revision 59081) @@ -1,25 +1,25 @@ https://github.com/ruby/ruby/blob/trunk/ext/openssl/openssl.gemspec#L1 # -*- encoding: utf-8 -*- -# stub: openssl 2.0.3 ruby lib +# stub: openssl 2.0.4 ruby lib # stub: ext/openssl/extconf.rb Gem::Specification.new do |s| s.name = "openssl".freeze - s.version = "2.0.3" + s.version = "2.0.4" s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.require_paths = ["lib".freeze] s.authors = ["Martin Bosslet".freeze, "SHIBATA Hiroshi".freeze, "Zachary Scott".freeze, "Kazuki Yamaguchi".freeze] - s.date = "2017-01-31" + s.date = "2017-06-14" s.description = "It wraps the OpenSSL library.".freeze s.email = ["ruby-core@r...".freeze] s.extensions = ["ext/openssl/extconf.rb".freeze] - s.extra_rdoc_files = ["CONTRIBUTING.md".freeze, "README.md".freeze, "History.md".freeze] + s.extra_rdoc_files = ["CONTRIBUTING.md".freeze, "History.md".freeze, "README.md".freeze] s.files = ["BSDL".freeze, "CONTRIBUTING.md".freeze, "History.md".freeze, "LICENSE.txt".freeze, "README.md".freeze, "ext/openssl/deprecation.rb".freeze, "ext/openssl/extconf.rb".freeze, "ext/openssl/openssl_missing.c".freeze, "ext/openssl/openssl_missing.h".freeze, "ext/openssl/ossl.c".freeze, "ext/openssl/ossl.h".freeze, "ext/openssl/ossl_asn1.c".freeze, "ext/openssl/ossl_asn1.h".freeze, "ext/openssl/ossl_bio.c".freeze, "ext/openssl/ossl_bio.h".freeze, "ext/openssl/ossl_bn.c".freeze, "ext/openssl/ossl_bn.h".freeze, "ext/openssl/ossl_cipher.c".freeze, "ext/openssl/ossl_cipher.h".freeze, "ext/openssl/ossl_config.c".freeze, "ext/openssl/ossl_config.h".freeze, "ext/openssl/ossl_digest.c".freeze, "ext/openssl/ossl_digest.h".freeze, "ext/openssl/ossl_engine.c".freeze, "ext/openssl/ossl_engine.h".freeze, "ext/openssl/ossl_hmac.c".freeze, "ext/openssl/ossl_hmac.h".freeze, "ext/openssl/ossl_ns_spki.c".freeze, "ext/openssl/ossl_ns_spki.h".freeze, "ext/openssl/ossl_ocsp.c".freeze, "ext/opens sl/ossl_ocsp.h".freeze, "ext/openssl/ossl_pkcs12.c".freeze, "ext/openssl/ossl_pkcs12.h".freeze, "ext/openssl/ossl_pkcs5.c".freeze, "ext/openssl/ossl_pkcs5.h".freeze, "ext/openssl/ossl_pkcs7.c".freeze, "ext/openssl/ossl_pkcs7.h".freeze, "ext/openssl/ossl_pkey.c".freeze, "ext/openssl/ossl_pkey.h".freeze, "ext/openssl/ossl_pkey_dh.c".freeze, "ext/openssl/ossl_pkey_dsa.c".freeze, "ext/openssl/ossl_pkey_ec.c".freeze, "ext/openssl/ossl_pkey_rsa.c".freeze, "ext/openssl/ossl_rand.c".freeze, "ext/openssl/ossl_rand.h".freeze, "ext/openssl/ossl_ssl.c".freeze, "ext/openssl/ossl_ssl.h".freeze, "ext/openssl/ossl_ssl_session.c".freeze, "ext/openssl/ossl_version.h".freeze, "ext/openssl/ossl_x509.c".freeze, "ext/openssl/ossl_x509.h".freeze, "ext/openssl/ossl_x509attr.c".freeze, "ext/openssl/ossl_x509cert.c".freeze, "ext/openssl/ossl_x509crl.c".freeze, "ext/openssl/ossl_x509ext.c".freeze, "ext/openssl/ossl_x509name.c".freeze, "ext/openssl/ossl_x509req.c".freeze, "ext/openssl/ossl_x509revoked.c".freez e, "ext/openssl/ossl_x509store.c".freeze, "ext/openssl/ruby_missing.h".freeze, "lib/openssl.rb".freeze, "lib/openssl/bn.rb".freeze, "lib/openssl/buffering.rb".freeze, "lib/openssl/cipher.rb".freeze, "lib/openssl/config.rb".freeze, "lib/openssl/digest.rb".freeze, "lib/openssl/pkey.rb".freeze, "lib/openssl/ssl.rb".freeze, "lib/openssl/x509.rb".freeze] s.homepage = "https://www.ruby-lang.org/".freeze s.licenses = ["Ruby".freeze] s.rdoc_options = ["--main".freeze, "README.md".freeze] s.required_ruby_version = Gem::Requirement.new(">= 2.3.0".freeze) - s.rubygems_version = "2.6.10".freeze + s.rubygems_version = "2.6.12".freeze s.summary = "OpenSSL provides SSL, TLS and general purpose cryptography.".freeze if s.respond_to? :specification_version then Index: ext/openssl/ossl_bn.c =================================================================== --- ext/openssl/ossl_bn.c (revision 59080) +++ ext/openssl/ossl_bn.c (revision 59081) @@ -129,7 +129,7 @@ try_convert_to_bn(VALUE obj) https://github.com/ruby/ruby/blob/trunk/ext/openssl/ossl_bn.c#L129 if (rb_obj_is_kind_of(obj, cBN)) return obj; if (RB_INTEGER_TYPE_P(obj)) { - newobj = NewBN(cBN); /* Handle potencial mem leaks */ + newobj = NewBN(cBN); /* Handle potential mem leaks */ bn = integer_to_bnptr(obj, NULL); SetBN(newobj, bn); } Index: ext/openssl/ossl_ns_spki.c =================================================================== --- ext/openssl/ossl_ns_spki.c (revision 59080) +++ ext/openssl/ossl_ns_spki.c (revision 59081) @@ -322,7 +322,7 @@ ossl_spki_verify(VALUE self, VALUE key) https://github.com/ruby/ruby/blob/trunk/ext/openssl/ossl_ns_spki.c#L322 /* Document-class: OpenSSL::Netscape::SPKI * - * A Simple Public Key Infrastructure implementation (pronounced "spookey"). + * A Simple Public Key Infrastructure implementation (pronounced "spooky"). * The structure is defined as * PublicKeyAndChallenge ::= SEQUENCE { * spki SubjectPublicKeyInfo, @@ -348,7 +348,7 @@ ossl_spki_verify(VALUE self, VALUE key) https://github.com/ruby/ruby/blob/trunk/ext/openssl/ossl_ns_spki.c#L348 * spki.public_key = key.public_key * spki.sign(key, OpenSSL::Digest::SHA256.new) * #send a request containing this to a server generating a certificate - * === Verifiying an SPKI request + * === Verifying an SPKI request * request = #... * spki = OpenSSL::Netscape::SPKI.new request * unless spki.verify(spki.public_key) Index: ext/openssl/History.md =================================================================== --- ext/openssl/History.md (revision 59080) +++ ext/openssl/History.md (revision 59081) @@ -1,3 +1,50 @@ https://github.com/ruby/ruby/blob/trunk/ext/openssl/History.md#L1 +Version 2.0.4 +============= + +Bug fixes +--------- + +* It now compiles with LibreSSL without renaming on Windows (mswin). +* A workaround for the error queue leak of X509_load_cert_crl_file() that + causes random errors is added. + [[Bug #11033]](https://bugs.ruby-lang.org/issues/11033) + + +Version 2.0.3 +============= + +Bug fixes +--------- + +* OpenSSL::ASN1::Constructive#each which was broken by 2.0.0 is fixed. + [[ruby/openssl#96]](https://github.com/ruby/openssl/pull/96) +* Fixed build with static OpenSSL libraries on Windows. + [[Bug #13080]](https://bugs.ruby-lang.org/issues/13080) +* OpenSSL::X509::Name#eql? which was broken by 2.0.0 is fixed. + + +Version 2.0.2 +============= + +Bug fixes +--------- + +* Fix build with early 0.9.8 series which did not have SSL_CTX_clear_options(). + [ruby-core:78693] + + +Version 2.0.1 +============= + +Bug fixes +--------- + +* A GC issue around OpenSSL::BN is fixed. + [[ruby/openssl#87]](https://github.com/ruby/openssl/issues/87) +* OpenSSL::ASN1 now parses BER encoding of GeneralizedTime without seconds. + [[ruby/openssl#88]](https://github.com/ruby/openssl/pull/88) + + Version 2.0.0 ============= @@ -23,7 +70,8 @@ Supported platforms https://github.com/ruby/ruby/blob/trunk/ext/openssl/History.md#L70 Notable changes --------------- -* Add support for OpenSSL 1.1.0. [[Feature #12324]](https://bugs.ruby-lang.org/issues/12324) +* Add support for OpenSSL 1.1.0. + [[Feature #12324]](https://bugs.ruby-lang.org/issues/12324) * Add support for LibreSSL * OpenSSL::Cipher Index: ext/openssl/extconf.rb =================================================================== --- ext/openssl/extconf.rb (revision 59080) +++ ext/openssl/extconf.rb (revision 59081) @@ -46,41 +46,43 @@ def find_openssl_library https://github.com/ruby/ruby/blob/trunk/ext/openssl/extconf.rb#L46 return false unless have_header("openssl/ssl.h") - libpath = $LIBPATH.dup - libpath |= ENV["LIB"].split(File::PATH_SEPARATOR).map{|d| d.tr(File::ALT_SEPARATOR, File::SEPARATOR)} if $mswin + ret = have_library("crypto", "CRYPTO_malloc") && + have_library("ssl", "SSL_new") + return ret if ret - result = false - %w[crypto eay32].each do |base| - libs = [base] - if $mswin || $mingw - libs << "lib" + libs.first - if base == "crypto" - libs << libs.first + "-[0-9][0-9]" - libs << "lib" + libs.last - end - libs = Dir.glob(libs.map{|l| libpath.map{|d| File.join(d, l + ".*")}}.flatten).map{|path| File.basename(path, ".*")}.uniq + if $mswin + # OpenSSL >= 1.1.0: libcrypto.lib and libssl.lib. + if have_library("libcrypto", "CRYPTO_malloc") && + have_library("libssl", "SSL_new") + return true end - libs.each do |lib| - result = have_library(lib, "CRYPTO_malloc") - break if result + + # OpenSSL <= 1.0.2: libeay32.lib and ssleay32.lib. + if have_library("libeay32", "CRYPTO_malloc") && + have_library("ssleay32", "SSL_new") + return true end - break if result - end - return false unless result - %w[ssl ssleay32].each do |base| - libs = [base] - if $mswin || $mingw - libs << "lib" + libs.first - if base == "ssl" - libs << libs.first + "-[0-9][0-9]" - libs << "lib" + libs.last - end + # LibreSSL: libcrypto-##.lib and libssl-##.lib, where ## is the ABI version + # number. We have to find the version number out by scanning libpath. + libpath = $LIBPATH.dup + libpath |= ENV["LIB"].split(File::PATH_SEPARATOR) + libpath.map! { |d| d.tr(File::ALT_SEPARATOR, File::SEPARATOR) } + + ret = [ + ["crypto", "CRYPTO_malloc"], + ["ssl", "SSL_new"] + ].all? do |base, func| + result = false + libs = ["lib#{base}-[0-9][0-9]", "lib#{base}-[0-9][0-9][0-9]"] libs = Dir.glob(libs.map{|l| libpath.map{|d| File.join(d, l + ".*")}}.flatten).map{|path| File.basename(path, ".*")}.uniq + libs.each do |lib| + result = have_library(lib, func) + break if result + end + result end - libs.each do |lib| - return true if have_library(lib, "SSL_new") - end + return ret if ret end return false end Index: ext/openssl/ossl_version.h =================================================================== --- ext/openssl/ossl_version.h (revision 59080) +++ ext/openssl/ossl_version.h (revision 59081) @@ -10,6 +10,6 @@ https://github.com/ruby/ruby/blob/trunk/ext/openssl/ossl_version.h#L10 #if !defined(_OSSL_VERSION_H_) #define _OSSL_VERSION_H_ -#define OSSL_VERSION "2.0.3" +#define OSSL_VERSION "2.0.4" #endif /* _OSSL_VERSION_H_ */ Index: ext/openssl/ossl_ssl.c =================================================================== --- ext/openssl/ossl_ssl.c (revision 59080) +++ ext/openssl/ossl_ssl.c (revision 59081) @@ -1483,7 +1483,8 @@ ossl_ssl_setup(VALUE self) https://github.com/ruby/ruby/blob/trunk/ext/openssl/ossl_ssl.c#L1483 GetOpenFile(io, fptr); rb_io_check_readable(fptr); rb_io_check_writable(fptr); - SSL_set_fd(ssl, TO_SOCKET(FPTR_TO_FD(fptr))); + if (!SSL_set_fd(ssl, TO_SOCKET(FPTR_TO_FD(fptr)))) + ossl_raise(eSSLError, "SSL_set_fd"); return Qtrue; } Index: ext/openssl/ossl_cipher.c =================================================================== --- ext/openssl/ossl_cipher.c (revision 59080) +++ ext/openssl/ossl_cipher.c (revision 59081) @@ -23,7 +23,7 @@ https://github.com/ruby/ruby/blob/trunk/ext/openssl/ossl_cipher.c#L23 #define GetCipher(obj, ctx) do { \ GetCipherInit((obj), (ctx)); \ if (!(ctx)) { \ - ossl_raise(rb_eRuntimeError, "Cipher not inititalized!"); \ + ossl_raise(rb_eRuntimeError, "Cipher not initialized!"); \ } \ } while (0) #define SafeGetCipher(obj, ctx) do { \ @@ -122,7 +122,7 @@ ossl_cipher_initialize(VALUE self, VALUE https://github.com/ruby/ruby/blob/trunk/ext/openssl/ossl_cipher.c#L122 name = StringValueCStr(str); GetCipherInit(self, ctx); if (ctx) { - ossl_raise(rb_eRuntimeError, "Cipher already inititalized!"); + ossl_raise(rb_eRuntimeError, "Cipher already initialized!"); } AllocCipher(self, ctx); if (!(cipher = EVP_get_cipherbyname(name))) { @@ -418,7 +418,7 @@ ossl_cipher_update(int argc, VALUE *argv https://github.com/ruby/ruby/blob/trunk/ext/openssl/ossl_cipher.c#L418 * Returns the remaining data held in the cipher object. Further calls to * Cipher#update or Cipher#final will return garbage. This call should always * be made as the last call of an encryption or decryption operation, after - * after having fed the entire plaintext or ciphertext to the Cipher instance. + * having fed the entire plaintext or ciphertext to the Cipher instance. * * If an authenticated cipher was used, a CipherError is raised if the tag * could not be authenticated successfully. Only call this method after @@ -1023,7 +1023,7 @@ Init_ossl_cipher(void) https://github.com/ruby/ruby/blob/trunk/ext/openssl/ossl_cipher.c#L1023 * An example using the GCM (Galois/Counter Mode). You have 16 bytes +key+, * 12 bytes (96 bits) +nonce+ and the associated data +auth_data+. Be sure * not to reuse the +key+ and +nonce+ pair. Reusing an nonce ruins the - * security gurantees of GCM mode. + * security guarantees of GCM mode. * * cipher = OpenSSL::Cipher::AES.new(128, :GCM).encrypt * cipher.key = key Index: ext/openssl/ossl_pkey_rsa.c =================================================================== --- ext/openssl/ossl_pkey_rsa.c (revision 59080) +++ ext/openssl/ossl_pkey_rsa.c (revision 59081) @@ -706,7 +706,7 @@ Init_ossl_rsa(void) https://github.com/ruby/ruby/blob/trunk/ext/openssl/ossl_pkey_rsa.c#L706 /* Document-class: OpenSSL::PKey::RSA * * RSA is an asymmetric public key algorithm that has been formalized in - * RFC 3447. It is in widespread use in public key infrastuctures (PKI) + * RFC 3447. It is in widespread use in public key infrastructures (PKI) * where certificates (cf. OpenSSL::X509::Certificate) often are issued * on the basis of a public/private RSA key pair. RSA is used in a wide * field of applications such as secure (symmetric) key exchange, e.g. Index: ext/openssl/ossl_pkey_ec.c =================================================================== --- ext/openssl/ossl_pkey_ec.c (revision 59080) +++ ext/openssl/ossl_pkey_ec.c (revision 59081) @@ -296,7 +296,7 @@ ossl_ec_key_get_group(VALUE self) https://github.com/ruby/ruby/blob/trunk/ext/openssl/ossl_pkey_ec.c#L296 * key.group = group * * Sets the EC::Group for the key. The group structure is internally copied so - * modifition to +group+ after assigning to a key has no effect on the key. + * modification to +group+ after assigning to a key has no effect on the key. */ static VALUE ossl_ec_key_set_group(VALUE self, VALUE group_v) @@ -1597,11 +1597,11 @@ ossl_ec_point_to_bn(int argc, VALUE *arg https://github.com/ruby/ruby/blob/trunk/ext/openssl/ossl_pkey_ec.c#L1597 * Performs elliptic curve point multiplication. * * The first form calculates <tt>bn1 * point + bn2 * G</tt>, where +G+ is the - * generator of the group of +point+. +bn2+ may be ommitted, and in that case, + * generator of the group of +point+. +bn2+ may be omitted, and in that case, * the result is just <tt>bn1 * point</tt>. * * The second form calculates <tt>bns[0] * point + bns[1] * points[0] + ... - * + bns[-1] * points[-1] + bn2 * G</tt>. +bn2+ may be ommitted. +bns+ must be + * + bns[-1] * points[-1] + bn2 * G</tt>. +bn2+ may be omitted. +bns+ must be * an array of OpenSSL::BN. +points+ must be an array of * OpenSSL::PKey::EC::Point. Please note that <tt>points[0]</tt> is not * multiplied by <tt>bns[0]</tt>, but <tt>bns[1]</tt>. -- ML: ruby-changes@q... Info: http://www.atdot.net/~ko1/quickml/