[RFC 1/4] util: Add support for GnuTLS decryption

Arun Menon via Devel posted 4 patches 1 week ago
There is a newer version of this series
[RFC 1/4] util: Add support for GnuTLS decryption
Posted by Arun Menon via Devel 1 week ago
Adds `virCryptoDecryptDataAESgnutls` and `virCryptoDecryptData`
as wrapper functions for GnuTLS decryption.

These functions are the inverse of the existing GnuTLS encryption wrappers.
This commit also includes a corresponding test case to validate data decryption.

Signed-off-by: Arun Menon <armenon@redhat.com>
---
 src/libvirt_private.syms |   1 +
 src/util/vircrypto.c     | 130 ++++++++++++++++++++++++++++++++++++++-
 src/util/vircrypto.h     |   8 +++
 tests/vircryptotest.c    |  65 ++++++++++++++++++++
 4 files changed, 202 insertions(+), 2 deletions(-)

diff --git a/src/libvirt_private.syms b/src/libvirt_private.syms
index fb482fff40..fc5fdb00f4 100644
--- a/src/libvirt_private.syms
+++ b/src/libvirt_private.syms
@@ -2252,6 +2252,7 @@ virConfWriteMem;
 
 
 # util/vircrypto.h
+virCryptoDecryptData;
 virCryptoEncryptData;
 virCryptoHashBuf;
 virCryptoHashString;
diff --git a/src/util/vircrypto.c b/src/util/vircrypto.c
index 3ce23264ca..e0d2b794a1 100644
--- a/src/util/vircrypto.c
+++ b/src/util/vircrypto.c
@@ -98,7 +98,7 @@ virCryptoHashString(virCryptoHash hash,
 }
 
 
