ruby-changes:26437
From: emboss <ko1@a...>
Date: Thu, 20 Dec 2012 15:03:15 +0900 (JST)
Subject: [ruby-changes:26437] emboss:r38488 (trunk): * ext/openssl/ossl_cipher.c: add support for Authenticated Encryption
emboss 2012-12-20 15:03:03 +0900 (Thu, 20 Dec 2012) New Revision: 38488 http://svn.ruby-lang.org/cgi-bin/viewvc.cgi?view=rev&revision=38488 Log: * ext/openssl/ossl_cipher.c: add support for Authenticated Encryption with Associated Data (AEAD) for OpenSSL versions that support the GCM encryption mode. It's the only mode supported for now by OpenSSL itself. Add Cipher#authenticated? to detect whether a chosen mode does support Authenticated Encryption. * test/openssl/test_cipher.rb: add tests for Authenticated Encryption. [Feature #6980] [ruby-core:47426] Thank you, Stephen Touset for providing a patch! Modified files: trunk/ChangeLog trunk/ext/openssl/ossl_cipher.c trunk/test/openssl/test_cipher.rb Index: ChangeLog =================================================================== --- ChangeLog (revision 38487) +++ ChangeLog (revision 38488) @@ -1,3 +1,14 @@ https://github.com/ruby/ruby/blob/trunk/ChangeLog#L1 +Thu Dec 20 16:00:33 2012 Martin Bosslet <Martin.Bosslet@g...> + + * ext/openssl/ossl_cipher.c: add support for Authenticated Encryption + with Associated Data (AEAD) for OpenSSL versions that support the + GCM encryption mode. It's the only mode supported for now by OpenSSL + itself. Add Cipher#authenticated? to detect whether a chosen mode + does support Authenticated Encryption. + * test/openssl/test_cipher.rb: add tests for Authenticated Encryption. + [Feature #6980] [ruby-core:47426] Thank you, Stephen Touset for + providing a patch! + Thu Dec 20 12:56:53 2012 Eric Hodel <drbrain@s...> * lib/rdoc/markup/to_html.rb (class RDoc): Added current heading and Index: ext/openssl/ossl_cipher.c =================================================================== --- ext/openssl/ossl_cipher.c (revision 38487) +++ ext/openssl/ossl_cipher.c (revision 38488) @@ -329,7 +329,6 @@ ossl_cipher_pkcs5_keyivgen(int argc, VAL https://github.com/ruby/ruby/blob/trunk/ext/openssl/ossl_cipher.c#L329 return Qnil; } - /* * call-seq: * cipher.update(data [, buffer]) -> string or buffer @@ -379,10 +378,15 @@ ossl_cipher_update(int argc, VALUE *argv https://github.com/ruby/ruby/blob/trunk/ext/openssl/ossl_cipher.c#L378 * call-seq: * cipher.final -> string * - * Returns the remaining data held in the cipher object. Further calls to - * Cipher#update or Cipher#final will return garbage. - * - * See EVP_CipherFinal_ex for further information. + * 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. + * + * If an authenticated cipher was used, a CipherError is raised if the tag + * could not be authenticated successfully. Only call this method after + * setting the authentication tag and passing the entire contents of the + * ciphertext into the cipher. */ static VALUE ossl_cipher_final(VALUE self) @@ -478,6 +482,168 @@ ossl_cipher_set_iv(VALUE self, VALUE iv) https://github.com/ruby/ruby/blob/trunk/ext/openssl/ossl_cipher.c#L482 return iv; } +/* + * call-seq: + * cipher.auth_data = string -> string + * + * Sets the cipher's additional authenticated data. This field must be + * set when using AEAD cipher modes such as GCM or CCM. If no associated + * data shall be used, this method must *still* be called with a value of "". + * The contents of this field should be non-sensitive data which will be + * added to the ciphertext to generate the authentication tag which validates + * the contents of the ciphertext. + * + * The AAD must be set prior to encryption or decryption. In encryption mode, + * it must be set after calling Cipher#encrypt and setting Cipher#key= and + * Cipher#iv=. When decrypting, the authenticated data must be set after key, + * iv and especially *after* the authentication tag has been set. I.e. set it + * only after calling Cipher#decrypt, Cipher#key=, Cipher#iv= and + * Cipher#auth_tag= first. + */ +static VALUE +ossl_cipher_set_auth_data(VALUE self, VALUE data) +{ + EVP_CIPHER_CTX *ctx; + unsigned char *in; + int in_len; + int out_len; + + StringValue(data); + + in = (unsigned char *) RSTRING_PTR(data); + in_len = RSTRING_LENINT(data); + + GetCipher(self, ctx); + + if (!EVP_CipherUpdate(ctx, NULL, &out_len, in, in_len)) + ossl_raise(eCipherError, "couldn't set additional authenticated data"); + + return data; +} + +#define ossl_is_gcm(nid) (nid) == NID_aes_128_gcm || \ + (nid) == NID_aes_192_gcm || \ + (nid) == NID_aes_256_gcm + +static VALUE +ossl_get_gcm_auth_tag(EVP_CIPHER_CTX *ctx, int len) +{ + unsigned char *tag; + VALUE ret; + + tag = ALLOC_N(unsigned char, len); + + if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, len, tag)) + ossl_raise(eCipherError, "retrieving the authentication tag failed"); + + ret = rb_str_new((const char *) tag, len); + xfree(tag); + return ret; +} + +/* + * call-seq: + * cipher.auth_tag([ tag_len ] -> string + * + * Gets the authentication tag generated by Authenticated Encryption Cipher + * modes (GCM for example). This tag may be stored along with the ciphertext, + * then set on the decryption cipher to authenticate the contents of the + * ciphertext against changes. If the optional integer parameter +tag_len+ is + * given, the returned tag will be +tag_len+ bytes long. If the parameter is + * omitted, the maximum length of 16 bytes will be returned. For maximum + * security, the default of 16 bytes should be chosen. + * + * The tag may only be retrieved after calling Cipher#final. + */ +static VALUE +ossl_cipher_get_auth_tag(int argc, VALUE *argv, VALUE self) +{ + VALUE vtag_len; + EVP_CIPHER_CTX *ctx; + int nid, tag_len; + + if (rb_scan_args(argc, argv, "01", &vtag_len) == 0) { + tag_len = 16; + } else { + tag_len = NUM2INT(vtag_len); + } + + GetCipher(self, ctx); + nid = EVP_CIPHER_CTX_nid(ctx); + + if (ossl_is_gcm(nid)) { + return ossl_get_gcm_auth_tag(ctx, tag_len); + } else { + ossl_raise(eCipherError, "authentication tag not supported by this cipher"); + return Qnil; /* dummy */ + } +} + +static inline void +ossl_set_gcm_auth_tag(EVP_CIPHER_CTX *ctx, unsigned char *tag, int tag_len) +{ + if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, tag_len, tag)) + ossl_raise(eCipherError, "unable to set GCM tag"); +} + +/* + * call-seq: + * cipher.auth_tag = string -> string + * + * Sets the authentication tag to verify the contents of the + * ciphertext. The tag must be set after calling Cipher#decrypt, + * Cipher#key= and Cipher#iv=, but before assigning the associated + * authenticated data using Cipher#auth_data= and of course, before + * decrypting any of the ciphertext. After all decryption is + * performed, the tag is verified automatically in the call to + * Cipher#final. + */ +static VALUE +ossl_cipher_set_auth_tag(VALUE self, VALUE vtag) +{ + EVP_CIPHER_CTX *ctx; + int nid; + unsigned char *tag; + int tag_len; + + StringValue(vtag); + tag = (unsigned char *) RSTRING_PTR(vtag); + tag_len = RSTRING_LENINT(vtag); + + GetCipher(self, ctx); + nid = EVP_CIPHER_CTX_nid(ctx); + + if (ossl_is_gcm(nid)) { + ossl_set_gcm_auth_tag(ctx, tag, tag_len); + } else { + ossl_raise(eCipherError, "authentication tag not supported by this cipher"); + } + + return vtag; +} + +/* + * call-seq: + * cipher.authenticated? -> boolean + * + * Indicated whether this Cipher instance uses an Authenticated Encryption + * mode. + */ +static VALUE +ossl_cipher_is_authenticated(VALUE self) +{ + EVP_CIPHER_CTX *ctx; + int nid; + + GetCipher(self, ctx); + nid = EVP_CIPHER_CTX_nid(ctx); + + if (ossl_is_gcm(nid)) { + return Qtrue; + } else { + return Qfalse; + } +} /* * call-seq: @@ -728,6 +894,45 @@ Init_ossl_cipher(void) https://github.com/ruby/ruby/blob/trunk/ext/openssl/ossl_cipher.c#L894 * * puts data == plain #=> true * + * === Authenticated Encryption and Associated Data (AEAD) + * + * If the OpenSSL version used supports it, an Authenticated Encryption + * mode (such as GCM or CCM) should always be preferred over any + * unauthenticated mode. Currently, OpenSSL supports AE only in combination + * with Associated Data (AEAD) where additional associated data is included + * in the encryption process to compute a tag at the end of the encryption. + * This tag will also be used in the decryption process and by verifying + * its validity, the authenticity of a given ciphertext is established. + * + * This is superior to unauthenticated modes in that it allows to detect + * if somebody effectively changed the ciphertext after it had been + * encrypted. This prevents malicious modifications of the ciphertext that + * could otherwise be exploited to modify ciphertexts in ways beneficial to + * potential attackers. + * + * If no associated data is needed for encryption and later decryption, + * the OpenSSL library still requires a value to be set - "" may be used in + * case none is available. An example using the GCM (Galois Counter Mode): + * + * cipher = OpenSSL::Cipher::AES.new(128, :GCM) + * cipher.encrypt + * key = cipher.random_key + * iv = cipher.random_iv + * cipher.auth_data = "" + * + * encrypted = cipher.update(data) + cipher.final + * tag = cipher.auth_tag + * + * decipher = OpenSSL::Cipher::AES.new(128, :GCM) + * decipher.decrypt + * decipher.key = key + * decipher.iv = iv + * decipher.auth_tag = tag + * decipher.auth_data = "" + * + * plain = decipher.update(encrypted) + decipher.final + * + * puts data == plain #=> true */ cCipher = rb_define_class_under(mOSSL, "Cipher", rb_cObject); eCipherError = rb_define_class_under(cCipher, "CipherError", eOSSLError); @@ -744,6 +949,10 @@ Init_ossl_cipher(void) https://github.com/ruby/ruby/blob/trunk/ext/openssl/ossl_cipher.c#L949 rb_define_method(cCipher, "final", ossl_cipher_final, 0); rb_define_method(cCipher, "name", ossl_cipher_name, 0); rb_define_method(cCipher, "key=", ossl_cipher_set_key, 1); + rb_define_method(cCipher, "auth_data=", ossl_cipher_set_auth_data, 1); + rb_define_method(cCipher, "auth_tag=", ossl_cipher_set_auth_tag, 1); + rb_define_method(cCipher, "auth_tag", ossl_cipher_get_auth_tag, -1); + rb_define_method(cCipher, "authenticated?", ossl_cipher_is_authenticated, 0); rb_define_method(cCipher, "key_len=", ossl_cipher_set_key_length, 1); rb_define_method(cCipher, "key_len", ossl_cipher_key_length, 0); rb_define_method(cCipher, "iv=", ossl_cipher_set_iv, 1); Index: test/openssl/test_cipher.rb =================================================================== --- test/openssl/test_cipher.rb (revision 38487) +++ test/openssl/test_cipher.rb (revision 38488) @@ -3,6 +3,25 @@ require_relative 'utils' https://github.com/ruby/ruby/blob/trunk/test/openssl/test_cipher.rb#L3 if defined?(OpenSSL) class OpenSSL::TestCipher < Test::Unit::TestCase + + class << self + + def has_cipher?(name) + ciphers = OpenSSL::Cipher.ciphers + # redefine method so we can use the cached ciphers value from the closure + # and need not recompute the list each time + define_singleton_method :has_cipher? do |name| + ciphers.include?(name) + end + has_cipher?(name) + end + + def has_ciphers?(list) + list.all? { |name| has_cipher?(name) } + end + + end + def setup @c1 = OpenSSL::Cipher::Cipher.new("DES-EDE3-CBC") @c2 = OpenSSL::Cipher::DES.new(:EDE3, "CBC") @@ -78,11 +97,8 @@ class OpenSSL::TestCipher < Test::Unit:: https://github.com/ruby/ruby/blob/trunk/test/openssl/test_cipher.rb#L97 cipher.decrypt cipher.pkcs5_keyivgen('password') assert_equal('hello,world', cipher.update(c) + cipher.final) - rescue RuntimeError => e - # CTR is from OpenSSL 1.0.1, and for an environment that disables CTR; No idea it exists. - assert_match(/unsupported cipher algorithm/, e.message) end - end + end if has_cipher?('aes-128-ctr') if OpenSSL::OPENSSL_VERSION_NUMBER > 0x00907000 def test_ciphers @@ -116,6 +132,123 @@ class OpenSSL::TestCipher < Test::Unit:: https://github.com/ruby/ruby/blob/trunk/test/openssl/test_cipher.rb#L132 end end end + + if has_ciphers?(['aes-128-gcm', 'aes-192-gcm', 'aes-128-gcm']) + + def test_authenticated + cipher = OpenSSL::Cipher.new('aes-128-gcm') + assert(cipher.authenticated?) + cipher = OpenSSL::Cipher.new('aes-128-cbc') + refute(cipher.authenticated?) + end + + def test_aes_gcm + ['aes-128-gcm', 'aes-192-gcm', 'aes-128-gcm'].each do |algo| + pt = "You should all use Authenticated Encryption!" + cipher, key, iv = new_encryptor(algo) + + cipher.auth_data = "aad" + ct = cipher.update(pt) + cipher.final + tag = cipher.auth_tag + assert_equal(16, tag.size) + + decipher = new_decryptor(algo, key, iv) + decipher.auth_tag = tag + decipher.auth_data = "aad" + + assert_equal(pt, decipher.update(ct) + decipher.final) + end + end + + def test_aes_gcm_short_tag + ['aes-128-gcm', 'aes-192-gcm', 'aes-128-gcm'].each do |algo| + pt = "You should all use Authenticated Encryption!" + cipher, key, iv = new_encryptor(algo) + + cipher.auth_data = "aad" + ct = cipher.update(pt) + cipher.final + tag = cipher.auth_tag(8) + assert_equal(8, tag.size) + + decipher = new_decryptor(algo, key, iv) + decipher.auth_tag = tag + decipher.auth_data = "aad" + + assert_equal(pt, decipher.update(ct) + decipher.final) + end + end + + def test_aes_gcm_wrong_tag + pt = "You should all use Authenticated Encryption!" + cipher, key, iv = new_encryptor('aes-128-gcm') + + cipher.auth_data = "aad" + ct = cipher.update(pt) + cipher.final + tag = cipher.auth_tag + + decipher = new_decryptor('aes-128-gcm', key, iv) + decipher.auth_tag = tag[0..-2] << tag[-1].succ + decipher.auth_data = "aad" + + assert_raise OpenSSL::Cipher::CipherError do + decipher.update(ct) + decipher.final + end + end + + def test_aes_gcm_wrong_auth_data + pt = "You should all use Authenticated Encryption!" + cipher, key, iv = new_encryptor('aes-128-gcm') + + cipher.auth_data = "aad" + ct = cipher.update(pt) + cipher.final + tag = cipher.auth_tag + + decipher = new_decryptor('aes-128-gcm', key, iv) + decipher.auth_tag = tag + decipher.auth_data = "daa" + + assert_raise OpenSSL::Cipher::CipherError do + decipher.update(ct) + decipher.final + end + end + + def test_aes_gcm_wrong_ciphertext + pt = "You should all use Authenticated Encryption!" + cipher, key, iv = new_encryptor('aes-128-gcm') + + cipher.auth_data = "aad" + ct = cipher.update(pt) + cipher.final + tag = cipher.auth_tag + + decipher = new_decryptor('aes-128-gcm', key, iv) + decipher.auth_tag = tag + decipher.auth_data = "aad" + + assert_raise OpenSSL::Cipher::CipherError do + decipher.update(ct[0..-2] << ct[-1].succ) + decipher.final + end + end + + end + + private + + def new_encryptor(algo) + cipher = OpenSSL::Cipher.new(algo) + cipher.encrypt + key = cipher.random_key + iv = cipher.random_iv + [cipher, key, iv] + end + + def new_decryptor(algo, key, iv) + OpenSSL::Cipher.new(algo).tap do |cipher| + cipher.decrypt + cipher.key = key + cipher.iv = iv + end + end + end end -- ML: ruby-changes@q... Info: http://www.atdot.net/~ko1/quickml/