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:
parent
ec6cacf00a
commit
215b54806b
3 changed files with 361 additions and 8 deletions
11
ChangeLog
11
ChangeLog
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue