From e9772e5bdcb9b2199cce4c514b2c037824cf3987 Mon Sep 17 00:00:00 2001 From: Daiki Ueno Date: Fri, 12 Jul 2024 12:54:13 +0900 Subject: [PATCH] sign: Support keys encoded with public key algorithm The current commit signing mechanism assumes raw Ed25519 key format for both public and private keys. That requires custom processing of keys after generated with openssl tools, and also lacks cryptographic agility[1]; when Ed25519 becomes vulnerable, it would not be straightforward to migrate to other algorithms. This patch switches to using the standard key formats natively supported by OpenSSL (PKCS#8 and SubjectPublicKeyInfo) and capable of embedding algorithm identifier, while the support for the original key format is preserved for backward compatibility. As a PoC of the feature, this adds a couple of new tests using Ed448, instead of Ed25519, in tests/test-signed-commit.sh. 1. https://en.wikipedia.org/wiki/Cryptographic_agility Signed-off-by: Daiki Ueno --- configure.ac | 4 + src/libostree/ostree-sign-ed25519.c | 102 ++++++++++++++++---------- src/libotcore/otcore-ed25519-verify.c | 49 ++++++++----- src/libotcore/otcore.h | 1 + tests/libtest.sh | 76 ++++++++++++++++++- tests/test-signed-commit.sh | 36 ++++++++- 6 files changed, 208 insertions(+), 60 deletions(-) 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 3dcff2ebb5..303801bb8b 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) { @@ -103,8 +106,9 @@ validate_length (gsize found, gsize expected, GError **error) return TRUE; return glnx_throw ( error, "Ill-formed input: expected %" G_GSIZE_FORMAT " bytes, got %" G_GSIZE_FORMAT " bytes", - found, expected); + expected, found); } +#endif static gboolean _ostree_sign_ed25519_is_initialized (OstreeSignEd25519 *self, GError **error) @@ -136,27 +140,41 @@ 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"); + gsize secret_key_size; + const guint8 *secret_key_buf = g_bytes_get_data (sign->secret_key, &secret_key_size); + 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); - return glnx_throw (error, "openssl: Failed to initialize ed5519 key"); + return glnx_throw (error, "openssl: Failed to initialize ed25519 key"); } 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 +190,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 +234,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 +264,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 +333,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 +388,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 +445,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 +479,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 1c0ec2b83e..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 ed5519 key"); + 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..5f1ff73208 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 @@ -202,3 +202,37 @@ 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 libsodium unavailability" +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"