diff --git a/Makefile b/Makefile index e9f117a25f0..2c301f9fac9 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,9 @@ all: clean format tests build build: go build +build-debug: + go build -gcflags="all=-N -l" + ## format: Applies Go formatting to code. format: find . -name '*.go' -exec gofmt -s -w {} + diff --git a/examples/webcrypto/generateKey/generateKey-ed25519.js b/examples/webcrypto/generateKey/generateKey-ed25519.js new file mode 100644 index 00000000000..6ac90684c2f --- /dev/null +++ b/examples/webcrypto/generateKey/generateKey-ed25519.js @@ -0,0 +1,11 @@ +export default async function () { + const ed25519KeyPair = await crypto.subtle.generateKey( + { + name: "Ed25519", + }, + true, + ["sign", "verify"] + ); + + console.log("ed25519 key pair: " + JSON.stringify(ed25519KeyPair)); +} \ No newline at end of file diff --git a/examples/webcrypto/generateKey/generateKey-x25519.js b/examples/webcrypto/generateKey/generateKey-x25519.js new file mode 100644 index 00000000000..29b772e7657 --- /dev/null +++ b/examples/webcrypto/generateKey/generateKey-x25519.js @@ -0,0 +1,12 @@ +export default async function () { + const key = await crypto.subtle.generateKey( + { + name: "ECDH", + namedCurve: "X25519", + }, + true, + ["deriveKey", "deriveBits"] + ); + + console.log(JSON.stringify(key)); +} \ No newline at end of file diff --git a/examples/webcrypto/import_export/export-ed25519-keys.js b/examples/webcrypto/import_export/export-ed25519-keys.js new file mode 100644 index 00000000000..885268e7ecc --- /dev/null +++ b/examples/webcrypto/import_export/export-ed25519-keys.js @@ -0,0 +1,33 @@ +export default async function () { + const generatedKeyPair = await crypto.subtle.generateKey( + { + name: "Ed25519", + }, + true, + ["sign", "verify"] + ); + + const exportedPrivateKey = await crypto.subtle.exportKey( + "pkcs8", + generatedKeyPair.privateKey + ); + console.log("exported private key: " + printArrayBuffer(exportedPrivateKey)); + + const exportedRawPublicKey = await crypto.subtle.exportKey( + "raw", + generatedKeyPair.publicKey + ); + + const exportedSpkiPublicKey = await crypto.subtle.exportKey( + "spki", + generatedKeyPair.publicKey + ); + + console.log("exported public key: " + printArrayBuffer(exportedRawPublicKey)); + console.log("exported spki public key: " + printArrayBuffer(exportedSpkiPublicKey)); +} + +const printArrayBuffer = (buffer) => { + let view = new Uint8Array(buffer); + return Array.from(view); +}; diff --git a/examples/webcrypto/import_export/import-ed25519-keys.js b/examples/webcrypto/import_export/import-ed25519-keys.js new file mode 100644 index 00000000000..17a4dddc1e4 --- /dev/null +++ b/examples/webcrypto/import_export/import-ed25519-keys.js @@ -0,0 +1,33 @@ +export default async function () { + const publicKey = await crypto.subtle.importKey( + "raw", + aliceRawPublicKeyData, + { name: "Ed25519" }, + true, + ["verify"] + ); + + const privateKey = await crypto.subtle.importKey( + "pkcs8", + alicePkcs8PrivateKeyData, + { name: "Ed25519" }, + true, + ["sign"] + ); + + const spkiPublicKey = await crypto.subtle.importKey( + "spki", + spkiPublicKeyData, + { name: "Ed25519" }, + true, + ["verify"] + ); + + console.log("raw public key: ", JSON.stringify(publicKey)); + console.log("pkcs8 private key: ", JSON.stringify(privateKey)); + console.log("spki public key: ", JSON.stringify(spkiPublicKey)); +} + +const aliceRawPublicKeyData = new Uint8Array([20,143,11,228,219,143,240,246,228,95,189,140,34,196,138,241,105,163,220,110,81,16,167,243,77,251,70,100,130,131,153,43]) +const alicePkcs8PrivateKeyData = new Uint8Array([48,46,2,1,0,48,5,6,3,43,101,112,4,34,4,32,235,89,226,177,105,103,230,133,229,2,157,78,107,14,0,197,81,149,209,139,6,37,80,98,219,50,0,38,144,234,156,194]) +const spkiPublicKeyData = new Uint8Array([48,42,48,5,6,3,43,101,112,3,33,0,210,238,42,158,126,130,110,253,80,77,38,242,209,88,172,114,11,120,31,243,24,171,47,144,217,186,184,71,152,40,110,168]) \ No newline at end of file diff --git a/examples/webcrypto/import_export/import-export-jwk-ed25519.js b/examples/webcrypto/import_export/import-export-jwk-ed25519.js new file mode 100644 index 00000000000..33da537d83b --- /dev/null +++ b/examples/webcrypto/import_export/import-export-jwk-ed25519.js @@ -0,0 +1,43 @@ +export default async function () { + const publicJwk = { + "kty":"OKP", + "crv":"Ed25519", + "x":"o7RbBVJW_6Ua3h5J3MCEGAeXRC6xHvtotIiAadK-xbM", + "key_ops":["verify"], + "ext":true + }; + + const privateJwk = { + "kty": "OKP", + crv: "Ed25519", + x: "o7RbBVJW_6Ua3h5J3MCEGAeXRC6xHvtotIiAadK-xbM", + d: "lHnUA3j3VmVOCYuF4nzEgbQ9QnaBNXXTLIK45adoyEmjtFsFUlb_pRreHkncwIQYB5dELrEe-2i0iIBp0r7Fsw", + key_ops: ["sign"], + ext: true + } + + const publicKey = await crypto.subtle.importKey( + "jwk", + publicJwk, + { name: "Ed25519" }, + true, + ["verify"] + ); + + const privateKey = await crypto.subtle.importKey( + "jwk", + privateJwk, + { name: "Ed25519" }, + true, + ["sign"] + ); + + console.log("public key: " + JSON.stringify(publicKey)); + console.log("private key: " + JSON.stringify(privateKey)); + + const exportedPublicJwk = await crypto.subtle.exportKey("jwk", publicKey); + console.log("exported public jwk: " + JSON.stringify(exportedPublicJwk)); + + const exportedPrivateJwk = await crypto.subtle.exportKey("jwk", privateKey); + console.log("exported private jwk: " + JSON.stringify(exportedPrivateJwk)); +} \ No newline at end of file diff --git a/examples/webcrypto/sign_verify/sign-verify-ed25519.js b/examples/webcrypto/sign_verify/sign-verify-ed25519.js new file mode 100644 index 00000000000..fd3dd41f550 --- /dev/null +++ b/examples/webcrypto/sign_verify/sign-verify-ed25519.js @@ -0,0 +1,44 @@ +import { crypto } from "k6/experimental/webcrypto"; + +export default async function () { + const keyPair = await crypto.subtle.generateKey( + { + name: "Ed25519", + }, + true, + ["sign", "verify"] + ); + + const data = string2ArrayBuffer("Hello World"); + + const alg = { name: "Ed25519" }; + + // makes a signature of the encoded data with the provided key + const signature = await crypto.subtle.sign(alg, keyPair.privateKey, data); + + console.log("signature: ", printArrayBuffer(signature)); + + //Verifies the signature of the encoded data with the provided key + const verified = await crypto.subtle.verify( + alg, + otherKeyPair.publicKey, + signature, + data + ); + + console.log("verified: ", verified); +} + +const string2ArrayBuffer = (str) => { + let buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char + let bufView = new Uint16Array(buf); + for (let i = 0, strLen = str.length; i < strLen; i++) { + bufView[i] = str.charCodeAt(i); + } + return buf; +}; + +const printArrayBuffer = (buffer) => { + let view = new Uint8Array(buffer); + return Array.from(view); +}; diff --git a/internal/js/modules/k6/webcrypto/algorithm.go b/internal/js/modules/k6/webcrypto/algorithm.go index 9b9df5b4767..0ddbd2cea9c 100644 --- a/internal/js/modules/k6/webcrypto/algorithm.go +++ b/internal/js/modules/k6/webcrypto/algorithm.go @@ -52,6 +52,12 @@ const ( // ECDH represents the ECDH algorithm. ECDH = "ECDH" + + // Ed25519 represents the Ed25519 algorithm. + Ed25519 = "ED25519" // TODO: This should be "Ed25519" + + // X25519 represents the X25519 algorithm. + X25519 = "X25519" ) // HashAlgorithmIdentifier represents the name of a hash algorithm. @@ -187,16 +193,18 @@ func isRegisteredAlgorithm(algorithmName string, forOperation string) bool { isHashAlgorithm(algorithmName) || algorithmName == HMAC || isEllipticCurve(algorithmName) || - isRSAAlgorithm(algorithmName) + isRSAAlgorithm(algorithmName) || + algorithmName == Ed25519 case OperationIdentifierExportKey, OperationIdentifierImportKey: return isAesAlgorithm(algorithmName) || algorithmName == HMAC || isEllipticCurve(algorithmName) || - isRSAAlgorithm(algorithmName) + isRSAAlgorithm(algorithmName) || + algorithmName == Ed25519 case OperationIdentifierEncrypt, OperationIdentifierDecrypt: return isAesAlgorithm(algorithmName) || algorithmName == RSAOaep case OperationIdentifierSign, OperationIdentifierVerify: - return algorithmName == HMAC || algorithmName == ECDSA || algorithmName == RSAPss || algorithmName == RSASsaPkcs1v15 + return algorithmName == HMAC || algorithmName == ECDSA || algorithmName == RSAPss || algorithmName == RSASsaPkcs1v15 || algorithmName == Ed25519 default: return false } @@ -221,5 +229,5 @@ type hasAlg interface { } func isEllipticCurve(algorithmName string) bool { - return algorithmName == ECDH || algorithmName == ECDSA + return algorithmName == ECDH || algorithmName == ECDSA || algorithmName == X25519 } diff --git a/internal/js/modules/k6/webcrypto/ed25519.go b/internal/js/modules/k6/webcrypto/ed25519.go new file mode 100644 index 00000000000..aedb3572b49 --- /dev/null +++ b/internal/js/modules/k6/webcrypto/ed25519.go @@ -0,0 +1,319 @@ +package webcrypto + +import ( + "crypto/ed25519" + "crypto/rand" + "crypto/x509" + "encoding/json" + "fmt" +) + +// Ed25519KeyGenParams represents the object that should be passed as the algorithm +// paramter into `SubtleCrypto.GenerateKey`, when generating an Ed25519 key pair. +// The Ed25519 key generation expects only the algorithm type as a parameter. +type Ed25519KeyGenParams struct { + Algorithm +} + +var _ KeyGenerator = &Ed25519KeyGenParams{} + +func newEd25519KeyGenParams(normalized Algorithm) KeyGenerator { + return &Ed25519KeyGenParams{ + Algorithm: normalized, + } +} + +func (kgp *Ed25519KeyGenParams) GenerateKey(extractable bool, keyUsages []CryptoKeyUsage) (CryptoKeyGenerationResult, error) { + rawPublicKey, rawPrivateKey, err := generateEd25519KeyPair(keyUsages) + if err != nil { + return nil, err + } + + alg := KeyAlgorithm{ + Algorithm: kgp.Algorithm, + } + privateKey := &CryptoKey{ + Type: PrivateCryptoKeyType, + Extractable: extractable, + Algorithm: alg, + Usages: UsageIntersection( + keyUsages, + []CryptoKeyUsage{SignCryptoKeyUsage}, + ), + handle: rawPrivateKey, + } + + publicKey := &CryptoKey{ + Type: PublicCryptoKeyType, + Extractable: true, + Algorithm: alg, + Usages: UsageIntersection( + keyUsages, + []CryptoKeyUsage{VerifyCryptoKeyUsage}, + ), + handle: rawPublicKey, + } + + return &CryptoKeyPair{ + PrivateKey: privateKey, + PublicKey: publicKey, + }, nil +} + +func generateEd25519KeyPair(keyUsages []CryptoKeyUsage) (ed25519.PublicKey, ed25519.PrivateKey, error) { + for _, usage := range keyUsages { + switch usage { + case SignCryptoKeyUsage, VerifyCryptoKeyUsage: + continue + default: + return nil, nil, NewError(SyntaxError, fmt.Sprintf("Invalid key usage: %s", usage)) + } + } + + return ed25519.GenerateKey(rand.Reader) +} + +type ed25519SignerVerifier struct{} + +func (ed25519SignerVerifier) Sign(key CryptoKey, data []byte) ([]byte, error) { + if key.Type != PrivateCryptoKeyType { + return nil, NewError(InvalidAccessError, "Must use private key to sign data") + } + + keyHandle, ok := key.handle.(ed25519.PrivateKey) + if !ok { + return nil, NewError(InvalidAccessError, "Key handle is not an Ed25519 Private Key") + } + + return ed25519.Sign(keyHandle, data), nil +} + +func (ed25519SignerVerifier) Verify(key CryptoKey, signature, data []byte) (bool, error) { + if key.Type != PublicCryptoKeyType { + return false, NewError(InvalidAccessError, "Must use public key to verify data") + } + + keyHandle, ok := key.handle.(ed25519.PublicKey) + if !ok { + return false, NewError(InvalidAccessError, "Key handle is not an Ed25519 public key") + } + + // TODO: verify that the ed25519 library conducts small-order checks, if not add them here + + return ed25519.Verify(keyHandle, data, signature), nil +} + +// Ed25519 is an internal placeholder struct for Ed25519 import parameters. +// Although not described by the specification, we define it to be able to implement +// our internal KeyImporter interface. +type Ed25519ImportParams struct { + Algorithm +} + +func newEd25519ImportParams(normalized Algorithm) *Ed25519ImportParams { + return &Ed25519ImportParams{ + Algorithm: normalized, + } +} + +func (eip *Ed25519ImportParams) ImportKey(format KeyFormat, keyData []byte, keyUsages []CryptoKeyUsage) (*CryptoKey, error) { + var importFn func(keyData []byte, keyUsages []CryptoKeyUsage) (any, CryptoKeyType, error) + + switch format { + case SpkiKeyFormat: + importFn = importEd25519Spki + case Pkcs8KeyFormat: + importFn = importEd25519Pkcs8 + case JwkKeyFormat: + importFn = importEd25519Jwk + case RawKeyFormat: + importFn = importEd25519Raw + default: + return nil, NewError(NotSupportedError, unsupportedKeyFormatErrorMsg+" "+format+" for algorithm "+eip.Algorithm.Name) + } + + handle, keyType, err := importFn(keyData, keyUsages) + if err != nil { + return nil, err + } + + return &CryptoKey{ + Algorithm: eip.Algorithm, + handle: handle, + Type: keyType, + }, nil +} + +func importEd25519Spki(keyData []byte, keyUsages []CryptoKeyUsage) (any, CryptoKeyType, error) { + for _, usage := range keyUsages { + switch usage { + case VerifyCryptoKeyUsage: + continue + default: + return nil, UnknownCryptoKeyType, NewError(SyntaxError, "invalid key usage: "+usage) + } + } + + parsedKey, err := x509.ParsePKIXPublicKey(keyData) + if err != nil { + return nil, UnknownCryptoKeyType, NewError(DataError, "Unable to import Ed25519 public key data: "+err.Error()) + } + + handle, ok := parsedKey.(ed25519.PublicKey) + if !ok { + return nil, UnknownCryptoKeyType, NewError(DataError, "given key is not an Ed25519 key") + } + + return handle, PublicCryptoKeyType, nil +} + +func importEd25519Pkcs8(keyData []byte, keyUsages []CryptoKeyUsage) (any, CryptoKeyType, error) { + for _, usage := range keyUsages { + switch usage { + case SignCryptoKeyUsage: + continue + default: + return nil, UnknownCryptoKeyType, NewError(SyntaxError, "invalid key usage: "+usage) + } + } + + parsedKey, err := x509.ParsePKCS8PrivateKey(keyData) + if err != nil { + return nil, UnknownCryptoKeyType, NewError(DataError, "unable to import Ed25519 private key data: "+err.Error()) + } + + handle, ok := parsedKey.(ed25519.PrivateKey) + if !ok { + return nil, UnknownCryptoKeyType, NewError(DataError, "given key is not an Ed25519 key") + } + + return handle, PrivateCryptoKeyType, nil +} + +func importEd25519Jwk(keyData []byte, keyUsages []CryptoKeyUsage) (any, CryptoKeyType, error) { + var jwkKey ed25519JWK + if err := json.Unmarshal(keyData, &jwkKey); err != nil { + return nil, UnknownCryptoKeyType, NewError(DataError, "failed to parse input as Ed25519 JWK key: "+err.Error()) + } + + if err := jwkKey.validateEd25519JWK(keyUsages); err != nil { + return nil, UnknownCryptoKeyType, err + } + + // If the 'd' field is not present, the key is public, so return the public key + if jwkKey.D == "" { + xBytes, err := base64URLDecode(jwkKey.X) + if err != nil { + return nil, UnknownCryptoKeyType, NewError(DataError, "failed to decode public key: "+err.Error()) + } + + if len(xBytes) != ed25519.PublicKeySize { + return nil, UnknownCryptoKeyType, NewError(DataError, fmt.Sprintf("invalid Ed25519 public key length: got %d, want %d", len(xBytes), ed25519.PublicKeySize)) + } + + publicKey := ed25519.PublicKey(xBytes) + return publicKey, PublicCryptoKeyType, nil + } + + dBytes, err := base64URLDecode(jwkKey.D) + if err != nil { + return nil, UnknownCryptoKeyType, NewError(DataError, "failed to decode private key: "+err.Error()) + } + + if len(dBytes) != ed25519.PrivateKeySize { + return nil, UnknownCryptoKeyType, NewError(DataError, fmt.Sprintf("invalid Ed25519 private key length: got %d, want %d", len(dBytes), ed25519.PrivateKeySize)) + } + + privateKey := ed25519.PrivateKey(dBytes) + return privateKey, PrivateCryptoKeyType, nil +} + +func importEd25519Raw(keyData []byte, keyUsages []CryptoKeyUsage) (any, CryptoKeyType, error) { + for _, usage := range keyUsages { + switch usage { + case VerifyCryptoKeyUsage: + continue + default: + return nil, UnknownCryptoKeyType, NewError(SyntaxError, fmt.Sprintf("invalid key usage: %s. Only 'verify' is valid for raw Ed25519 keys", usage)) + } + } + + if len(keyData) != ed25519.PublicKeySize { + return nil, UnknownCryptoKeyType, NewError(DataError, fmt.Sprintf("invalid Ed25519 public key length: got %d, want %d", len(keyData), ed25519.PublicKeySize)) + } + + handle := ed25519.PublicKey(keyData) + return handle, PublicCryptoKeyType, nil +} + +func exportEd25519Key(key *CryptoKey, format KeyFormat) (any, error) { + if !key.Extractable { + return nil, NewError(InvalidAccessError, "the key is not extractable") + } + + if key.handle == nil { + return nil, NewError(OperationError, "the key is not valid, no data") + } + + switch format { + case SpkiKeyFormat: + return exportEd25519Spki(key) + case Pkcs8KeyFormat: + return exportEd25519Pkcs8(key) + case JwkKeyFormat: + return exportEd25519JWK(key) + case RawKeyFormat: + return exportEd25519Raw(key) + default: + return nil, NewError(NotSupportedError, unsupportedKeyFormatErrorMsg+" "+format+" for algorithm Ed25519") + } +} + +func exportEd25519Spki(key *CryptoKey) ([]byte, error) { + if key.Type != PublicCryptoKeyType { + return nil, NewError(InvalidAccessError, "Must use public key to export as SPKI") + } + + handle, ok := key.handle.(ed25519.PublicKey) + if !ok { + return nil, NewError(InvalidAccessError, "Key handle is not an Ed25519 public key") + } + + bytes, err := x509.MarshalPKIXPublicKey(handle) + if err != nil { + return nil, NewError(OperationError, "unable to marshal key to SPKI format: "+err.Error()) + } + + return bytes, nil +} + +func exportEd25519Pkcs8(key *CryptoKey) ([]byte, error) { + if key.Type != PrivateCryptoKeyType { + return nil, NewError(InvalidAccessError, "Must use private key to export as PKCS8") + } + + handle, ok := key.handle.(ed25519.PrivateKey) + if !ok { + return nil, NewError(InvalidAccessError, "Key handle is not an Ed25519 private key") + } + + bytes, err := x509.MarshalPKCS8PrivateKey(handle) + if err != nil { + return nil, NewError(OperationError, "unable to marshal key to PKCS8 format: "+err.Error()) + } + + return bytes, nil +} + +func exportEd25519Raw(key *CryptoKey) ([]byte, error) { + if key.Type != PublicCryptoKeyType { + return nil, NewError(InvalidAccessError, "Must use public key to export as raw") + } + + handle, ok := key.handle.(ed25519.PublicKey) + if !ok { + return nil, NewError(InvalidAccessError, "Key handle is not an Ed25519 public key") + } + + return handle, nil +} diff --git a/internal/js/modules/k6/webcrypto/elliptic_curve.go b/internal/js/modules/k6/webcrypto/elliptic_curve.go index d8f6a8621f4..197ae39104b 100644 --- a/internal/js/modules/k6/webcrypto/elliptic_curve.go +++ b/internal/js/modules/k6/webcrypto/elliptic_curve.go @@ -14,9 +14,10 @@ import ( ) const ( - p256Canonical = "P-256" - p384Canonical = "P-384" - p521Canonical = "P-521" + p256Canonical = "P-256" + p384Canonical = "P-384" + p521Canonical = "P-521" + x25519Canonical = "X25519" ) // EcKeyAlgorithm is the algorithm for elliptic curve keys as defined in the [specification]. @@ -162,6 +163,9 @@ const ( // EllipticCurveKindP521 represents the P-521 curve. EllipticCurveKindP521 EllipticCurveKind = "P-521" + + // EllipticCurveKindX25519 represents the X25519 curve. + EllipticCurveKindX25519 EllipticCurveKind = "X25519" ) func (k EllipticCurveKind) String() string { @@ -178,6 +182,8 @@ func IsEllipticCurve(name string) bool { return true case string(EllipticCurveKindP521): return true + case string(EllipticCurveKindX25519): + return true default: return false } @@ -274,7 +280,7 @@ func (ecgp *ECKeyGenParams) GenerateKey( var privateKeyUsages, publicKeyUsages []CryptoKeyUsage switch ecgp.Algorithm.Name { - case ECDH: + case ECDH, X25519: keyPairGenerator = generateECDHKeyPair privateKeyUsages = []CryptoKeyUsage{DeriveKeyCryptoKeyUsage, DeriveBitsCryptoKeyUsage} publicKeyUsages = []CryptoKeyUsage{} @@ -383,7 +389,7 @@ func generateECDSAKeyPair(curve EllipticCurveKind, keyUsages []CryptoKeyUsage) ( // isValidEllipticCurve returns true if the given elliptic curve is supported, func isValidEllipticCurve(curve EllipticCurveKind) bool { - return curve == EllipticCurveKindP256 || curve == EllipticCurveKindP384 || curve == EllipticCurveKindP521 + return curve == EllipticCurveKindP256 || curve == EllipticCurveKindP384 || curve == EllipticCurveKindP521 || curve == EllipticCurveKindX25519 } func pickECDHCurve(k string) (ecdh.Curve, error) { @@ -394,6 +400,8 @@ func pickECDHCurve(k string) (ecdh.Curve, error) { return ecdh.P384(), nil case p521Canonical: return ecdh.P521(), nil + case x25519Canonical: + return ecdh.X25519(), nil default: return nil, errors.New("invalid ECDH curve") } diff --git a/internal/js/modules/k6/webcrypto/jwk.go b/internal/js/modules/k6/webcrypto/jwk.go index 36f12cbcd5c..c58c4f2c6ee 100644 --- a/internal/js/modules/k6/webcrypto/jwk.go +++ b/internal/js/modules/k6/webcrypto/jwk.go @@ -3,12 +3,14 @@ package webcrypto import ( "crypto/ecdh" "crypto/ecdsa" + "crypto/ed25519" "crypto/elliptic" "crypto/rsa" "encoding/json" "errors" "fmt" "math/big" + "slices" ) const ( @@ -426,3 +428,163 @@ func exportRSAJWK(key *CryptoKey) (interface{}, error) { return exported, nil } + +type ed25519JWK struct { + // Key type + Kty string `json:"kty"` + // Canonical Curve + Crv string `json:"crv"` + // Public Key Use + Use string `json:"use"` + // Private scalar + KeyOps []CryptoKeyUsage `json:"key_ops"` + // Public key + X string `json:"x"` + // Private key + D string `json:"d"` + // Extractable + Ext *bool `json:"ext"` +} + +func exportEd25519JWK(key *CryptoKey) (*JsonWebKey, error) { + exported := &JsonWebKey{} + exported.Set("kty", "OKP") + exported.Set("crv", "Ed25519") + switch ed25519Key := key.handle.(type) { + case ed25519.PublicKey: + exported.Set("x", base64URLEncode([]byte(ed25519Key))) + case ed25519.PrivateKey: + exported.Set("x", base64URLEncode([]byte(ed25519Key.Public().(ed25519.PublicKey)))) + exported.Set("d", base64URLEncode([]byte(ed25519Key))) + default: + return nil, fmt.Errorf("key's handle isn't an Ed25519 public/private key, got: %T", key.handle) + } + exported.Set("key_ops", key.Usages) + exported.Set("ext", key.Extractable) + + return exported, nil +} + +// validateKeyOps validates that the key_ops field on the JWK does not conflict +// with the given usages according to the JWK specification +func validateKeyOps(keyOps []CryptoKeyUsage, keyUsages []CryptoKeyUsage) error { + if len(keyOps) == 0 { + return nil + } + + // Check for duplicates in keyOps + seen := make(map[CryptoKeyUsage]bool) + for _, op := range keyOps { + if seen[op] { + return NewError(DataError, "duplicate key operation values are not allowed in key_ops") + } + seen[op] = true + } + + // Validate allowed operation combinations + hasSign := false + hasVerify := false + hasEncrypt := false + hasDecrypt := false + hasWrapKey := false + hasUnwrapKey := false + hasDerive := false // covers both deriveKey and deriveBits + + for _, op := range keyOps { + switch op { + case SignCryptoKeyUsage: + hasSign = true + case VerifyCryptoKeyUsage: + hasVerify = true + case EncryptCryptoKeyUsage: + hasEncrypt = true + case DecryptCryptoKeyUsage: + hasDecrypt = true + case WrapKeyCryptoKeyUsage: + hasWrapKey = true + case UnwrapKeyCryptoKeyUsage: + hasUnwrapKey = true + case DeriveKeyCryptoKeyUsage, DeriveBitsCryptoKeyUsage: + hasDerive = true + default: + // Spec allows for other values, so we don't error here + continue + } + } + + // Check for invalid combinations + validCombos := (hasSign && hasVerify && !hasEncrypt && !hasDecrypt && !hasWrapKey && !hasUnwrapKey && !hasDerive) || + (hasEncrypt && hasDecrypt && !hasSign && !hasVerify && !hasWrapKey && !hasUnwrapKey && !hasDerive) || + (hasWrapKey && hasUnwrapKey && !hasSign && !hasVerify && !hasEncrypt && !hasDecrypt && !hasDerive) || + (hasDerive && !hasSign && !hasVerify && !hasEncrypt && !hasDecrypt && !hasWrapKey && !hasUnwrapKey) || + // Single operation cases + (hasSign && !hasVerify && !hasEncrypt && !hasDecrypt && !hasWrapKey && !hasUnwrapKey && !hasDerive) || + (hasVerify && !hasSign && !hasEncrypt && !hasDecrypt && !hasWrapKey && !hasUnwrapKey && !hasDerive) || + (hasEncrypt && !hasDecrypt && !hasSign && !hasVerify && !hasWrapKey && !hasUnwrapKey && !hasDerive) || + (hasDecrypt && !hasEncrypt && !hasSign && !hasVerify && !hasWrapKey && !hasUnwrapKey && !hasDerive) || + (hasWrapKey && !hasUnwrapKey && !hasSign && !hasVerify && !hasEncrypt && !hasDecrypt && !hasDerive) || + (hasUnwrapKey && !hasWrapKey && !hasSign && !hasVerify && !hasEncrypt && !hasDecrypt && !hasDerive) + + if !validCombos { + return NewError(DataError, "invalid combination of key operations. Only sign/verify, encrypt/decrypt, or wrapKey/unwrapKey pairs are allowed, or single derive operations") + } + + // Verify that all requested usages are present in keyOps + for _, usage := range keyUsages { + if !slices.Contains(keyOps, usage) { + return NewError(DataError, fmt.Sprintf("requested usage '%s' is not present in key_ops", usage)) + } + } + + return nil +} + +func (jwk *ed25519JWK) validateEd25519JWK(keyUsages []CryptoKeyUsage) error { + private := jwk.D != "" + if private { + for _, usage := range keyUsages { + switch usage { + case SignCryptoKeyUsage: + continue + default: + return NewError(SyntaxError, fmt.Sprintf("invalid key usage: %s. Only 'sign' is valid for private Ed25519 keys", usage)) + } + } + } else { + for _, usage := range keyUsages { + switch usage { + case VerifyCryptoKeyUsage: + continue + default: + return NewError(SyntaxError, fmt.Sprintf("invalid key usage: %s. Only 'verify' is valid for public Ed25519 keys", usage)) + } + } + } + + if jwk.Kty != "OKP" { + return NewError(DataError, fmt.Sprintf("invalid 'kty': %s. kty value must be 'OKP' for Ed25519 keys", jwk.Kty)) + } + + if jwk.Crv != "Ed25519" { + return NewError(DataError, fmt.Sprintf("invalid 'crv': %s. crv value must be Ed25519", jwk.Crv)) + } + + if jwk.X == "" { + return NewError(DataError, "invalid 'x': x field is required for all Ed25519 keys") + } + + if private && jwk.D == "" { + return NewError(DataError, "invalid 'd': d field is required for private Ed25519 keys") + } + + if len(keyUsages) != 0 && jwk.Use != "" && jwk.Use != "sig" { + return NewError(DataError, fmt.Sprintf("invalid 'use': %s. use field must be 'sig' in the JWK if usages are supplied ", jwk.Use)) + } + + if err := validateKeyOps(jwk.KeyOps, keyUsages); err != nil { + return err + } + + // TODO: pass extractable down from JS params and validate properly + return nil +} diff --git a/internal/js/modules/k6/webcrypto/key.go b/internal/js/modules/k6/webcrypto/key.go index 0f56a4b3d13..14c410f7393 100644 --- a/internal/js/modules/k6/webcrypto/key.go +++ b/internal/js/modules/k6/webcrypto/key.go @@ -187,12 +187,14 @@ func newKeyGenerator(rt *sobek.Runtime, normalized Algorithm, params sobek.Value kg, err = newAESKeyGenParams(rt, normalized, params) case HMAC: kg, err = newHMACKeyGenParams(rt, normalized, params) - case ECDH, ECDSA: + case ECDH, ECDSA, X25519: kg, err = newECKeyGenParams(rt, normalized, params) case RSASsaPkcs1v15, RSAPss, RSAOaep: kg, err = newRsaHashedKeyGenParams(rt, normalized, params) + case Ed25519: + kg = newEd25519KeyGenParams(normalized) default: - validAlgorithms := []string{AESCbc, AESCtr, AESGcm, AESKw, HMAC, ECDH, ECDSA, RSASsaPkcs1v15, RSAPss, RSAOaep} + validAlgorithms := []string{AESCbc, AESCtr, AESGcm, AESKw, HMAC, ECDH, ECDSA, RSASsaPkcs1v15, RSAPss, RSAOaep, Ed25519} return nil, NewError( NotImplemented, "unsupported key generation algorithm '"+normalized.Name+"', "+ @@ -226,6 +228,8 @@ func newKeyImporter(rt *sobek.Runtime, normalized Algorithm, params sobek.Value) ki, err = newEcKeyImportParams(rt, normalized, params) case RSASsaPkcs1v15, RSAPss, RSAOaep: ki, err = newRsaHashedImportParams(rt, normalized, params) + case Ed25519: + ki = newEd25519ImportParams(normalized) default: return nil, errors.New("key import not implemented for algorithm " + normalized.Name) } diff --git a/internal/js/modules/k6/webcrypto/signer.go b/internal/js/modules/k6/webcrypto/signer.go index 98a8257f7b9..878b684688d 100644 --- a/internal/js/modules/k6/webcrypto/signer.go +++ b/internal/js/modules/k6/webcrypto/signer.go @@ -18,6 +18,8 @@ func newSignerVerifier(rt *sobek.Runtime, normalized Algorithm, params sobek.Val return &rsaSsaPkcs1v15SignerVerifier{}, nil case RSAPss: return newRSAPssParams(rt, normalized, params) + case Ed25519: + return &ed25519SignerVerifier{}, nil default: return nil, NewError(NotSupportedError, "unsupported algorithm for signing/verifying: "+normalized.Name) } diff --git a/internal/js/modules/k6/webcrypto/subtle_crypto.go b/internal/js/modules/k6/webcrypto/subtle_crypto.go index 9627656d649..d8183ff4921 100644 --- a/internal/js/modules/k6/webcrypto/subtle_crypto.go +++ b/internal/js/modules/k6/webcrypto/subtle_crypto.go @@ -766,6 +766,7 @@ func (sc *SubtleCrypto) DeriveBits( //nolint:funlen,gocognit // we have a lot of // `ALGORITHM` is the name of the algorithm. // - for PBKDF2: pass the string "PBKDF2" // - for HKDF: pass the string "HKDF" +// - for Ed25519: pass the string "Ed25519" func (sc *SubtleCrypto) ImportKey( //nolint:funlen // we have a lot of error handling format KeyFormat, keyData sobek.Value, @@ -911,6 +912,8 @@ func (sc *SubtleCrypto) ExportKey( //nolint:funlen // we have a lot of error han keyExporter = exportECKey case RSASsaPkcs1v15, RSAOaep, RSAPss: keyExporter = exportRSAKey + case Ed25519: + keyExporter = exportEd25519Key default: return NewError(NotSupportedError, "unsupported algorithm "+algorithm.Name) } diff --git a/internal/js/modules/k6/webcrypto/tests/wpt-patches/WebCryptoAPI__generateKey__failures.js.patch b/internal/js/modules/k6/webcrypto/tests/wpt-patches/WebCryptoAPI__generateKey__failures.js.patch index 201d5f94cf2..901b69fe0a8 100644 --- a/internal/js/modules/k6/webcrypto/tests/wpt-patches/WebCryptoAPI__generateKey__failures.js.patch +++ b/internal/js/modules/k6/webcrypto/tests/wpt-patches/WebCryptoAPI__generateKey__failures.js.patch @@ -1,18 +1,15 @@ diff --git a/WebCryptoAPI/generateKey/failures.js b/WebCryptoAPI/generateKey/failures.js -index e0f0279a6..61495ca75 100644 +index e0f0279a6..5b3b69764 100644 --- a/WebCryptoAPI/generateKey/failures.js +++ b/WebCryptoAPI/generateKey/failures.js -@@ -32,10 +32,10 @@ function run_test(algorithmNames) { - {name: "RSA-OAEP", resultType: "CryptoKeyPair", usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"], mandatoryUsages: ["decrypt", "unwrapKey"]}, +@@ -33,9 +33,9 @@ function run_test(algorithmNames) { {name: "ECDSA", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]}, {name: "ECDH", resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]}, -- {name: "Ed25519", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]}, + {name: "Ed25519", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]}, - {name: "Ed448", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]}, -- {name: "X25519", resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]}, -- {name: "X448", resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]}, -+ // {name: "Ed25519", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]}, + // {name: "Ed448", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]}, -+ // {name: "X25519", resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]}, + {name: "X25519", resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]}, +- {name: "X448", resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]}, + // {name: "X448", resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]}, ]; diff --git a/internal/js/modules/k6/webcrypto/tests/wpt-patches/WebCryptoAPI__generateKey__successes.js.patch b/internal/js/modules/k6/webcrypto/tests/wpt-patches/WebCryptoAPI__generateKey__successes.js.patch index 50d839c0aab..447b5b87b56 100644 --- a/internal/js/modules/k6/webcrypto/tests/wpt-patches/WebCryptoAPI__generateKey__successes.js.patch +++ b/internal/js/modules/k6/webcrypto/tests/wpt-patches/WebCryptoAPI__generateKey__successes.js.patch @@ -1,5 +1,5 @@ diff --git a/WebCryptoAPI/generateKey/successes.js b/WebCryptoAPI/generateKey/successes.js -index a9a168e1a..88861ab87 100644 +index a9a168e1a..e3976c378 100644 --- a/WebCryptoAPI/generateKey/successes.js +++ b/WebCryptoAPI/generateKey/successes.js @@ -21,17 +21,17 @@ function run_test(algorithmNames, slowTest) { @@ -16,11 +16,10 @@ index a9a168e1a..88861ab87 100644 {name: "ECDH", resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]}, - {name: "Ed25519", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]}, - {name: "Ed448", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]}, -- {name: "X25519", resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]}, -- {name: "X448", resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]}, -+ // {name: "Ed25519", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]}, ++ {name: "Ed25519", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign", "wrapKey"]}, + // {name: "Ed448", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]}, -+ // {name: "X25519", resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]}, + {name: "X25519", resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]}, +- {name: "X448", resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]}, + // {name: "X448", resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]}, ];