diff --git a/configure.ac b/configure.ac index 538ff3456e..9bcc60dc30 100644 --- a/configure.ac +++ b/configure.ac @@ -457,6 +457,10 @@ if test x$with_openssl != xno || test x$with_ed25519_libsodium != xno; then OSTREE_FEATURES="$OSTREE_FEATURES sign-ed25519" fi +if test x$with_openssl != xno; then + OSTREE_FEATURES="$OSTREE_FEATURES sign-pkcs8" +fi + dnl begin gnutls; in contrast to openssl this one only dnl supports --with-crypto=gnutls GNUTLS_DEPENDENCY="gnutls >= 3.5.0" diff --git a/src/libostree/ostree-sign-ed25519.c b/src/libostree/ostree-sign-ed25519.c index e0e3ac4613..b916b048a8 100644 --- a/src/libostree/ostree-sign-ed25519.c +++ b/src/libostree/ostree-sign-ed25519.c @@ -48,9 +48,9 @@ struct _OstreeSignEd25519 { GObject parent; ed25519_state state; - guchar *secret_key; /* malloc'd buffer of length OSTREE_SIGN_ED25519_SECKEY_SIZE */ - GList *public_keys; /* malloc'd buffer of length OSTREE_SIGN_ED25519_PUBKEY_SIZE */ - GList *revoked_keys; /* malloc'd buffer of length OSTREE_SIGN_ED25519_PUBKEY_SIZE */ + GBytes *secret_key; + GList *public_keys; /* GBytes */ + GList *revoked_keys; /* GBytes */ }; static void ostree_sign_ed25519_iface_init (OstreeSignInterface *self); @@ -96,6 +96,9 @@ _ostree_sign_ed25519_init (OstreeSignEd25519 *self) #endif } +#if defined(USE_LIBSODIUM) +// Strictly verify pubkey and signature lengths, as libsodium can +// only handle raw ed25519 public key and signatures. static gboolean validate_length (gsize found, gsize expected, GError **error) { @@ -105,6 +108,7 @@ validate_length (gsize found, gsize expected, GError **error) error, "Ill-formed input: expected %" G_GSIZE_FORMAT " bytes, got %" G_GSIZE_FORMAT " bytes", expected, found); } +#endif static gboolean _ostree_sign_ed25519_is_initialized (OstreeSignEd25519 *self, GError **error) @@ -136,19 +140,33 @@ ostree_sign_ed25519_data (OstreeSign *self, GBytes *data, GBytes **signature, if (sign->secret_key == NULL) return glnx_throw (error, "Not able to sign: secret key is not set"); +#if defined(USE_LIBSODIUM) || defined(USE_OPENSSL) + gsize secret_key_size; + const guint8 *secret_key_buf = g_bytes_get_data (sign->secret_key, &secret_key_size); +#endif + unsigned long long sig_size = 0; - g_autofree guchar *sig = g_malloc0 (OSTREE_SIGN_ED25519_SIG_SIZE); + g_autofree guchar *sig = NULL; #if defined(USE_LIBSODIUM) + sig = g_malloc0 (OSTREE_SIGN_ED25519_SIG_SIZE); if (crypto_sign_detached (sig, &sig_size, g_bytes_get_data (data, NULL), g_bytes_get_size (data), - sign->secret_key)) + secret_key_buf)) sig_size = 0; #elif defined(USE_OPENSSL) EVP_MD_CTX *ctx = EVP_MD_CTX_new (); if (!ctx) return glnx_throw (error, "openssl: failed to allocate context"); - EVP_PKEY *pkey = EVP_PKEY_new_raw_private_key (EVP_PKEY_ED25519, NULL, sign->secret_key, - OSTREE_SIGN_ED25519_SEED_SIZE); + + // Try PKCS8 encoded private key first. + const unsigned char *p = secret_key_buf; + EVP_PKEY *pkey = d2i_AutoPrivateKey (NULL, &p, secret_key_size); + + // Try raw ed25519 private key if the length matches. + if (pkey == NULL && secret_key_size == OSTREE_SIGN_ED25519_SECKEY_SIZE) + pkey = EVP_PKEY_new_raw_private_key (EVP_PKEY_ED25519, NULL, secret_key_buf, + OSTREE_SIGN_ED25519_SEED_SIZE); + if (!pkey) { EVP_MD_CTX_free (ctx); @@ -157,6 +175,8 @@ ostree_sign_ed25519_data (OstreeSign *self, GBytes *data, GBytes **signature, size_t len; if (EVP_DigestSignInit (ctx, NULL, NULL, NULL, pkey) + && EVP_DigestSign (ctx, NULL, &len, g_bytes_get_data (data, NULL), g_bytes_get_size (data)) + && (sig = g_malloc0 (len)) != NULL && EVP_DigestSign (ctx, sig, &len, g_bytes_get_data (data, NULL), g_bytes_get_size (data))) sig_size = len; @@ -172,12 +192,6 @@ ostree_sign_ed25519_data (OstreeSign *self, GBytes *data, GBytes **signature, return TRUE; } -static gint -_compare_ed25519_keys (gconstpointer a, gconstpointer b) -{ - return memcmp (a, b, OSTREE_SIGN_ED25519_PUBKEY_SIZE); -} - gboolean ostree_sign_ed25519_data_verify (OstreeSign *self, GBytes *data, GVariant *signatures, char **out_success_message, GError **error) @@ -222,29 +236,27 @@ ostree_sign_ed25519_data_verify (OstreeSign *self, GBytes *data, GVariant *signa g_autoptr (GVariant) child = g_variant_get_child_value (signatures, i); g_autoptr (GBytes) signature = g_variant_get_data_as_bytes (child); +#if defined(USE_LIBSODIUM) if (!validate_length (g_bytes_get_size (signature), OSTREE_SIGN_ED25519_SIG_SIZE, error)) return glnx_prefix_error (error, "Invalid signature"); - - g_autofree char *hex = g_malloc0 (OSTREE_SIGN_ED25519_PUBKEY_SIZE * 2 + 1); +#endif g_debug ("Read signature %d: %s", (gint)i, g_variant_print (child, TRUE)); - for (GList *public_key = sign->public_keys; public_key != NULL; public_key = public_key->next) + for (GList *l = sign->public_keys; l != NULL; l = l->next) { + GBytes *public_key = l->data; /* TODO: use non-list for tons of revoked keys? */ - if (g_list_find_custom (sign->revoked_keys, public_key->data, _compare_ed25519_keys) - != NULL) + if (g_list_find_custom (sign->revoked_keys, public_key, g_bytes_compare) != NULL) { - ot_bin2hex (hex, public_key->data, OSTREE_SIGN_ED25519_PUBKEY_SIZE); + g_autofree char *hex = g_malloc0 (g_bytes_get_size (public_key) * 2 + 1); + ot_bin2hex (hex, g_bytes_get_data (public_key, NULL), g_bytes_get_size (public_key)); g_debug ("Skip revoked key '%s'", hex); continue; } bool valid = false; - // Wrap the pubkey in a GBytes as that's what this API wants - g_autoptr (GBytes) public_key_bytes - = g_bytes_new_static (public_key->data, OSTREE_SIGN_ED25519_PUBKEY_SIZE); - if (!otcore_validate_ed25519_signature (data, public_key_bytes, signature, &valid, error)) + if (!otcore_validate_ed25519_signature (data, public_key, signature, &valid, error)) return FALSE; if (!valid) { @@ -254,14 +266,17 @@ ostree_sign_ed25519_data_verify (OstreeSign *self, GBytes *data, GVariant *signa else g_string_append (invalid_signatures, "; "); n_invalid_signatures++; - ot_bin2hex (hex, public_key->data, OSTREE_SIGN_ED25519_PUBKEY_SIZE); + g_autofree char *hex = g_malloc0 (g_bytes_get_size (public_key) * 2 + 1); + ot_bin2hex (hex, g_bytes_get_data (public_key, NULL), g_bytes_get_size (public_key)); g_string_append_printf (invalid_signatures, "key '%s'", hex); } else { if (out_success_message) { - ot_bin2hex (hex, public_key->data, OSTREE_SIGN_ED25519_PUBKEY_SIZE); + g_autofree char *hex = g_malloc0 (g_bytes_get_size (public_key) * 2 + 1); + ot_bin2hex (hex, g_bytes_get_data (public_key, NULL), + g_bytes_get_size (public_key)); *out_success_message = g_strdup_printf ( "ed25519: Signature verified successfully with key '%s'", hex); } @@ -320,22 +335,23 @@ ostree_sign_ed25519_clear_keys (OstreeSign *self, GError **error) /* Clear secret key */ if (sign->secret_key != NULL) { - memset (sign->secret_key, 0, OSTREE_SIGN_ED25519_SECKEY_SIZE); - g_free (sign->secret_key); + gsize size; + gpointer data = g_bytes_unref_to_data (sign->secret_key, &size); + memset (data, 0, size); sign->secret_key = NULL; } /* Clear already loaded trusted keys */ if (sign->public_keys != NULL) { - g_list_free_full (sign->public_keys, g_free); + g_list_free_full (sign->public_keys, (GDestroyNotify)g_bytes_unref); sign->public_keys = NULL; } /* Clear already loaded revoked keys */ if (sign->revoked_keys != NULL) { - g_list_free_full (sign->revoked_keys, g_free); + g_list_free_full (sign->revoked_keys, (GDestroyNotify)g_bytes_unref); sign->revoked_keys = NULL; } @@ -374,10 +390,12 @@ ostree_sign_ed25519_set_sk (OstreeSign *self, GVariant *secret_key, GError **err return glnx_throw (error, "Unknown ed25519 secret key type"); } +#if defined(USE_LIBSODIUM) if (!validate_length (n_elements, OSTREE_SIGN_ED25519_SECKEY_SIZE, error)) return glnx_prefix_error (error, "Invalid ed25519 secret key"); +#endif - sign->secret_key = g_steal_pointer (&secret_key_buf); + sign->secret_key = g_bytes_new_take (g_steal_pointer (&secret_key_buf), n_elements); return TRUE; } @@ -429,17 +447,20 @@ ostree_sign_ed25519_add_pk (OstreeSign *self, GVariant *public_key, GError **err return glnx_throw (error, "Unknown ed25519 public key type"); } +#if defined(USE_LIBSODIUM) if (!validate_length (n_elements, OSTREE_SIGN_ED25519_PUBKEY_SIZE, error)) return glnx_prefix_error (error, "Invalid ed25519 public key"); +#endif - g_autofree char *hex = g_malloc0 (OSTREE_SIGN_ED25519_PUBKEY_SIZE * 2 + 1); + g_autofree char *hex = g_malloc0 (n_elements * 2 + 1); ot_bin2hex (hex, key, n_elements); g_debug ("Read ed25519 public key = %s", hex); - if (g_list_find_custom (sign->public_keys, key, _compare_ed25519_keys) == NULL) + g_autoptr (GBytes) key_bytes = g_bytes_new_static (key, n_elements); + if (g_list_find_custom (sign->public_keys, key_bytes, g_bytes_compare) == NULL) { - gpointer newkey = g_memdup2 (key, n_elements); - sign->public_keys = g_list_prepend (sign->public_keys, newkey); + GBytes *new_key_bytes = g_bytes_new (key, n_elements); + sign->public_keys = g_list_prepend (sign->public_keys, new_key_bytes); } return TRUE; @@ -460,17 +481,20 @@ _ed25519_add_revoked (OstreeSign *self, GVariant *revoked_key, GError **error) gsize n_elements = 0; g_autofree guint8 *key = g_base64_decode (rk_ascii, &n_elements); +#if defined(USE_LIBSODIUM) if (!validate_length (n_elements, OSTREE_SIGN_ED25519_PUBKEY_SIZE, error)) return glnx_prefix_error (error, "Incorrect ed25519 revoked key"); +#endif - g_autofree char *hex = g_malloc0 (OSTREE_SIGN_ED25519_PUBKEY_SIZE * 2 + 1); + g_autofree char *hex = g_malloc0 (n_elements * 2 + 1); ot_bin2hex (hex, key, n_elements); g_debug ("Read ed25519 revoked key = %s", hex); - if (g_list_find_custom (sign->revoked_keys, key, _compare_ed25519_keys) == NULL) + g_autoptr (GBytes) key_bytes = g_bytes_new_static (key, n_elements); + if (g_list_find_custom (sign->revoked_keys, key, g_bytes_compare) == NULL) { - gpointer newkey = g_memdup2 (key, n_elements); - sign->revoked_keys = g_list_prepend (sign->revoked_keys, newkey); + GBytes *new_key_bytes = g_bytes_new (key, n_elements); + sign->revoked_keys = g_list_prepend (sign->revoked_keys, new_key_bytes); } return TRUE; diff --git a/src/libotcore/otcore-ed25519-verify.c b/src/libotcore/otcore-ed25519-verify.c index 24f173b445..95dcb33fe7 100644 --- a/src/libotcore/otcore-ed25519-verify.c +++ b/src/libotcore/otcore-ed25519-verify.c @@ -51,7 +51,12 @@ otcore_ed25519_init (void) * `out_valid` will be set to `false`. * * If the signature is correct, `out_valid` will be `true`. - * */ + * + * Note: when OpenSSL is enabled, public key is not restricted to ed25519 but + * something else if encoded in the X.509 SubjectPublicKeyInfo format. In that + * case, however, the hash algorithm is implicitly determined and thus + * unrestricted key types, e.g., raw RSA or ECDSA are not supported. + */ gboolean otcore_validate_ed25519_signature (GBytes *data, GBytes *public_key, GBytes *signature, bool *out_valid, GError **error) @@ -64,21 +69,24 @@ otcore_validate_ed25519_signature (GBytes *data, GBytes *public_key, GBytes *sig // It is OK for error to be NULL, though according to GError rules. #if defined(HAVE_LIBSODIUM) || defined(HAVE_OPENSSL) - // And strictly verify pubkey and signature lengths - if (g_bytes_get_size (public_key) != OSTREE_SIGN_ED25519_PUBKEY_SIZE) - return glnx_throw (error, "Invalid public key of %" G_GSIZE_FORMAT " expected %" G_GSIZE_FORMAT, - (gsize)g_bytes_get_size (public_key), - (gsize)OSTREE_SIGN_ED25519_PUBKEY_SIZE); - const guint8 *public_key_buf = g_bytes_get_data (public_key, NULL); - if (g_bytes_get_size (signature) != OSTREE_SIGN_ED25519_SIG_SIZE) - return glnx_throw ( - error, "Invalid signature length of %" G_GSIZE_FORMAT " bytes, expected %" G_GSIZE_FORMAT, - (gsize)g_bytes_get_size (signature), (gsize)OSTREE_SIGN_ED25519_SIG_SIZE); - const guint8 *signature_buf = g_bytes_get_data (signature, NULL); + gsize public_key_size; + const guint8 *public_key_buf = g_bytes_get_data (public_key, &public_key_size); + gsize signature_size; + const guint8 *signature_buf = g_bytes_get_data (signature, &signature_size); #endif #if defined(HAVE_LIBSODIUM) + // Strictly verify pubkey and signature lengths, as libsodium can + // only handle raw ed25519 public key and signatures. + if (public_key_size != OSTREE_SIGN_ED25519_PUBKEY_SIZE) + return glnx_throw (error, "Invalid public key of %" G_GSIZE_FORMAT " expected %" G_GSIZE_FORMAT, + public_key_size, (gsize)OSTREE_SIGN_ED25519_PUBKEY_SIZE); + if (signature_size != OSTREE_SIGN_ED25519_SIG_SIZE) + return glnx_throw ( + error, "Invalid signature length of %" G_GSIZE_FORMAT " bytes, expected %" G_GSIZE_FORMAT, + signature_size, (gsize)OSTREE_SIGN_ED25519_SIG_SIZE); + // Note that libsodium assumes the passed byte arrays for the signature and public key // have at least the expected length, but we checked that above. if (crypto_sign_verify_detached (signature_buf, g_bytes_get_data (data, NULL), @@ -92,16 +100,23 @@ otcore_validate_ed25519_signature (GBytes *data, GBytes *public_key, GBytes *sig EVP_MD_CTX *ctx = EVP_MD_CTX_new (); if (!ctx) return glnx_throw (error, "openssl: failed to allocate context"); - EVP_PKEY *pkey = EVP_PKEY_new_raw_public_key (EVP_PKEY_ED25519, NULL, public_key_buf, - OSTREE_SIGN_ED25519_PUBKEY_SIZE); + + // Try SubjectPublicKeyInfo encoded public key first. + const unsigned char *p = public_key_buf; + EVP_PKEY *pkey = d2i_PUBKEY (NULL, &p, public_key_size); + + // Try raw ed25519 public key if the length matches. + if (pkey == NULL && public_key_size == OSTREE_SIGN_ED25519_PUBKEY_SIZE) + pkey = EVP_PKEY_new_raw_public_key (EVP_PKEY_ED25519, NULL, public_key_buf, public_key_size); + if (!pkey) { EVP_MD_CTX_free (ctx); return glnx_throw (error, "openssl: Failed to initialize ed25519 key"); } if (EVP_DigestVerifyInit (ctx, NULL, NULL, NULL, pkey) != 0 - && EVP_DigestVerify (ctx, signature_buf, OSTREE_SIGN_ED25519_SIG_SIZE, - g_bytes_get_data (data, NULL), g_bytes_get_size (data)) + && EVP_DigestVerify (ctx, signature_buf, signature_size, g_bytes_get_data (data, NULL), + g_bytes_get_size (data)) != 0) { *out_valid = true; diff --git a/src/libotcore/otcore.h b/src/libotcore/otcore.h index fc6b81ca1a..f76cca6833 100644 --- a/src/libotcore/otcore.h +++ b/src/libotcore/otcore.h @@ -27,6 +27,7 @@ #define USE_LIBSODIUM #elif defined(HAVE_OPENSSL) #include +#include #define USE_OPENSSL #endif diff --git a/tests/libtest.sh b/tests/libtest.sh index 2c2a33f0d9..01bb6614f4 100755 --- a/tests/libtest.sh +++ b/tests/libtest.sh @@ -757,7 +757,34 @@ ED25519PUBLIC= ED25519SEED= ED25519SECRET= -gen_ed25519_keys () +gen_keys_pkcs8 () +{ + local lower=$1 + shift + local upper=$1 + shift + + # Generate private key in PEM format + local pemfile="$(mktemp -p ${test_tmpdir} ${lower}_XXXXXX.pem)" + openssl genpkey -algorithm "$lower" -outform PEM -out "${pemfile}" + + local public="$(openssl pkey -outform DER -pubout -in ${pemfile} | base64 -w 0)" + local secret="$(openssl pkey -outform DER -in ${pemfile} | base64 -w 0)" + + echo "Generated $lower keys:" + echo "public: ${public}" + echo "secret: ${secret}" + + eval "${upper}PUBLIC=${public}" + eval "${upper}SECRET=${secret}" +} + +gen_ed25519_keys_pkcs8 () +{ + gen_keys_pkcs8 ed25519 ED25519 +} + +gen_ed25519_keys_raw () { # Generate private key in PEM format pemfile="$(mktemp -p ${test_tmpdir} ed25519_XXXXXX.pem)" @@ -775,9 +802,54 @@ gen_ed25519_keys () echo " seed: ${ED25519SEED}" } +gen_ed25519_keys () +{ + if has_ostree_feature sign-pkcs8; then + gen_ed25519_keys_pkcs8 + else + gen_ed25519_keys_raw + fi +} + +gen_random_public_spki() +{ + local lower=$1 + shift + + openssl genpkey -algorithm "$lower" | openssl pkey -pubout -outform DER | base64 -w 0 + echo +} + +gen_ed25519_random_public_spki() +{ + gen_random_public_spki ed25519 +} + +gen_ed25519_random_public_raw() +{ + openssl genpkey -algorithm ed25519 | openssl pkey -pubout -outform DER | tail -c 32 | base64 +} + gen_ed25519_random_public() { - openssl genpkey -algorithm ED25519 | openssl pkey -outform DER | tail -c 32 | base64 + if has_ostree_feature sign-pkcs8; then + gen_ed25519_random_public_spki + else + gen_ed25519_random_public_raw + fi +} + +ED448PUBLIC= +ED448SECRET= + +gen_ed448_keys() +{ + gen_keys_pkcs8 ed448 ED448 +} + +gen_ed448_random_public() +{ + gen_random_public_spki ed448 } is_bare_user_only_repo () { diff --git a/tests/test-signed-commit.sh b/tests/test-signed-commit.sh index cf1cd1c852..f887b3e909 100755 --- a/tests/test-signed-commit.sh +++ b/tests/test-signed-commit.sh @@ -21,7 +21,7 @@ set -euo pipefail . $(dirname $0)/libtest.sh -echo "1..11" +echo "1..13" # This is explicitly opt in for testing export OSTREE_DUMMY_SIGN_ENABLED=1 @@ -69,6 +69,8 @@ if ! has_ostree_feature sign-ed25519; then echo "ok sign with ed25519 keys file # SKIP due libsodium unavailability" echo "ok verify ed25519 system-wide configuration # SKIP due libsodium unavailability" echo "ok verify ed25519 revoking keys mechanism # SKIP due libsodium unavailability" + echo "ok Detached ed448 signature added # SKIP due openssl unavailability" + echo "ok ed448 signature verified # SKIP due openssl unavailability" exit 0 fi @@ -202,3 +204,38 @@ if ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed2 fi rm -rf ${test_tmpdir}/{trusted,revoked}.ed25519.d echo "ok verify ed25519 revoking keys mechanism" + +# Basic test using different underlying key type (ed448) +if ! has_ostree_feature sign-pkcs8; then + echo "ok Detached ed448 signature added # SKIP due openssl unavailability" + echo "ok ed448 signature verified # SKIP due openssl unavailability" + exit 0 +fi + +gen_ed448_keys +PUBLIC=${ED448PUBLIC} +SECRET=${ED448SECRET} + +WRONG_PUBLIC="$(gen_ed448_random_public)" + +echo "PUBLIC = $PUBLIC" + +echo "Signed commit with ed25519: ${SECRET}" >> file.txt +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo commit -b main -s "Signed with ed25519 module" --sign="${SECRET}" --sign-type=ed25519 +COMMIT="$(ostree --repo=${test_tmpdir}/repo rev-parse main)" + +# Ensure that detached metadata contain signature +${CMD_PREFIX} ostree --repo=repo show ${COMMIT} --print-detached-metadata-key=ostree.sign.ed25519 &>/dev/null +echo "ok Detached ed448 signature added" + +# Verify vith sign mechanism +if ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 ${COMMIT} ${WRONG_PUBLIC}; then + exit 1 +fi +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 ${COMMIT} ${PUBLIC} +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 ${COMMIT} ${PUBLIC} ${PUBLIC} +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 ${COMMIT} $(gen_ed448_random_public) ${PUBLIC} +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 ${COMMIT} $(gen_ed448_random_public) $(gen_ed448_random_public) ${PUBLIC} +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 ${COMMIT} ${PUBLIC} $(gen_ed448_random_public) $(gen_ed448_random_public) +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 ${COMMIT} $(gen_ed448_random_public) $(gen_ed448_random_public) ${PUBLIC} $(gen_ed448_random_public) $(gen_ed448_random_public) +echo "ok ed448 signature verified"