-/* virCryptoEncryptDataAESgntuls:
+/* virCryptoEncryptDataAESgnutls:
  *
  * Performs the AES gnutls encryption
  *
@@ -200,7 +200,7 @@ virCryptoEncryptData(virCryptoCipher algorithm,
 {
     switch (algorithm) {
     case VIR_CRYPTO_CIPHER_AES256CBC:
-        if (enckeylen != 32) {
+        if (enckeylen < 32) {
             virReportError(VIR_ERR_INVALID_ARG,
                            _("AES256CBC encryption invalid keylen=%1$zu"),
                            enckeylen);
@@ -233,3 +233,129 @@ virCryptoEncryptData(virCryptoCipher algorithm,
                    _("algorithm=%1$d is not supported"), algorithm);
     return -1;
 }
+
+/* virCryptoDecryptDataAESgnutls:
+ *
+ * Performs the AES gnutls decryption
+ *
+ * Same input as virCryptoDecryptData, except the algorithm is replaced
+ * by the specific gnutls algorithm.
+ *
+ * Decrypts the @data buffer using the @deckey and if available the @iv
+ *
+ * Returns 0 on success with the plaintext being filled. It is the
+ * caller's responsibility to clear and free it. Returns -1 on failure
+ * w/ error set.
+ */
+static int
+virCryptoDecryptDataAESgnutls(gnutls_cipher_algorithm_t gnutls_dec_alg,
+                              uint8_t *deckey,
+                              size_t deckeylen,
+                              uint8_t *iv,
+                              size_t ivlen,
+                              uint8_t *data,
+                              size_t datalen,
+                              uint8_t **plaintextret,
+                              size_t *plaintextlenret)
+{
+    int rc;
+    size_t i;
+    gnutls_cipher_hd_t handle = NULL;
+    gnutls_datum_t dec_key = { .data = deckey, .size = deckeylen };
+    gnutls_datum_t iv_buf = { .data = iv, .size = ivlen };
+    g_autofree uint8_t *plaintext = NULL;
+    size_t plaintextlen;
+
+    if ((rc = gnutls_cipher_init(&handle, gnutls_dec_alg,
+                                 &dec_key, &iv_buf)) < 0) {
+        virReportError(VIR_ERR_INTERNAL_ERROR,
+                       _("failed to initialize cipher: '%1$s'"),
+                       gnutls_strerror(rc));
+        return -1;
+    }
+
+    plaintext = g_memdup2(data, datalen);
+    plaintextlen = datalen;
+
+    rc = gnutls_cipher_decrypt(handle, plaintext, plaintextlen);
+    gnutls_cipher_deinit(handle);
+    if (rc < 0) {
+        virSecureErase(plaintext, plaintextlen);
+        virReportError(VIR_ERR_INTERNAL_ERROR,
+                       _("failed to decrypt the data: '%1$s'"),
+                       gnutls_strerror(rc));
+        return -1;
+    }
+    if (plaintextlen == 0) {
+        virSecureErase(plaintext, plaintextlen);
+        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                       _("decrypted data has zero length"));
+        return -1;
+    }
+    i = plaintext[plaintextlen - 1];
+    if (i > plaintextlen) {
+        virSecureErase(plaintext, plaintextlen);
+        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                       _("decrypted data has invalid padding"));
+        return -1;
+    }
+    *plaintextlenret = plaintextlen - i;
+    *plaintextret = g_steal_pointer(&plaintext);
+    return 0;
+}
+
+/* virCryptoDecryptData:
+    * @algorithm: algorithm desired for decryption
+    * @deckey: decryption key
+    * @deckeylen: decryption key length
+    * @iv: initialization vector
+    * @ivlen: length of initialization vector
+    * @data: data to decrypt
+    * @datalen: length of data
+    * @plaintext: stream of bytes allocated to store plaintext
+    * @plaintextlen: size of the stream of bytes
+    * Returns 0 on success, -1 on failure with error set
+    */
+int
+virCryptoDecryptData(virCryptoCipher algorithm,
+                     uint8_t *deckey,
+                     size_t deckeylen,
+                     uint8_t *iv,
+                     size_t ivlen,
+                     uint8_t *data,
+                     size_t datalen,
+                     uint8_t **plaintext,
+                     size_t *plaintextlen)
+{
+    switch (algorithm) {
+    case VIR_CRYPTO_CIPHER_AES256CBC:
+        if (deckeylen < 32) {
+            virReportError(VIR_ERR_INVALID_ARG,
+                            _("AES256CBC decryption invalid keylen=%1$zu"),
+                            deckeylen);
+            return -1;
+        }
+        if (ivlen != 16) {
+            virReportError(VIR_ERR_INVALID_ARG,
+                            _("AES256CBC initialization vector invalid len=%1$zu"),
+                            ivlen);
+            return -1;
+        }
+        /*
+         * Decrypt the data buffer using a decryption key and
+         * initialization vector via the gnutls_cipher_decrypt API
+         * for GNUTLS_CIPHER_AES_256_CBC.
+         */
+        return virCryptoDecryptDataAESgnutls(GNUTLS_CIPHER_AES_256_CBC,
+                                             deckey, deckeylen, iv, ivlen,
+                                             data, datalen,
+                                             plaintext, plaintextlen);
+    case VIR_CRYPTO_CIPHER_NONE:
+    case VIR_CRYPTO_CIPHER_LAST:
+        break;
+    }
+
+    virReportError(VIR_ERR_INVALID_ARG,
+                    _("algorithm=%1$d is not supported"), algorithm);
+    return -1;
+}
diff --git a/src/util/vircrypto.h b/src/util/vircrypto.h
index 5f079ac335..2e8557839d 100644
--- a/src/util/vircrypto.h
+++ b/src/util/vircrypto.h
@@ -61,3 +61,11 @@ int virCryptoEncryptData(virCryptoCipher algorithm,
                          uint8_t **ciphertext, size_t *ciphertextlen)
     ATTRIBUTE_NONNULL(2) ATTRIBUTE_NONNULL(6)
     ATTRIBUTE_NONNULL(8) ATTRIBUTE_NONNULL(9) G_GNUC_WARN_UNUSED_RESULT;
