From 777beb10e242da09b65b6480cb77f3f717f62285 Mon Sep 17 00:00:00 2001 From: Joey Bright Date: Sat, 2 May 2026 16:08:23 -0700 Subject: [PATCH 1/6] Add support for generating signatures with the Ed25519 algorithm --- integration_tests/src/Test/Crypto.gren | 267 +++++++++++++++++++++++++ src/Crypto.gren | 207 ++++++++++++++++++- src/Gren/Kernel/Crypto.js | 109 +++++++++- 3 files changed, 578 insertions(+), 5 deletions(-) diff --git a/integration_tests/src/Test/Crypto.gren b/integration_tests/src/Test/Crypto.gren index 622732d9..781c4f5a 100644 --- a/integration_tests/src/Test/Crypto.gren +++ b/integration_tests/src/Test/Crypto.gren @@ -32,6 +32,7 @@ tests = , hmacTests secureContext , rsaPssTests secureContext , ecdsaTests secureContext + , ed25519Tests secureContext , digestTests secureContext ] ) @@ -2504,6 +2505,272 @@ ecdsaTestsHelper secureContext label { namedCurve, digestAlgorithm } = +-- Ed25519 Tests + + +{-|-} +ed25519Tests secureContext = + concat + [ await + (Crypto.generateEd25519KeyPair + secureContext + Crypto.CannotBeExtracted + ) + "Ed25519: Generating a key that cannot be extracted" + (\{ publicKey, privateKey } -> + concat + [ await + (Crypto.exportEd25519PublicKeyAsRaw publicKey) + "Exporting public Ed25519 key as RAW when not extractable" + (\exportedKey -> + await + (Crypto.importEd25519PublicKeyFromRaw + secureContext + exportedKey + ) + "Importing the exported key" + (\importedKey -> + test "Imported key equals the original key" + (\_ -> + Expect.equal importedKey publicKey + ) + ) + ) + , await + (Crypto.exportEd25519PublicKeyAsSpki publicKey) + "Exporting public Ed25519 key as SPKI when not extractable" + (\exportedKey -> + await + (Crypto.importEd25519PublicKeyFromSpki + secureContext + exportedKey + ) + "Importing the exported key" + (\importedKey -> + test "Imported key equals the original key" + (\_ -> + Expect.equal importedKey publicKey + ) + ) + ) + , await + (Crypto.exportEd25519PublicKeyAsJwk publicKey) + "Exporting public Ed25519 key as JWK when not extractable" + (\exportedKey -> + await + (Crypto.importEd25519PublicKeyFromJwk + secureContext + exportedKey + ) + "Importing the exported key" + (\importedKey -> + test "Imported key equals the original key" + (\_ -> + Expect.equal importedKey publicKey + ) + ) + ) + , awaitError + (Crypto.exportEd25519PrivateKeyAsPkcs8 privateKey) + "Exporting private Ed25519 key as PKCS8 when not extractable" + (\err -> + test "When exporting private key that isn't extractable, fails with expected message" + (\_ -> + Expect.equal err Crypto.KeyNotExportable + ) + ) + , awaitError + (Crypto.exportEd25519PrivateKeyAsJwk privateKey) + "Exporting private Ed25519 key as JWK when not extractable" + (\err -> + test "When exporting private key that isn't extractable, fails with expected message" + (\_ -> + Expect.equal err Crypto.KeyNotExportable + ) + ) + , await + (Crypto.signWithEd25519 privateKey (Bytes.fromString "hello")) + "Signing some bytes with a non-extractable key" + (\signature -> + await + (Crypto.verifyWithEd25519 publicKey signature (Bytes.fromString "hello")) + "Verifying signed bytes" + (\verifiedBytes -> + test "The verified bytes equal the signed bytes" + (\_ -> + Expect.equal verifiedBytes (Bytes.fromString "hello") + ) + ) + ) + ] + ) + , await + (Crypto.generateEd25519KeyPair + secureContext + Crypto.CanBeExtracted + ) + "Ed25519: Generating a key that can be extracted" + (\{ publicKey, privateKey } -> + let + bytesToSign = + Bytes.fromString "hello" + in + concat + [ await + (Crypto.exportEd25519PublicKeyAsRaw publicKey) + "Exporting public Ed25519 key as RAW when extractable" + (\exportedKey -> + await + (Crypto.importEd25519PublicKeyFromRaw + secureContext + exportedKey + ) + "Importing the exported key" + (\importedKey -> + test "Imported key equals the original key" + (\_ -> + Expect.equal importedKey publicKey + ) + ) + ) + , await + (Crypto.exportEd25519PublicKeyAsSpki publicKey) + "Exporting public Ed25519 key as SPKI when extractable" + (\exportedKey -> + await + (Crypto.importEd25519PublicKeyFromSpki + secureContext + exportedKey + ) + "Importing the exported key" + (\importedKey -> + test "Imported key equals the original key" + (\_ -> + Expect.equal importedKey publicKey + ) + ) + ) + , await + (Crypto.exportEd25519PublicKeyAsJwk publicKey) + "Exporting public Ed25519 key as JWK when extractable" + (\exportedKey -> + await + (Crypto.importEd25519PublicKeyFromJwk + secureContext + exportedKey + ) + "Importing the exported key" + (\importedKey -> + test "Imported key equals the original key" + (\_ -> + Expect.equal importedKey publicKey + ) + ) + ) + , await + (Crypto.exportEd25519PrivateKeyAsPkcs8 privateKey) + "Exporting private Ed25519 key as PKCS8 when extractable" + (\exportedKey -> + await + (Crypto.importEd25519PrivateKeyFromPkcs8 + secureContext + Crypto.CanBeExtracted + exportedKey + ) + "Importing the exported key" + (\importedKey -> + test "Imported key equals the original key" + (\_ -> + Expect.equal importedKey privateKey + ) + ) + ) + , await + (Crypto.exportEd25519PrivateKeyAsJwk privateKey) + "Exporting private Ed25519 key as JWK when extractable" + (\exportedKey -> + await + (Crypto.importEd25519PrivateKeyFromJwk + secureContext + Crypto.CanBeExtracted + exportedKey + ) + "Importing the exported key" + (\importedKey -> + test "Imported key equals the original key" + (\_ -> + Expect.equal importedKey privateKey + ) + ) + ) + , await + (Crypto.signWithEd25519 privateKey bytesToSign) + "Sign some bytes" + (\signature -> + concat + [ await + (Crypto.verifyWithEd25519 publicKey signature bytesToSign) + "Verifying signed bytes" + (\verifiedBytes -> + test "The verified bytes are the same as the signed bytes" + (\_ -> + Expect.equal verifiedBytes bytesToSign + ) + ) + , awaitError + (Crypto.verifyWithEd25519 publicKey signature (Bytes.fromString "some other bytes")) + "Verifying bytes that were not signed" + (\_ -> + test "Fails when bytes that are being verified are different than what was signed" + (\_ -> + Expect.pass + ) + ) + ] + ) + ] + ) + , awaitError + (Crypto.importEd25519PublicKeyFromRaw + secureContext + (Bytes.fromString "obviously not a valid ed25519 public key") + ) + "Importing an Ed25519 public key from invalid raw bytes" + (\err -> + test "Produces the expected error" + (\_ -> + Expect.equal err Crypto.ImportEd25519KeyError + ) + ) + , awaitError + (Crypto.importEd25519PublicKeyFromSpki + secureContext + (Bytes.fromString "junk") + ) + "Importing an Ed25519 public key from invalid SPKI bytes" + (\err -> + test "Produces the expected error" + (\_ -> + Expect.equal err Crypto.ImportEd25519KeyError + ) + ) + , awaitError + (Crypto.importEd25519PrivateKeyFromPkcs8 + secureContext + Crypto.CanBeExtracted + (Bytes.fromString "junk") + ) + "Importing an Ed25519 private key from invalid PKCS8 bytes" + (\err -> + test "Produces the expected error" + (\_ -> + Expect.equal err Crypto.ImportEd25519KeyError + ) + ) + ] + + + -- HMAC Tests diff --git a/src/Crypto.gren b/src/Crypto.gren index 415db1a9..a95e3867 100644 --- a/src/Crypto.gren +++ b/src/Crypto.gren @@ -22,6 +22,7 @@ module Crypto exposing , RsaPssParams, RsaPssSigningError(..) , signWithRsaPss, verifyWithRsaPss , signWithEcdsa, verifyWithEcdsa + , signWithEd25519, verifyWithEd25519 , signWithHmac, verifyWithHmac , DigestAlgorithm(..), digest , Key, PublicKey, PrivateKey, KeyPair @@ -32,6 +33,8 @@ module Crypto exposing , generateAesCtrKey, generateAesCbcKey, generateAesGcmKey , EcKeyParams, EcNamedCurve(..) , generateEcdsaKeyPair + , Ed25519Key, Ed25519KeyGenerationError(..) + , generateEd25519KeyPair , HmacKeyParams, HmacKeyGenerationError(..) , generateHmacKey , ExportKeyError(..) @@ -46,6 +49,8 @@ module Crypto exposing , exportAesGcmKeyAsRaw, exportAesGcmKeyAsJwk , exportEcdsaPublicKeyAsRaw, exportEcdsaPublicKeyAsSpki, exportEcdsaPublicKeyAsJwk , exportEcdsaPrivateKeyAsPkcs8, exportEcdsaPrivateKeyAsJwk + , exportEd25519PublicKeyAsRaw, exportEd25519PublicKeyAsSpki, exportEd25519PublicKeyAsJwk + , exportEd25519PrivateKeyAsPkcs8, exportEd25519PrivateKeyAsJwk , exportHmacKeyAsRaw, exportHmacKeyAsJwk , ImportRsaKeyError(..) , importRsaOaepPublicKeyFromJwk, importRsaOaepPublicKeyFromSpki @@ -61,6 +66,9 @@ module Crypto exposing , ImportEcKeyError(..) , importEcdsaPublicKeyFromRaw, importEcdsaPublicKeyFromSpki, importEcdsaPublicKeyFromJwk , importEcdsaPrivateKeyFromPkcs8, importEcdsaPrivateKeyFromSpki, importEcdsaPrivateKeyFromJwk + , ImportEd25519KeyError(..) + , importEd25519PublicKeyFromRaw, importEd25519PublicKeyFromSpki, importEd25519PublicKeyFromJwk + , importEd25519PrivateKeyFromPkcs8, importEd25519PrivateKeyFromJwk , ImportHmacKeyError(..) , importHmacKeyFromJwk, importHmacKeyFromRaw ) @@ -81,6 +89,7 @@ module supports the following algorithms and operations: - RSA-SSAPKCS1v1.5 - [Signing](#signWithRsaSsaPkcs1V1_5) and [verifying](#verifyWithRsaSsaPkcs1V1_5) - RSA-PSS - [Signing](#signWithRsaPss) and [verifying](#verifyWithRsaPss) - ECDSA - [Signing](#signWithEcdsa) and [verifying](#verifyWithEcdsa) +- Ed25519 - [Signing](#signWithEd25519) and [verifying](#verifyWithEd25519) - HMAC - [Signing](#signWithHmac) and [verifying](#verifyWithHmac) - SHA - [Digest](#digest) @@ -208,6 +217,13 @@ These functions require an RSA-PSS key. You can generate one with the @docs signWithEcdsa, verifyWithEcdsa +### Sign & verify with the Ed25519 algorithm + +Sign and verify some `Bytes` with the Ed25519 algorithm. These functions require an Ed25519 key. +You can generate one with the [`generateEd25519KeyPair`](#generateEd25519KeyPair) function. + +@docs signWithEd25519, verifyWithEd25519 + ### Sign & verify with the HMAC algorithm Sign and verify some `Bytes` with the HMAC (Hash-Based Message Authentication Code) algorithm. @@ -252,6 +268,14 @@ Generate keys to use with EC (Elliptic Curve) algorithm. @docs generateEcdsaKeyPair +### Generate Ed25519 Keys + +Generate keys to use with the Ed25519 algorithm. + +@docs Ed25519Key, Ed25519KeyGenerationError + +@docs generateEd25519KeyPair + ### Generate HMAC Keys Generate keys to use with HMAC (Hash-Based Message Authentication Code) algorithm. @@ -296,6 +320,12 @@ for the Web Crypto API. @docs exportEcdsaPrivateKeyAsPkcs8, exportEcdsaPrivateKeyAsJwk +### Export Ed25519 Keys + +@docs exportEd25519PublicKeyAsRaw, exportEd25519PublicKeyAsSpki, exportEd25519PublicKeyAsJwk + +@docs exportEd25519PrivateKeyAsPkcs8, exportEd25519PrivateKeyAsJwk + ### Export HMAC Keys @docs exportHmacKeyAsRaw, exportHmacKeyAsJwk @@ -340,7 +370,15 @@ for the Web Crypto API. @docs importEcdsaPublicKeyFromJwk, importEcdsaPublicKeyFromRaw, importEcdsaPublicKeyFromSpki -### Import HMAC Keys +### Import Ed25519 Keys + +@docs ImportEd25519KeyError + +@docs importEd25519PublicKeyFromRaw, importEd25519PublicKeyFromSpki, importEd25519PublicKeyFromJwk + +@docs importEd25519PrivateKeyFromPkcs8, importEd25519PrivateKeyFromJwk + +### Import HMAC Keys @docs ImportHmacKeyError @@ -750,7 +788,7 @@ type EcNamedCurve {-| Generate a new key using the ECDSA algorithm. -Produces a `KeyPair` that can be used to sign data with [`signWithEcdsa`](#signWithEcdsa) +Produces a `KeyPair` that can be used to sign data with [`signWithEcdsa`](#signWithEcdsa) and verify data with [`verifyWithEcdsa`](#verifyWithEcdsa). -} generateEcdsaKeyPair : SecureContext -> EcKeyParams -> Task x (KeyPair EcdsaKey EcKeyParams) @@ -774,6 +812,43 @@ generateEcdhKeyPair _context { namedCurve, extractable } = +-- ED25519 KEYS + + +{-| A key generated for use with the Ed25519 algorithm. Used to sign and verify +values. + +Unlike ECDSA, Ed25519 has no curve or hash parameters: the algorithm fully +specifies the curve (Curve25519, in Edwards form) and the hash function (SHA-512) +internally. +-} +type Ed25519Key + = Ed25519Key + + +{-| Errors that can happen when generating a key for use with the Ed25519 +algorithm. + +- `Ed25519KeyGenerationNotSupported` happens when the runtime's Web Crypto API +does not implement Ed25519. +-} +type Ed25519KeyGenerationError + = Ed25519KeyGenerationNotSupported + + +{-| Generate a new key using the Ed25519 algorithm. + +Produces a `KeyPair` that can be used to sign data with [`signWithEd25519`](#signWithEd25519) +and verify data with [`verifyWithEd25519`](#verifyWithEd25519). +-} +generateEd25519KeyPair : SecureContext -> Extractable -> Task Ed25519KeyGenerationError (KeyPair Ed25519Key {}) +generateEd25519KeyPair _context extractable = + Gren.Kernel.Crypto.generateEd25519Key + (extractableToBool extractable) + [ "sign", "verify" ] + + + -- HMAC KEYS @@ -1004,12 +1079,22 @@ exportEcdhPublicKeyAsRaw : PublicKey EcdhKey EcKeyParams -> Task {} Bytes exportEcdhPublicKeyAsRaw (PublicKey key) = exportPublicKeyAsRaw key +{-|-} +exportEd25519PublicKeyAsRaw : PublicKey Ed25519Key {} -> Task {} Bytes +exportEd25519PublicKeyAsRaw (PublicKey key) = + exportPublicKeyAsRaw key + {-|-} exportEcdhPublicKeyAsSpki : PublicKey EcdhKey EcKeyParams -> Task {} Bytes exportEcdhPublicKeyAsSpki (PublicKey key) = exportPublicKeyAsSpki key +{-|-} +exportEd25519PublicKeyAsSpki : PublicKey Ed25519Key {} -> Task {} Bytes +exportEd25519PublicKeyAsSpki (PublicKey key) = + exportPublicKeyAsSpki key + {-|-} exportEcdhPublicKeyAsJwk : PublicKey EcdhKey EcKeyParams -> Task {} Json.Encode.Value @@ -1017,10 +1102,22 @@ exportEcdhPublicKeyAsJwk (PublicKey key) = exportPublicKeyAsJwk key +{-|-} +exportEd25519PublicKeyAsJwk : PublicKey Ed25519Key {} -> Task {} Json.Encode.Value +exportEd25519PublicKeyAsJwk (PublicKey key) = + exportPublicKeyAsJwk key + + {-|-} exportEcdhPrivateKeyAsPkcs8 : PrivateKey EcdhKey EcKeyParams -> Task ExportKeyError Bytes exportEcdhPrivateKeyAsPkcs8 (PrivateKey key) = exportKeyAsPkcs8 key + + +{-|-} +exportEd25519PrivateKeyAsPkcs8 : PrivateKey Ed25519Key {} -> Task ExportKeyError Bytes +exportEd25519PrivateKeyAsPkcs8 (PrivateKey key) = + exportKeyAsPkcs8 key {-|-} @@ -1029,6 +1126,12 @@ exportEcdhPrivateKeyAsJwk (PrivateKey key) = exportKeyAsJwk key +{-|-} +exportEd25519PrivateKeyAsJwk : PrivateKey Ed25519Key {} -> Task ExportKeyError Json.Encode.Value +exportEd25519PrivateKeyAsJwk (PrivateKey key) = + exportKeyAsJwk key + + {-|-} exportHmacKeyAsRaw : Key HmacKey HmacKeyParams -> Task ExportKeyError Bytes exportHmacKeyAsRaw = @@ -1518,6 +1621,74 @@ importEcdhPrivateKeyFromJwk _context extractable namedCurve jwk = [ "deriveKey", "deriveBits" ] +{-| Errors that can happen when importing a key using the Ed25519 algorithm. +There are two reasons this error can happen: + +- `ImportEd25519KeyError`: the passed key value (either `Json.Encode.Value` or +`Bytes`) is not a valid key and cannot be imported. +- `ImportEd25519KeyNotSupported`: the runtime's Web Crypto API does not +implement Ed25519. +-} +type ImportEd25519KeyError + = ImportEd25519KeyError + | ImportEd25519KeyNotSupported + + +{-|-} +importEd25519PublicKeyFromRaw : SecureContext -> Bytes -> Task ImportEd25519KeyError (PublicKey Ed25519Key {}) +importEd25519PublicKeyFromRaw _context bytes = + Gren.Kernel.Crypto.importEd25519Key + "public" + "raw" + bytes + True + [ "verify" ] + + +{-|-} +importEd25519PublicKeyFromSpki : SecureContext -> Bytes -> Task ImportEd25519KeyError (PublicKey Ed25519Key {}) +importEd25519PublicKeyFromSpki _context bytes = + Gren.Kernel.Crypto.importEd25519Key + "public" + "spki" + bytes + True + [ "verify" ] + + +{-|-} +importEd25519PublicKeyFromJwk : SecureContext -> Json.Encode.Value -> Task ImportEd25519KeyError (PublicKey Ed25519Key {}) +importEd25519PublicKeyFromJwk _context jwk = + Gren.Kernel.Crypto.importEd25519Key + "public" + "jwk" + (Gren.Kernel.Json.unwrap jwk) + True + [ "verify" ] + + +{-|-} +importEd25519PrivateKeyFromPkcs8 : SecureContext -> Extractable -> Bytes -> Task ImportEd25519KeyError (PrivateKey Ed25519Key {}) +importEd25519PrivateKeyFromPkcs8 _context extractable bytes = + Gren.Kernel.Crypto.importEd25519Key + "private" + "pkcs8" + bytes + (extractableToBool extractable) + [ "sign" ] + + +{-|-} +importEd25519PrivateKeyFromJwk : SecureContext -> Extractable -> Json.Encode.Value -> Task ImportEd25519KeyError (PrivateKey Ed25519Key {}) +importEd25519PrivateKeyFromJwk _context extractable jwk = + Gren.Kernel.Crypto.importEd25519Key + "private" + "jwk" + (Gren.Kernel.Json.unwrap jwk) + (extractableToBool extractable) + [ "sign" ] + + {-| Errors that can happen when importing a key using an HMAC algorithm. There are three known reasons an errors can happen when importing HMAC keys: @@ -2030,7 +2201,21 @@ signWithEcdsa : DigestAlgorithm -> PrivateKey EcdsaKey EcKeyParams -> Bytes -> T signWithEcdsa hash (PrivateKey (Key { key })) bytes = Gren.Kernel.Crypto.signWithEcdsa (digestAlgorithmToString hash) - key + key + bytes + + +{-| Sign some `Bytes` with the Ed25519 algorithm. This produces a `Signature` +(which is just some `Bytes`). The `Signature` can be used with the +corresponding verification function to verify that the passed `Bytes` were +signed with the passed key. + +The returned `Task` should not fail. If it does, please file a ticket! +-} +signWithEd25519 : PrivateKey Ed25519Key {} -> Bytes -> Task x Signature +signWithEd25519 (PrivateKey (Key { key })) bytes = + Gren.Kernel.Crypto.signWithEd25519 + key bytes @@ -2095,7 +2280,21 @@ verifyWithEcdsa hash (PublicKey (Key { key })) signature bytes = bytes -{-| Verify that some `Bytes` were signed with the passed `Signature` with the +{-| Verify that some `Bytes` were signed with the passed `Signature` with the +Ed25519 algorithm. + +The `Task` succeeds with the verified `Bytes` if the passed signature is valid +and fails otherwise. +-} +verifyWithEd25519 : PublicKey Ed25519Key {} -> Signature -> Bytes -> Task {} Bytes +verifyWithEd25519 (PublicKey (Key { key })) signature bytes = + Gren.Kernel.Crypto.verifyWithEd25519 + key + signature + bytes + + +{-| Verify that some `Bytes` were signed with the passed `Signature` with the HMAC algorithm. The `Task` succeeds with the verified `Bytes` if the passed signature is valid and diff --git a/src/Gren/Kernel/Crypto.js b/src/Gren/Kernel/Crypto.js index ebe7b07d..8b6ebc3a 100644 --- a/src/Gren/Kernel/Crypto.js +++ b/src/Gren/Kernel/Crypto.js @@ -2,7 +2,7 @@ import Gren.Kernel.Scheduler exposing (binding, succeed, fail) import Gren.Kernel.Bytes exposing (writeBytes) -import Crypto exposing (RsaSsaPkcs1V1_5SigningError, RsaPssSigningError, AesCtrEncryptionError, RsaOaepEncryptionError, RsaOaepDecryptionError, P256, P384, P521, AesLength128, AesLength192, AesLength256, CanBeExtracted, CannotBeExtracted, HmacKey, Sha256, Sha384, Sha512, SignWithRsaPssError, AesGcmDecryptionError, AesGcmEncryptionError, AesCbcDecryptionError, AesCbcEncryptionError, AesCtrDecryptionError, DecryptWithRsaOaepError, ImportRsaKeyError, ImportHmacKeyError, ImportEcKeyError, ImportAesKeyError, Key, SecureContext, PublicKey, PrivateKey, KeyNotExportable) +import Crypto exposing (RsaSsaPkcs1V1_5SigningError, RsaPssSigningError, AesCtrEncryptionError, RsaOaepEncryptionError, RsaOaepDecryptionError, P256, P384, P521, AesLength128, AesLength192, AesLength256, CanBeExtracted, CannotBeExtracted, HmacKey, Ed25519Key, Sha256, Sha384, Sha512, SignWithRsaPssError, AesGcmDecryptionError, AesGcmEncryptionError, AesCbcDecryptionError, AesCbcEncryptionError, AesCtrDecryptionError, DecryptWithRsaOaepError, ImportRsaKeyError, ImportHmacKeyError, ImportEcKeyError, ImportEd25519KeyError, ImportEd25519KeyNotSupported, Ed25519KeyGenerationNotSupported, ImportAesKeyError, Key, SecureContext, PublicKey, PrivateKey, KeyNotExportable) import Maybe exposing (Just, Nothing) import Bytes exposing (Bytes) @@ -103,6 +103,13 @@ var _Crypto_constructEcKey = function (key) { }); }; +var _Crypto_constructEd25519Key = function (key) { + return __Crypto_Key({ + __$key: key, + __$data: {}, + }); +}; + // Random var _Crypto_randomUUID = __Scheduler_binding(function (callback) { @@ -236,6 +243,33 @@ var _Crypto_generateEcKey = F4( }, ); +var _Crypto_generateEd25519Key = F2(function (extractable, permissions) { + return __Scheduler_binding(function (callback) { + var algorithm = { + name: "Ed25519", + }; + _Crypto_impl.subtle + .generateKey(algorithm, extractable, permissions) + .then(function (key) { + return callback( + __Scheduler_succeed({ + __$publicKey: __Crypto_PublicKey( + _Crypto_constructEd25519Key(key.publicKey), + ), + __$privateKey: __Crypto_PrivateKey( + _Crypto_constructEd25519Key(key.privateKey), + ), + }), + ); + }) + .catch(function (err) { + return callback( + __Scheduler_fail(__Crypto_Ed25519KeyGenerationNotSupported), + ); + }); + }); +}); + var _Crypto_generateHmacKey = F5( function (name, hash, length, extractable, permissions) { return __Scheduler_binding(function (callback) { @@ -385,6 +419,44 @@ var _Crypto_importEcKey = F7( }, ); +var _Crypto_importEd25519Key = F5( + function (wrapper, format, keyData, extractable, keyUsages) { + return __Scheduler_binding(function (callback) { + _Crypto_impl.subtle + .importKey( + format, + keyData, + { name: "Ed25519" }, + extractable, + keyUsages, + ) + .then(function (key) { + switch (wrapper) { + case "public": + return callback( + __Scheduler_succeed( + __Crypto_PublicKey(_Crypto_constructEd25519Key(key)), + ), + ); + case "private": + return callback( + __Scheduler_succeed( + __Crypto_PrivateKey(_Crypto_constructEd25519Key(key)), + ), + ); + default: + return callback( + __Scheduler_fail(__Crypto_ImportEd25519KeyError), + ); + } + }) + .catch(function (err) { + return callback(__Scheduler_fail(__Crypto_ImportEd25519KeyError)); + }); + }); + }, +); + var _Crypto_importHmacKey = F7( function ( format, @@ -651,6 +723,22 @@ var _Crypto_signWithEcdsa = F3(function (hash, key, bytes) { }); }); +var _Crypto_signWithEd25519 = F2(function (key, bytes) { + return __Scheduler_binding(function (callback) { + var algorithm = { + name: "Ed25519", + }; + _Crypto_impl.subtle + .sign(algorithm, key, bytes) + .then(function (res) { + return callback(__Scheduler_succeed(new DataView(res))); + }) + .catch(function (err) { + throw "There was an unforseen error that occured when attempting to sign using the Ed25519 algorithm. This shouldn't happen! Please file a ticket in the `gren-lang/core` Github repo (https://github.com/gren-lang/core)"; + }); + }); +}); + var _Crypto_signWithHmac = F2(function (key, bytes) { return __Scheduler_binding(function (callback) { var algorithm = { @@ -728,6 +816,25 @@ var _Crypto_verifyWithEcdsa = F4(function (hash, key, signature, bytes) { }); }); +var _Crypto_verifyWithEd25519 = F3(function (key, signature, bytes) { + return __Scheduler_binding(function (callback) { + var algorithm = { + name: "Ed25519", + }; + _Crypto_impl.subtle + .verify(algorithm, key, signature, bytes) + .then(function (res) { + if (res) { + return callback(__Scheduler_succeed(bytes)); + } + return callback(__Scheduler_fail()); + }) + .catch(function (err) { + throw "There was an unforseen error that occured when attempting to verify with the Ed25519 algorithm. This shouldn't happen! Please file a ticket in the `gren-lang/core` Github repo (https://github.com/gren-lang/core)"; + }); + }); +}); + var _Crypto_verifyWithHmac = F3(function (key, signature, bytes) { return __Scheduler_binding(function (callback) { var algorithm = { From a194ee0f6b0cf702eb39fc08bbddb244799148bc Mon Sep 17 00:00:00 2001 From: Joey Bright Date: Sat, 2 May 2026 17:01:50 -0700 Subject: [PATCH 2/6] Detecting support for ed25519 key generation and not failing tests if not explicitly supported by the runtime --- integration_tests/src/Test/Crypto.gren | 180 ++++++++++++++----------- 1 file changed, 99 insertions(+), 81 deletions(-) diff --git a/integration_tests/src/Test/Crypto.gren b/integration_tests/src/Test/Crypto.gren index 781c4f5a..c28f13ff 100644 --- a/integration_tests/src/Test/Crypto.gren +++ b/integration_tests/src/Test/Crypto.gren @@ -2510,100 +2510,118 @@ ecdsaTestsHelper secureContext label { namedCurve, digestAlgorithm } = {-|-} ed25519Tests secureContext = + await + (Crypto.generateEd25519KeyPair secureContext Crypto.CannotBeExtracted + |> Task.map Just + |> Task.onError + (\err -> + when err is + Crypto.Ed25519KeyGenerationNotSupported -> + Task.succeed Nothing + ) + ) + "Ed25519: Checking runtime support" + (\maybeKeyPair -> + when maybeKeyPair is + Just keyPair -> + ed25519SupportedTests secureContext keyPair + + Nothing -> + test "Ed25519 is not supported by this runtime, so Ed25519 tests are skipped" + (\_ -> + Expect.pass + ) + ) + + +{-|-} +ed25519SupportedTests secureContext { publicKey, privateKey } = concat - [ await - (Crypto.generateEd25519KeyPair - secureContext - Crypto.CannotBeExtracted - ) - "Ed25519: Generating a key that cannot be extracted" - (\{ publicKey, privateKey } -> - concat - [ await - (Crypto.exportEd25519PublicKeyAsRaw publicKey) - "Exporting public Ed25519 key as RAW when not extractable" - (\exportedKey -> - await - (Crypto.importEd25519PublicKeyFromRaw - secureContext - exportedKey - ) - "Importing the exported key" - (\importedKey -> - test "Imported key equals the original key" - (\_ -> - Expect.equal importedKey publicKey - ) - ) + [ describe "Ed25519: Generated a key that cannot be extracted" + [ await + (Crypto.exportEd25519PublicKeyAsRaw publicKey) + "Exporting public Ed25519 key as RAW when not extractable" + (\exportedKey -> + await + (Crypto.importEd25519PublicKeyFromRaw + secureContext + exportedKey ) - , await - (Crypto.exportEd25519PublicKeyAsSpki publicKey) - "Exporting public Ed25519 key as SPKI when not extractable" - (\exportedKey -> - await - (Crypto.importEd25519PublicKeyFromSpki - secureContext - exportedKey - ) - "Importing the exported key" - (\importedKey -> - test "Imported key equals the original key" - (\_ -> - Expect.equal importedKey publicKey - ) + "Importing the exported key" + (\importedKey -> + test "Imported key equals the original key" + (\_ -> + Expect.equal importedKey publicKey ) ) - , await - (Crypto.exportEd25519PublicKeyAsJwk publicKey) - "Exporting public Ed25519 key as JWK when not extractable" - (\exportedKey -> - await - (Crypto.importEd25519PublicKeyFromJwk - secureContext - exportedKey - ) - "Importing the exported key" - (\importedKey -> - test "Imported key equals the original key" - (\_ -> - Expect.equal importedKey publicKey - ) - ) + ) + , await + (Crypto.exportEd25519PublicKeyAsSpki publicKey) + "Exporting public Ed25519 key as SPKI when not extractable" + (\exportedKey -> + await + (Crypto.importEd25519PublicKeyFromSpki + secureContext + exportedKey ) - , awaitError - (Crypto.exportEd25519PrivateKeyAsPkcs8 privateKey) - "Exporting private Ed25519 key as PKCS8 when not extractable" - (\err -> - test "When exporting private key that isn't extractable, fails with expected message" + "Importing the exported key" + (\importedKey -> + test "Imported key equals the original key" (\_ -> - Expect.equal err Crypto.KeyNotExportable + Expect.equal importedKey publicKey ) ) - , awaitError - (Crypto.exportEd25519PrivateKeyAsJwk privateKey) - "Exporting private Ed25519 key as JWK when not extractable" - (\err -> - test "When exporting private key that isn't extractable, fails with expected message" + ) + , await + (Crypto.exportEd25519PublicKeyAsJwk publicKey) + "Exporting public Ed25519 key as JWK when not extractable" + (\exportedKey -> + await + (Crypto.importEd25519PublicKeyFromJwk + secureContext + exportedKey + ) + "Importing the exported key" + (\importedKey -> + test "Imported key equals the original key" (\_ -> - Expect.equal err Crypto.KeyNotExportable + Expect.equal importedKey publicKey ) ) - , await - (Crypto.signWithEd25519 privateKey (Bytes.fromString "hello")) - "Signing some bytes with a non-extractable key" - (\signature -> - await - (Crypto.verifyWithEd25519 publicKey signature (Bytes.fromString "hello")) - "Verifying signed bytes" - (\verifiedBytes -> - test "The verified bytes equal the signed bytes" - (\_ -> - Expect.equal verifiedBytes (Bytes.fromString "hello") - ) + ) + , awaitError + (Crypto.exportEd25519PrivateKeyAsPkcs8 privateKey) + "Exporting private Ed25519 key as PKCS8 when not extractable" + (\err -> + test "When exporting private key that isn't extractable, fails with expected message" + (\_ -> + Expect.equal err Crypto.KeyNotExportable + ) + ) + , awaitError + (Crypto.exportEd25519PrivateKeyAsJwk privateKey) + "Exporting private Ed25519 key as JWK when not extractable" + (\err -> + test "When exporting private key that isn't extractable, fails with expected message" + (\_ -> + Expect.equal err Crypto.KeyNotExportable + ) + ) + , await + (Crypto.signWithEd25519 privateKey (Bytes.fromString "hello")) + "Signing some bytes with a non-extractable key" + (\signature -> + await + (Crypto.verifyWithEd25519 publicKey signature (Bytes.fromString "hello")) + "Verifying signed bytes" + (\verifiedBytes -> + test "The verified bytes equal the signed bytes" + (\_ -> + Expect.equal verifiedBytes (Bytes.fromString "hello") ) ) - ] - ) + ) + ] , await (Crypto.generateEd25519KeyPair secureContext From 184ba643a276b49c1167643651e3dba9debc00a2 Mon Sep 17 00:00:00 2001 From: Joey Bright Date: Sun, 3 May 2026 09:49:47 -0700 Subject: [PATCH 3/6] Added comment to better explain platform compatibility check --- integration_tests/src/Test/Crypto.gren | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/integration_tests/src/Test/Crypto.gren b/integration_tests/src/Test/Crypto.gren index c28f13ff..8ff32ec2 100644 --- a/integration_tests/src/Test/Crypto.gren +++ b/integration_tests/src/Test/Crypto.gren @@ -2510,6 +2510,10 @@ ecdsaTestsHelper secureContext label { namedCurve, digestAlgorithm } = {-|-} ed25519Tests secureContext = + -- Checks to see if generating a key pair succeeds. This assumes that the platform that's + -- running the tests supports this algorithm and other tests can be run. If this fails, + -- it's assumed the platform does not support the algorithm and does not run the test that + -- will, ultimately, fail. await (Crypto.generateEd25519KeyPair secureContext Crypto.CannotBeExtracted |> Task.map Just From a4c139752b0b75b6aee80c813d3d8a31f6000d75 Mon Sep 17 00:00:00 2001 From: Joey Bright Date: Sun, 3 May 2026 09:56:55 -0700 Subject: [PATCH 4/6] Removed unnecessary comment --- src/Crypto.gren | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Crypto.gren b/src/Crypto.gren index a95e3867..31415bb1 100644 --- a/src/Crypto.gren +++ b/src/Crypto.gren @@ -817,10 +817,6 @@ generateEcdhKeyPair _context { namedCurve, extractable } = {-| A key generated for use with the Ed25519 algorithm. Used to sign and verify values. - -Unlike ECDSA, Ed25519 has no curve or hash parameters: the algorithm fully -specifies the curve (Curve25519, in Edwards form) and the hash function (SHA-512) -internally. -} type Ed25519Key = Ed25519Key From 6ed8feaac6ba8139ee755490fb9f3de26844aa29 Mon Sep 17 00:00:00 2001 From: Joey Bright Date: Wed, 6 May 2026 17:04:08 -0700 Subject: [PATCH 5/6] Ran `devbox format` --- src/Gren/Kernel/Crypto.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/Gren/Kernel/Crypto.js b/src/Gren/Kernel/Crypto.js index 8b6ebc3a..f1bacf5e 100644 --- a/src/Gren/Kernel/Crypto.js +++ b/src/Gren/Kernel/Crypto.js @@ -423,13 +423,7 @@ var _Crypto_importEd25519Key = F5( function (wrapper, format, keyData, extractable, keyUsages) { return __Scheduler_binding(function (callback) { _Crypto_impl.subtle - .importKey( - format, - keyData, - { name: "Ed25519" }, - extractable, - keyUsages, - ) + .importKey(format, keyData, { name: "Ed25519" }, extractable, keyUsages) .then(function (key) { switch (wrapper) { case "public": @@ -445,9 +439,7 @@ var _Crypto_importEd25519Key = F5( ), ); default: - return callback( - __Scheduler_fail(__Crypto_ImportEd25519KeyError), - ); + return callback(__Scheduler_fail(__Crypto_ImportEd25519KeyError)); } }) .catch(function (err) { From acdc2ab170707193994595699753ecc84484374d Mon Sep 17 00:00:00 2001 From: Joey Bright Date: Wed, 6 May 2026 17:46:37 -0700 Subject: [PATCH 6/6] Fixing shadowing error in integration tests --- integration_tests/src/Test/Crypto.gren | 28 +++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/integration_tests/src/Test/Crypto.gren b/integration_tests/src/Test/Crypto.gren index 8ff32ec2..2b4d4d47 100644 --- a/integration_tests/src/Test/Crypto.gren +++ b/integration_tests/src/Test/Crypto.gren @@ -2632,14 +2632,14 @@ ed25519SupportedTests secureContext { publicKey, privateKey } = Crypto.CanBeExtracted ) "Ed25519: Generating a key that can be extracted" - (\{ publicKey, privateKey } -> + (\extractableKey -> let bytesToSign = Bytes.fromString "hello" in concat [ await - (Crypto.exportEd25519PublicKeyAsRaw publicKey) + (Crypto.exportEd25519PublicKeyAsRaw extractableKey.publicKey) "Exporting public Ed25519 key as RAW when extractable" (\exportedKey -> await @@ -2651,12 +2651,12 @@ ed25519SupportedTests secureContext { publicKey, privateKey } = (\importedKey -> test "Imported key equals the original key" (\_ -> - Expect.equal importedKey publicKey + Expect.equal importedKey extractableKey.publicKey ) ) ) , await - (Crypto.exportEd25519PublicKeyAsSpki publicKey) + (Crypto.exportEd25519PublicKeyAsSpki extractableKey.publicKey) "Exporting public Ed25519 key as SPKI when extractable" (\exportedKey -> await @@ -2668,12 +2668,12 @@ ed25519SupportedTests secureContext { publicKey, privateKey } = (\importedKey -> test "Imported key equals the original key" (\_ -> - Expect.equal importedKey publicKey + Expect.equal importedKey extractableKey.publicKey ) ) ) , await - (Crypto.exportEd25519PublicKeyAsJwk publicKey) + (Crypto.exportEd25519PublicKeyAsJwk extractableKey.publicKey) "Exporting public Ed25519 key as JWK when extractable" (\exportedKey -> await @@ -2685,12 +2685,12 @@ ed25519SupportedTests secureContext { publicKey, privateKey } = (\importedKey -> test "Imported key equals the original key" (\_ -> - Expect.equal importedKey publicKey + Expect.equal importedKey extractableKey.publicKey ) ) ) , await - (Crypto.exportEd25519PrivateKeyAsPkcs8 privateKey) + (Crypto.exportEd25519PrivateKeyAsPkcs8 extractableKey.privateKey) "Exporting private Ed25519 key as PKCS8 when extractable" (\exportedKey -> await @@ -2703,12 +2703,12 @@ ed25519SupportedTests secureContext { publicKey, privateKey } = (\importedKey -> test "Imported key equals the original key" (\_ -> - Expect.equal importedKey privateKey + Expect.equal importedKey extractableKey.privateKey ) ) ) , await - (Crypto.exportEd25519PrivateKeyAsJwk privateKey) + (Crypto.exportEd25519PrivateKeyAsJwk extractableKey.privateKey) "Exporting private Ed25519 key as JWK when extractable" (\exportedKey -> await @@ -2721,17 +2721,17 @@ ed25519SupportedTests secureContext { publicKey, privateKey } = (\importedKey -> test "Imported key equals the original key" (\_ -> - Expect.equal importedKey privateKey + Expect.equal importedKey extractableKey.privateKey ) ) ) , await - (Crypto.signWithEd25519 privateKey bytesToSign) + (Crypto.signWithEd25519 extractableKey.privateKey bytesToSign) "Sign some bytes" (\signature -> concat [ await - (Crypto.verifyWithEd25519 publicKey signature bytesToSign) + (Crypto.verifyWithEd25519 extractableKey.publicKey signature bytesToSign) "Verifying signed bytes" (\verifiedBytes -> test "The verified bytes are the same as the signed bytes" @@ -2740,7 +2740,7 @@ ed25519SupportedTests secureContext { publicKey, privateKey } = ) ) , awaitError - (Crypto.verifyWithEd25519 publicKey signature (Bytes.fromString "some other bytes")) + (Crypto.verifyWithEd25519 extractableKey.publicKey signature (Bytes.fromString "some other bytes")) "Verifying bytes that were not signed" (\_ -> test "Fails when bytes that are being verified are different than what was signed"