diff --git a/ext/openssl/ossl_cipher.c b/ext/openssl/ossl_cipher.c index 2685151..7119285 100644 --- a/ext/openssl/ossl_cipher.c +++ b/ext/openssl/ossl_cipher.c @@ -403,6 +403,35 @@ ossl_cipher_final(VALUE self) /* * call-seq: + * cipher.verify -> string + * + * Verifies the decrypted ciphertext against the tag set prior to + * decryption using the ciphertext and optional associated + * authentication data. Returns an empty string if the ciphertext was + * authenticated successfully, otherwise raises an + * OpenSSL::Cipher::CipherError. + * + * Only call this method after setting the authentication tag + * appropriate for your cipher mode and passing the entire contents + * of the ciphertext into the cipher, and before calling + * Cipher#final. + */ +static VALUE +ossl_cipher_verify(VALUE self) +{ + EVP_CIPHER_CTX *ctx; + int out_len = 0; + + GetCipher(self, ctx); + + if (!EVP_CipherUpdate(ctx, NULL, &out_len, NULL, 0)) + ossl_raise(eCipherError, "ciphertext failed to authenticate"); + + return rb_str_new(0, 0); +} + +/* + * call-seq: * cipher.name -> string * * Returns the name of the cipher which may differ slightly from the original @@ -478,6 +507,97 @@ ossl_cipher_set_iv(VALUE self, VALUE iv) return iv; } +/* + * call-seq: + * cipher.aad = string -> string + * + * Sets the cipher's additional authenticated data. This field may be + * set optionally before using AEAD cipher modes such as GCM or + * CCM. 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. Only call + * this method after calling Cipher#encrypt when encrypting, and + * after Cipher#decrypt and Cipher#gcm_tag= when decrypting. + */ +static VALUE +ossl_cipher_set_aad(VALUE self, VALUE data) +{ + EVP_CIPHER_CTX *ctx; + unsigned char *in = NULL; + int in_len = 0; + int out_len = 0; + + 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 self; +} + +/* + * call-seq: + * cipher.gcm_tag -> string + * + * Gets the authentication tag generated by GCM cipher modes. This + * tag may be stored along with the ciphertext, then set on the + * decryption cipher to authenticate the contents of the ciphertext + * against changes. + * + * The tag may only be retrieved after calling Cipher#final. + */ +static VALUE +ossl_cipher_get_gcm_tag(VALUE self) +{ + EVP_CIPHER_CTX *ctx; + VALUE tag; + + // GCM tags are 16 bytes in OpenSSL + tag = rb_str_new(NULL, 16); + + GetCipher(self, ctx); + + if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, (unsigned char *)RSTRING_PTR(tag))) + ossl_raise(eCipherError, "Cipher#finish must be called before getting the tag"); + + return tag; +} + +/* + * call-seq: + * cipher.gcm_tag = string -> string + * + * Sets the authentication tag to verify the contents of the + * ciphertext. The GCM tag must be set after calling Cipher#decrypt + * but before decrypting any of the ciphertext. After all decryption + * is performed, the tag can be verified by calling Cipher#verify. + */ +static VALUE +ossl_cipher_set_gcm_tag(VALUE self, VALUE data) +{ + EVP_CIPHER_CTX *ctx; + unsigned char *in = NULL; + int in_len = 0; + + StringValue(data); + + in = (unsigned char *) RSTRING_PTR(data); + in_len = RSTRING_LENINT(data); + + GetCipher(self, ctx); + + if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, in_len, in)) + ossl_raise(eCipherError, "unable to set GCM tag"); + + return data; +} /* * call-seq: @@ -742,8 +862,12 @@ Init_ossl_cipher(void) rb_define_method(cCipher, "pkcs5_keyivgen", ossl_cipher_pkcs5_keyivgen, -1); rb_define_method(cCipher, "update", ossl_cipher_update, -1); rb_define_method(cCipher, "final", ossl_cipher_final, 0); + rb_define_method(cCipher, "verify", ossl_cipher_verify, 0); rb_define_method(cCipher, "name", ossl_cipher_name, 0); rb_define_method(cCipher, "key=", ossl_cipher_set_key, 1); + rb_define_method(cCipher, "aad=", ossl_cipher_set_aad, 1); + rb_define_method(cCipher, "gcm_tag=", ossl_cipher_set_gcm_tag, 1); + rb_define_method(cCipher, "gcm_tag", ossl_cipher_get_gcm_tag, 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); diff --git a/test/openssl/test_cipher.rb b/test/openssl/test_cipher.rb index 0e4a775..8620be3 100644 --- a/test/openssl/test_cipher.rb +++ b/test/openssl/test_cipher.rb @@ -101,6 +101,38 @@ class OpenSSL::TestCipher < Test::Unit::TestCase end end end + + if OpenSSL::OPENSSL_VERSION_NUMBER > 0x1000103f + def test_aes_gcm + pt = File.read(__FILE__) + c1 = OpenSSL::Cipher.new('aes-256-gcm') + c2 = OpenSSL::Cipher.new('aes-256-gcm') + c3 = OpenSSL::Cipher.new('aes-256-gcm') + + c1.encrypt + c1.pkcs5_keyivgen('passwd') + c1.aad = 'aad' + + ct = c1.update(pt) + c1.final + tag = c1.gcm_tag + + c2.decrypt + c2.pkcs5_keyivgen('passwd') + c2.gcm_tag = tag + c2.aad = 'aad' + + assert_equal(pt, c2.update(ct) + c2.verify + c2.final) + + c3.decrypt + c3.pkcs5_keyivgen('passwd') + c3.gcm_tag = tag[0..-2] << tag[-1].succ + c3.aad = 'aad' + + assert_raise OpenSSL::Cipher::CipherError do + c3.update(ct) + c3.verify + c3.final + end + end + end end end