[前][次][番号順一覧][スレッド一覧]

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/

[前][次][番号順一覧][スレッド一覧]