+
+int virCryptoDecryptData(virCryptoCipher algorithm,
+                         uint8_t *deckey, size_t deckeylen,
+                         uint8_t *iv, size_t ivlen,
+                         uint8_t *data, size_t datalen,
+                         uint8_t **plaintext, size_t *plaintextlen)
+    ATTRIBUTE_NONNULL(2) ATTRIBUTE_NONNULL(6)
+    ATTRIBUTE_NONNULL(8) ATTRIBUTE_NONNULL(9) G_GNUC_WARN_UNUSED_RESULT;
diff --git a/tests/vircryptotest.c b/tests/vircryptotest.c
index 9ffe70756e..864fa8838d 100644
--- a/tests/vircryptotest.c
+++ b/tests/vircryptotest.c
@@ -62,6 +62,14 @@ struct testCryptoEncryptData {
     size_t ciphertextlen;
 };
 
+struct testCryptoDecryptData {
+    virCryptoCipher algorithm;
+    uint8_t *input;
+    size_t inputlen;
+    uint8_t *plaintext;
+    size_t plaintextlen;
+};
+
 static int
 testCryptoEncrypt(const void *opaque)
 {
@@ -101,6 +109,44 @@ testCryptoEncrypt(const void *opaque)
     return 0;
 }
 
+static int
+testCryptoDecrypt(const void *opaque)
+{
+    const struct testCryptoDecryptData *data = opaque;
+    g_autofree uint8_t *deckey = NULL;
+    size_t deckeylen = 32;
+    g_autofree uint8_t *iv = NULL;
+    size_t ivlen = 16;
+    g_autofree uint8_t *plaintext = NULL;
+    size_t plaintextlen = 0;
+
+    deckey = g_new0(uint8_t, deckeylen);
+    iv = g_new0(uint8_t, ivlen);
+
+    if (virRandomBytes(deckey, deckeylen) < 0 ||
+        virRandomBytes(iv, ivlen) < 0) {
+        fprintf(stderr, "Failed to generate random bytes\n");
+        return -1;
+    }
+
+    if (virCryptoDecryptData(data->algorithm, deckey, deckeylen, iv, ivlen,
+                             data->input, data->inputlen,
+                             &plaintext, &plaintextlen) < 0)
+        return -1;
+
+    if (data->plaintextlen != plaintextlen) {
+        fprintf(stderr, "Expected plaintexlen(%zu) doesn't match (%zu)\n",
+                data->plaintextlen, plaintextlen);
+        return -1;
+    }
+
+    if (memcmp(data->plaintext, plaintext, plaintextlen)) {
+        fprintf(stderr, "Expected plaintext doesn't match\n");
+        return -1;
+    }
+
+    return 0;
+}
 
 static int
 mymain(void)
@@ -155,7 +201,26 @@ mymain(void)
 
 #undef VIR_CRYPTO_ENCRYPT
 
+#define VIR_CRYPTO_DECRYPT(a, n, i, il, c, cl) \
+    do { \
+        struct testCryptoDecryptData data = { \
+            .algorithm = a, \
+            .input = i, \
+            .inputlen = il, \
+            .plaintext = c, \
+            .plaintextlen = cl, \
+        }; \
+        if (virTestRun("Decrypt " n, testCryptoDecrypt, &data) < 0) \
+            ret = -1; \
+    } while (0)
+
+    VIR_CRYPTO_DECRYPT(VIR_CRYPTO_CIPHER_AES256CBC, "aes256cbc",
+                       expected_ciphertext, 16, secretdata, 7);
+
+#undef VIR_CRYPTO_DECRYPT
+
     return ret == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
+
 }
 
 /* Forces usage of not so random virRandomBytes */
