From 215b54806b035509272779fefd57b432a1a0f9e5 Mon Sep 17 00:00:00 2001 From: emboss Date: Thu, 20 Dec 2012 06:03:03 +0000 Subject: * 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 --- ext/openssl/ossl_cipher.c | 217 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 213 insertions(+), 4 deletions(-) (limited to 'ext/openssl/ossl_cipher.c') diff --git a/ext/openssl/ossl_cipher.c b/ext/openssl/ossl_cipher.c index 26851515cd..03e89fe420 100644 --- a/ext/openssl/ossl_cipher.c +++ b/ext/openssl/ossl_cipher.c @@ -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); -- cgit v1.2.3