1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00

* 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!



git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@38488 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
This commit is contained in:
emboss 2012-12-20 06:03:03 +00:00
parent ec6cacf00a
commit 215b54806b
3 changed files with 361 additions and 8 deletions

View file

@ -1,3 +1,14 @@
Thu Dec 20 16:00:33 2012 Martin Bosslet <Martin.Bosslet@gmail.com>
* 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@segment7.net>
* lib/rdoc/markup/to_html.rb (class RDoc): Added current heading and

View file

@ -329,7 +329,6 @@ ossl_cipher_pkcs5_keyivgen(int argc, VALUE *argv, VALUE self)
return Qnil;
}
/*
* call-seq:
* cipher.update(data [, buffer]) -> string or buffer
@ -379,10 +378,15 @@ ossl_cipher_update(int argc, VALUE *argv, VALUE self)
* 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.
* 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.
*
* See EVP_CipherFinal_ex for further information.
* 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)
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)
*
* 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)
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);

View file

@ -3,6 +3,25 @@ require_relative 'utils'
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::TestCase
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::TestCase
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