-- 
2.51.1
Re: [RFC 1/4] util: Add support for GnuTLS decryption
Posted by Daniel P. Berrangé via Devel 6 days, 21 hours ago
On Thu, Nov 13, 2025 at 07:02:20PM +0530, Arun Menon wrote:
> Adds `virCryptoDecryptDataAESgnutls` and `virCryptoDecryptData`
> as wrapper functions for GnuTLS decryption.
> 
> These functions are the inverse of the existing GnuTLS encryption wrappers.
> This commit also includes a corresponding test case to validate data decryption.
> 
> Signed-off-by: Arun Menon <armenon@redhat.com>
> ---
>  src/libvirt_private.syms |   1 +
>  src/util/vircrypto.c     | 130 ++++++++++++++++++++++++++++++++++++++-
>  src/util/vircrypto.h     |   8 +++
>  tests/vircryptotest.c    |  65 ++++++++++++++++++++
>  4 files changed, 202 insertions(+), 2 deletions(-)
> 
> diff --git a/src/libvirt_private.syms b/src/libvirt_private.syms
> index fb482fff40..fc5fdb00f4 100644
> --- a/src/libvirt_private.syms
> +++ b/src/libvirt_private.syms
> @@ -2252,6 +2252,7 @@ virConfWriteMem;
>  
>  
>  # util/vircrypto.h
> +virCryptoDecryptData;
>  virCryptoEncryptData;
>  virCryptoHashBuf;
>  virCryptoHashString;
> diff --git a/src/util/vircrypto.c b/src/util/vircrypto.c
> index 3ce23264ca..e0d2b794a1 100644
> --- a/src/util/vircrypto.c
> +++ b/src/util/vircrypto.c
> @@ -98,7 +98,7 @@ virCryptoHashString(virCryptoHash hash,
>  }
>  
>  
> -/* virCryptoEncryptDataAESgntuls:
> +/* virCryptoEncryptDataAESgnutls:
>   *
>   * Performs the AES gnutls encryption
>   *
> @@ -200,7 +200,7 @@ virCryptoEncryptData(virCryptoCipher algorithm,
>  {
>      switch (algorithm) {
>      case VIR_CRYPTO_CIPHER_AES256CBC:
> -        if (enckeylen != 32) {
> +        if (enckeylen < 32) {

Why change this ?   For AES-256 the key *must* be exactly
32 bytes, no more, no less.

>              virReportError(VIR_ERR_INVALID_ARG,
>                             _("AES256CBC encryption invalid keylen=%1$zu"),
>                             enckeylen);
> @@ -233,3 +233,129 @@ virCryptoEncryptData(virCryptoCipher algorithm,
>                     _("algorithm=%1$d is not supported"), algorithm);
>      return -1;

>  }
> +
> +/* virCryptoDecryptDataAESgnutls:
> + *
> + * Performs the AES gnutls decryption
> + *
> + * Same input as virCryptoDecryptData, except the algorithm is replaced
> + * by the specific gnutls algorithm.
> + *
> + * Decrypts the @data buffer using the @deckey and if available the @iv
> + *
> + * Returns 0 on success with the plaintext being filled. It is the
> + * caller's responsibility to clear and free it. Returns -1 on failure
> + * w/ error set.
> + */
> +static int
> +virCryptoDecryptDataAESgnutls(gnutls_cipher_algorithm_t gnutls_dec_alg,
> +                              uint8_t *deckey,
> +                              size_t deckeylen,
> +                              uint8_t *iv,
> +                              size_t ivlen,
> +                              uint8_t *data,
> +                              size_t datalen,
> +                              uint8_t **plaintextret,
> +                              size_t *plaintextlenret)
> +{
> +    int rc;
> +    size_t i;
> +    gnutls_cipher_hd_t handle = NULL;
> +    gnutls_datum_t dec_key = { .data = deckey, .size = deckeylen };
> +    gnutls_datum_t iv_buf = { .data = iv, .size = ivlen };
> +    g_autofree uint8_t *plaintext = NULL;
> +    size_t plaintextlen;
> +
> +    if ((rc = gnutls_cipher_init(&handle, gnutls_dec_alg,
> +                                 &dec_key, &iv_buf)) < 0) {
> +        virReportError(VIR_ERR_INTERNAL_ERROR,
> +                       _("failed to initialize cipher: '%1$s'"),
> +                       gnutls_strerror(rc));
> +        return -1;
> +    }
> +
> +    plaintext = g_memdup2(data, datalen);
> +    plaintextlen = datalen;
> +
> +    rc = gnutls_cipher_decrypt(handle, plaintext, plaintextlen);
> +    gnutls_cipher_deinit(handle);
> +    if (rc < 0) {
> +        virSecureErase(plaintext, plaintextlen);

Instead of repeating virSecureErase many times over, create
an 'error:' label and jump to that after the virReportError
call.

> +        virReportError(VIR_ERR_INTERNAL_ERROR,
> +                       _("failed to decrypt the data: '%1$s'"),
> +                       gnutls_strerror(rc));
> +        return -1;
> +    }
> +    if (plaintextlen == 0) {
> +        virSecureErase(plaintext, plaintextlen);
> +        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
> +                       _("decrypted data has zero length"));
> +        return -1;
> +    }
> +    i = plaintext[plaintextlen - 1];
> +    if (i > plaintextlen) {

Is that check right ?  I thought that padding would be in the
range 0 -> key length. IOW, 0 through 31 inclusive for AES256 ? 

> +        virSecureErase(plaintext, plaintextlen);
> +        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
> +                       _("decrypted data has invalid padding"));
> +        return -1;
> +    }
> +    *plaintextlenret = plaintextlen - i;
> +    *plaintextret = g_steal_pointer(&plaintext);
> +    return 0;
> +}

> +int
> +virCryptoDecryptData(virCryptoCipher algorithm,
> +                     uint8_t *deckey,
> +                     size_t deckeylen,
> +                     uint8_t *iv,
> +                     size_t ivlen,
> +                     uint8_t *data,
> +                     size_t datalen,
> +                     uint8_t **plaintext,
> +                     size_t *plaintextlen)
> +{
> +    switch (algorithm) {
> +    case VIR_CRYPTO_CIPHER_AES256CBC:
> +        if (deckeylen < 32) {

Again, why < instead of != ?

> +            virReportError(VIR_ERR_INVALID_ARG,
> +                            _("AES256CBC decryption invalid keylen=%1$zu"),
> +                            deckeylen);
> +            return -1;
> +        }
> +        if (ivlen != 16) {
> +            virReportError(VIR_ERR_INVALID_ARG,
> +                            _("AES256CBC initialization vector invalid len=%1$zu"),
> +                            ivlen);
> +            return -1;
> +        }
> +        /*
> +         * Decrypt the data buffer using a decryption key and
> +         * initialization vector via the gnutls_cipher_decrypt API
> +         * for GNUTLS_CIPHER_AES_256_CBC.
> +         */
> +        return virCryptoDecryptDataAESgnutls(GNUTLS_CIPHER_AES_256_CBC,
> +                                             deckey, deckeylen, iv, ivlen,
> +                                             data, datalen,
> +                                             plaintext, plaintextlen);
> +    case VIR_CRYPTO_CIPHER_NONE:
> +    case VIR_CRYPTO_CIPHER_LAST:
> +        break;
> +    }
> +
> +    virReportError(VIR_ERR_INVALID_ARG,
> +                    _("algorithm=%1$d is not supported"), algorithm);
> +    return -1;

With regards,
Daniel
-- 
|: https://berrange.com      -o-    https://www.flickr.com/photos/dberrange :|
|: https://libvirt.org         -o-            https://fstop138.berrange.com :|
|: https://entangle-photo.org    -o-    https://www.instagram.com/dberrange :|