diff --git a/README.md b/README.md index 48f7768..ed0c02d 100644 --- a/README.md +++ b/README.md @@ -29,19 +29,19 @@ The current state of the project is that it is an experimental module of the Web | API | HMAC | ECDSA | RSASSA-PKCS1-v1_5 | RSA-PSS | | :----------------------- | :--- | :---- | :---------------- | :------ | -| `crypto.subtle.sign()` | ✅ | ❌ | ❌ | ❌ | -| `crypto.subtle.verify()` | ✅ | ❌ | ❌ | ❌ | +| `crypto.subtle.sign()` | ✅ | ✅ | ❌ | ❌ | +| `crypto.subtle.verify()` | ✅ | ✅ | ❌ | ❌ | ##### Key generation, import and export | API | AES-CBC | AES-GCM | AES-CTR | AES-KW | HMAC | ECDSA | ECDH | RSASSA-PKCS1-v1_5 | RSA-PSS | RSA-OAEP | | :---------------------------- | :------ | :------ | :------ | :----- | :--- | :---- | :--- | :---------------- | :------ | :------- | -| `crypto.subtle.generateKey()` | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | -| `crypto.subtle.importKey()` | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | -| `crypto.subtle.exportKey()` | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | +| `crypto.subtle.generateKey()` | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | +| `crypto.subtle.importKey()` | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | +| `crypto.subtle.exportKey()` | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | > [!WARNING] -> Currently, only the `raw` and `jwk` (JSON Web Key) formats are supported for import/export operations for the `AES-*` and `HMAC` algorithms. `ECDH` has support for `pkcs8` and `raw` formats. +> Currently, only the `raw` and `jwk` (JSON Web Key) formats are supported for import/export operations for the `AES-*` and `HMAC` algorithms. `ECDH` and `ECDSA` have support for `pkcs8`, `spki` and `raw` formats. ##### Key derivation diff --git a/examples/generateKey/generateKey-ecdsa.js b/examples/generateKey/generateKey-ecdsa.js new file mode 100644 index 0000000..b387bea --- /dev/null +++ b/examples/generateKey/generateKey-ecdsa.js @@ -0,0 +1,14 @@ +import { crypto } from "k6/x/webcrypto"; + +export default async function () { + const key = await crypto.subtle.generateKey( + { + name: "ECDSA", + namedCurve: "P-256" + }, + true, + ["sign", "verify"] + ); + + console.log(JSON.stringify(key)) +} \ No newline at end of file diff --git a/examples/import_export/export-ecdsa-keys.js b/examples/import_export/export-ecdsa-keys.js new file mode 100644 index 0000000..2449485 --- /dev/null +++ b/examples/import_export/export-ecdsa-keys.js @@ -0,0 +1,30 @@ +import { crypto } from "k6/x/webcrypto"; + +export default async function () { + const generatedKeyPair = await crypto.subtle.generateKey( + { + name: "ECDSA", + namedCurve: "P-256", + }, + true, + ["sign", "verify"] + ); + + const exportedPrivateKey = await crypto.subtle.exportKey( + "pkcs8", + generatedKeyPair.privateKey + ); + console.log("exported private key: " + printArrayBuffer(exportedPrivateKey)); + + const exportedPublicKey = await crypto.subtle.exportKey( + "raw", + generatedKeyPair.publicKey + ); + console.log("exported public key: " + printArrayBuffer(exportedPublicKey)); +} + +const printArrayBuffer = (buffer) => { + let view = new Uint8Array(buffer); + return Array.from(view); +}; + diff --git a/examples/import_export/import-ecdsa-keys.js b/examples/import_export/import-ecdsa-keys.js new file mode 100644 index 0000000..c2443f2 --- /dev/null +++ b/examples/import_export/import-ecdsa-keys.js @@ -0,0 +1,46 @@ +import { crypto } from "k6/x/webcrypto"; + +export default async function () { + const aliceKeyPair = await importKeys( + new Uint8Array([ + 4, 106, 149, 34, 76, 184, 103, 101, 35, 234, 57, 76, 231, 21, 188, 244, + 15, 179, 101, 113, 24, 6, 17, 21, 195, 60, 181, 73, 154, 170, 206, 21, + 244, 102, 50, 21, 235, 66, 107, 55, 97, 177, 160, 21, 167, 210, 15, 233, + 76, 31, 135, 131, 215, 123, 149, 171, 153, 231, 152, 197, 87, 176, 32, 39, + 137, + ]), + new Uint8Array([ + 48, 129, 135, 2, 1, 0, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, + 134, 72, 206, 61, 3, 1, 7, 4, 109, 48, 107, 2, 1, 1, 4, 32, 41, 167, 202, + 58, 174, 179, 236, 224, 240, 214, 91, 12, 207, 12, 10, 4, 200, 252, 81, + 163, 175, 76, 120, 60, 102, 201, 132, 40, 177, 74, 244, 226, 161, 68, 3, + 66, 0, 4, 106, 149, 34, 76, 184, 103, 101, 35, 234, 57, 76, 231, 21, 188, + 244, 15, 179, 101, 113, 24, 6, 17, 21, 195, 60, 181, 73, 154, 170, 206, + 21, 244, 102, 50, 21, 235, 66, 107, 55, 97, 177, 160, 21, 167, 210, 15, + 233, 76, 31, 135, 131, 215, 123, 149, 171, 153, 231, 152, 197, 87, 176, + 32, 39, 137, + ]) + ); + + console.log("alice: ", JSON.stringify(aliceKeyPair)); +} + +const importKeys = async (publicKeyData, privateKeyData) => { + const publicKey = await crypto.subtle.importKey( + "raw", + publicKeyData, + { name: "ECDSA", namedCurve: "P-256" }, + true, + ["verify"] + ); + + const privateKey = await crypto.subtle.importKey( + "pkcs8", + privateKeyData, + { name: "ECDSA", namedCurve: "P-256" }, + true, + ["sign"] + ); + + return { publicKey: publicKey, privateKey: privateKey }; +}; diff --git a/examples/sign_verify/sign-verify-ecdsa.js b/examples/sign_verify/sign-verify-ecdsa.js new file mode 100644 index 0000000..f29c108 --- /dev/null +++ b/examples/sign_verify/sign-verify-ecdsa.js @@ -0,0 +1,49 @@ +import { crypto } from "k6/x/webcrypto"; + +export default async function () { + try { + const keyPair = await crypto.subtle.generateKey( + { + name: "ECDSA", + namedCurve: "P-256", + }, + true, + ["sign", "verify"] + ); + + const data = string2ArrayBuffer("Hello World"); + + const alg = { name: "ECDSA", hash: {name: "SHA-256" } }; + + // 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, + keyPair.publicKey, + signature, + data + ); + + console.log("verified: ", verified); + } catch (err) { + console.log("err: " + JSON.stringify(err)); + } +} + +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/examples/sign_verify/verify-spki-ecdsa-invalid.js b/examples/sign_verify/verify-spki-ecdsa-invalid.js new file mode 100644 index 0000000..1b64431 --- /dev/null +++ b/examples/sign_verify/verify-spki-ecdsa-invalid.js @@ -0,0 +1,59 @@ +import { crypto } from "k6/x/webcrypto"; + +export default async function () { + const publicKey = await crypto.subtle.importKey( + "spki", + new Uint8Array([ + 48, 118, 48, 16, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 5, 43, 129, 4, 0, + 34, 3, 98, 0, 4, 29, 49, 157, 105, 45, 202, 95, 87, 84, 186, 123, 50, 193, + 22, 66, 198, 216, 210, 180, 251, 130, 73, 195, 242, 20, 215, 30, 144, 181, + 37, 41, 102, 217, 127, 123, 235, 31, 170, 177, 228, 243, 226, 96, 85, 73, + 194, 238, 219, 82, 3, 41, 179, 190, 166, 181, 229, 86, 36, 161, 81, 80, + 161, 105, 102, 99, 95, 25, 22, 239, 4, 221, 117, 142, 105, 64, 157, 6, 51, + 203, 75, 37, 153, 65, 121, 178, 42, 118, 156, 116, 52, 54, 145, 14, 121, + 153, 81, + ]), + { name: "ECDSA", namedCurve: "P-384" }, + true, + ["verify"] + ); + + let plaintText = new Uint8Array([ + 95, 77, 186, 79, 50, 12, 12, 232, 118, 114, 90, 252, 229, 251, 210, 91, 248, + 62, 90, 113, 37, 160, 140, 175, 231, 60, 62, 186, 196, 33, 119, 157, 249, + 213, 93, 24, 12, 58, 233, 148, 38, 69, 225, 216, 47, 238, 140, 157, 41, 75, + 60, 177, 160, 138, 153, 49, 32, 27, 60, 14, 129, 252, 71, 202, 207, 131, 21, + 162, 175, 102, 50, 65, 19, 195, 182, 98, 48, 195, 70, 8, 196, 244, 89, 54, + 52, 206, 2, 178, 103, 54, 34, 119, 240, 168, 64, 202, 116, 188, 61, 26, 98, + 54, 149, 44, 94, 215, 170, 248, 168, 254, 203, 221, 250, 117, 132, 230, 151, + 140, 234, 93, 42, 91, 159, 183, 241, 180, 140, 139, 11, 229, 138, 48, 82, 2, + 117, 77, 131, 118, 16, 115, 116, 121, 60, 240, 38, 170, 238, 83, 0, 114, + 125, 131, 108, 215, 30, 113, 179, 69, 221, 178, 228, 68, 70, 255, 197, 185, + 1, 99, 84, 19, 137, 13, 145, 14, 163, 128, 152, 74, 144, 25, 16, 49, 50, 63, + 22, 219, 204, 157, 107, 225, 104, 184, 72, 133, 56, 76, 160, 62, 18, 96, 10, + 193, 194, 72, 2, 138, 243, 114, 108, 201, 52, 99, 136, 46, 168, 192, 42, + 171, + ]); + + let signature1 = new Uint8Array([ + 13, 217, 194, 199, 240, 182, 244, 217, 50, 130, 84, 169, 2, 232, 115, 116, + 179, 192, 146, 25, 94, 107, 226, 26, 161, 166, 220, 216, 235, 166, 15, 123, + 11, 56, 196, 0, 109, 250, 33, 70, 212, 233, 253, 35, 220, 51, 97, 121, 151, + 64, 23, 73, 58, 31, 79, 116, 238, 207, 228, 85, 190, 61, 169, 237, 153, 100, + 29, 129, 97, 13, 254, 180, 104, 182, 7, 218, 148, 29, 87, 20, 231, 181, 26, + 238, 44, 69, 170, 14, 156, 77, 160, 33, 178, 55, 0, + ]); + + //Verifies the signature of the encoded data with the provided key + const verified = await crypto.subtle.verify( + { + name: "ECDSA", + hash: "SHA-384", + }, + publicKey, + signature1, + plaintText + ); + + console.log("verified: ", verified); +} diff --git a/examples/sign_verify/verify-spki-ecdsa-valid.js b/examples/sign_verify/verify-spki-ecdsa-valid.js new file mode 100644 index 0000000..d8c9a27 --- /dev/null +++ b/examples/sign_verify/verify-spki-ecdsa-valid.js @@ -0,0 +1,55 @@ +import { crypto } from "k6/x/webcrypto"; + +export default async function () { + const publicKey = await crypto.subtle.importKey( + "spki", + new Uint8Array([ + 48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, + 61, 3, 1, 7, 3, 66, 0, 4, 10, 5, 30, 56, 111, 103, 196, 166, 225, 229, + 203, 238, 125, 55, 116, 91, 88, 142, 190, 114, 15, 117, 89, 22, 40, 111, + 150, 41, 105, 122, 57, 23, 17, 216, 106, 234, 201, 103, 8, 210, 58, 38, + 35, 216, 198, 237, 187, 84, 217, 164, 63, 100, 6, 105, 49, 128, 15, 53, + 29, 158, 117, 235, 238, 30, + ]), + { name: "ECDSA", namedCurve: "P-256" }, + true, + ["verify"] + ); + + let plaintText = new Uint8Array([ + 95, 77, 186, 79, 50, 12, 12, 232, 118, 114, 90, 252, 229, 251, 210, 91, 248, + 62, 90, 113, 37, 160, 140, 175, 231, 60, 62, 186, 196, 33, 119, 157, 249, + 213, 93, 24, 12, 58, 233, 148, 38, 69, 225, 216, 47, 238, 140, 157, 41, 75, + 60, 177, 160, 138, 153, 49, 32, 27, 60, 14, 129, 252, 71, 202, 207, 131, 21, + 162, 175, 102, 50, 65, 19, 195, 182, 98, 48, 195, 70, 8, 196, 244, 89, 54, + 52, 206, 2, 178, 103, 54, 34, 119, 240, 168, 64, 202, 116, 188, 61, 26, 98, + 54, 149, 44, 94, 215, 170, 248, 168, 254, 203, 221, 250, 117, 132, 230, 151, + 140, 234, 93, 42, 91, 159, 183, 241, 180, 140, 139, 11, 229, 138, 48, 82, 2, + 117, 77, 131, 118, 16, 115, 116, 121, 60, 240, 38, 170, 238, 83, 0, 114, + 125, 131, 108, 215, 30, 113, 179, 69, 221, 178, 228, 68, 70, 255, 197, 185, + 1, 99, 84, 19, 137, 13, 145, 14, 163, 128, 152, 74, 144, 25, 16, 49, 50, 63, + 22, 219, 204, 157, 107, 225, 104, 184, 72, 133, 56, 76, 160, 62, 18, 96, 10, + 193, 194, 72, 2, 138, 243, 114, 108, 201, 52, 99, 136, 46, 168, 192, 42, + 171, + ]); + + let signature1 = new Uint8Array([ + 83, 223, 63, 226, 42, 29, 106, 105, 225, 145, 197, 180, 118, 154, 109, 110, + 66, 67, 47, 251, 53, 190, 203, 65, 207, 36, 19, 57, 49, 122, 124, 118, 59, + 74, 222, 134, 42, 235, 180, 229, 134, 24, 205, 81, 171, 156, 100, 218, 127, + 242, 126, 53, 27, 77, 249, 101, 157, 132, 244, 30, 67, 30, 64, 12, + ]); + + //Verifies the signature of the encoded data with the provided key + const verified = await crypto.subtle.verify( + { + name: "ECDSA", + hash: "SHA-256", + }, + publicKey, + signature1, + plaintText + ); + + console.log("verified: ", verified); +} diff --git a/webcrypto/algorithm.go b/webcrypto/algorithm.go index f34e313..01c4f92 100644 --- a/webcrypto/algorithm.go +++ b/webcrypto/algorithm.go @@ -177,7 +177,7 @@ func isRegisteredAlgorithm(algorithmName string, forOperation string) bool { case OperationIdentifierEncrypt, OperationIdentifierDecrypt: return isAesAlgorithm(algorithmName) case OperationIdentifierSign, OperationIdentifierVerify: - return algorithmName == HMAC + return algorithmName == HMAC || algorithmName == ECDSA default: return false } @@ -198,6 +198,5 @@ type hasAlg interface { } func isEllipticCurve(algorithmName string) bool { - // TODO: algorithmName == ECDSA - return algorithmName == ECDH + return algorithmName == ECDH || algorithmName == ECDSA } diff --git a/webcrypto/bits.go b/webcrypto/bits.go index 230626e..4d08ebb 100644 --- a/webcrypto/bits.go +++ b/webcrypto/bits.go @@ -3,9 +3,9 @@ package webcrypto type bitsDeriver func(CryptoKey, CryptoKey) ([]byte, error) func newBitsDeriver(algName string) (bitsDeriver, error) { - if algName == "ECDH" { + if algName == ECDH { return deriveBitsECDH, nil } - return nil, NewError(NotSupportedError, "unsupported algorithm: "+algName) + return nil, NewError(NotSupportedError, "unsupported algorithm for derive bits: "+algName) } diff --git a/webcrypto/elliptic_curve.go b/webcrypto/elliptic_curve.go index ddd9779..1e98866 100644 --- a/webcrypto/elliptic_curve.go +++ b/webcrypto/elliptic_curve.go @@ -3,9 +3,11 @@ package webcrypto import ( "crypto/ecdh" "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" "crypto/x509" "errors" + "math/big" "github.com/dop251/goja" ) @@ -55,43 +57,34 @@ func (e *EcKeyImportParams) ImportKey( var keyType CryptoKeyType var handle any - if format == RawKeyFormat { - // raw key type is always public - keyType = PublicCryptoKeyType - - // pick the elliptic curve - c, err := pickEllipticCurve(e.NamedCurve) - if err != nil { - return nil, NewError(NotSupportedError, "invalid elliptic curve "+string(e.NamedCurve)) - } - - handle, err = c.NewPublicKey(keyData) - if err != nil { - return nil, NewError(DataError, "unable to import key data: "+err.Error()) - } - } + var importFn func(curve EllipticCurveKind, keyData []byte) (any, error) - if format == Pkcs8KeyFormat { - // pkcs8 key type is always private + switch { + case e.Algorithm.Name == ECDH && format == Pkcs8KeyFormat: + importFn = importECDHPrivateKey keyType = PrivateCryptoKeyType + case e.Algorithm.Name == ECDH && format == RawKeyFormat: + importFn = importECDHPublicKey + keyType = PublicCryptoKeyType + case e.Algorithm.Name == ECDSA && format == Pkcs8KeyFormat: + importFn = importECDSAPrivateKey + keyType = PrivateCryptoKeyType + case e.Algorithm.Name == ECDSA && format == RawKeyFormat: + importFn = importECDSAPublicKey + keyType = PublicCryptoKeyType + case e.Algorithm.Name == ECDH && format == SpkiKeyFormat: + importFn = importECDHSPKIPublicKey + keyType = PublicCryptoKeyType + case e.Algorithm.Name == ECDSA && format == SpkiKeyFormat: + importFn = importECDSASPKIPublicKey + keyType = PublicCryptoKeyType + default: + return nil, NewError(NotSupportedError, unsupportedKeyFormatErrorMsg+" "+format+" for algorithm "+e.Algorithm.Name) + } - var err error - parsedKey, err := x509.ParsePKCS8PrivateKey(keyData) - if err != nil { - return nil, NewError(DataError, "unable to import key data: "+err.Error()) - } - - // check if the key is an ECDSA key - ecdsaKey, ok := parsedKey.(*ecdsa.PrivateKey) - if !ok { - return nil, NewError(DataError, "a private key is not an ECDSA key") - } - - // try to restore the ECDH key - handle, err = ecdsaKey.ECDH() - if err != nil { - return nil, NewError(DataError, "unable to import key data: "+err.Error()) - } + handle, err := importFn(e.NamedCurve, keyData) + if err != nil { + return nil, err } return &CryptoKey{ @@ -106,6 +99,50 @@ func (e *EcKeyImportParams) ImportKey( }, nil } +func importECDHPublicKey(curve EllipticCurveKind, keyData []byte) (any, error) { + c, err := pickECDHCurve(curve) + if err != nil { + return nil, NewError(NotSupportedError, "invalid elliptic curve "+string(curve)) + } + + handle, err := c.NewPublicKey(keyData) + if err != nil { + return nil, NewError(DataError, "unable to import ECDH public key data: "+err.Error()) + } + + return handle, nil +} + +func importECDHSPKIPublicKey(_ EllipticCurveKind, keyData []byte) (any, error) { + pk, err := x509.ParsePKIXPublicKey(keyData) + if err != nil { + return nil, NewError(DataError, "unable to import ECDH public key data: "+err.Error()) + } + + ecdsaKey, ok := pk.(*ecdsa.PublicKey) + if !ok { + return nil, NewError(DataError, "a public key is not an ECDSA key") + } + + // try to restore the ECDH key + return ecdsaKey.ECDH() +} + +func importECDSASPKIPublicKey(_ EllipticCurveKind, keyData []byte) (any, error) { + pk, err := x509.ParsePKIXPublicKey(keyData) + if err != nil { + return nil, NewError(DataError, "unable to import ECDH public key data: "+err.Error()) + } + + ecdsaKey, ok := pk.(*ecdsa.PublicKey) + if !ok { + return nil, NewError(DataError, "a public key is not an ECDSA key") + } + + // try to restore the ECDH key + return ecdsaKey, nil +} + // EllipticCurveKind represents the kind of elliptic curve that is being used. type EllipticCurveKind string @@ -135,6 +172,59 @@ func IsEllipticCurve(name string) bool { } } +func importECDHPrivateKey(_ EllipticCurveKind, keyData []byte) (any, error) { + parsedKey, err := x509.ParsePKCS8PrivateKey(keyData) + if err != nil { + return nil, NewError(DataError, "unable to import ECDH private key data: "+err.Error()) + } + + // check if the key is an ECDSA key + ecdsaKey, ok := parsedKey.(*ecdsa.PrivateKey) + if !ok { + return nil, NewError(DataError, "a private key is not an ECDSA key") + } + + // try to restore the ECDH key + handle, err := ecdsaKey.ECDH() + if err != nil { + return nil, NewError(DataError, "unable to import key data: "+err.Error()) + } + + return handle, nil +} + +func importECDSAPrivateKey(_ EllipticCurveKind, keyData []byte) (any, error) { + parsedKey, err := x509.ParsePKCS8PrivateKey(keyData) + if err != nil { + return nil, NewError(DataError, "unable to import ECDSA private key data: "+err.Error()) + } + + ecdsaKey, ok := parsedKey.(*ecdsa.PrivateKey) + if !ok { + return nil, NewError(DataError, "a private key is not an ECDSA key") + } + + return ecdsaKey, nil +} + +func importECDSAPublicKey(curve EllipticCurveKind, keyData []byte) (any, error) { + c, err := pickEllipticCurve(curve) + if err != nil { + return nil, NewError(NotSupportedError, "invalid elliptic curve "+string(curve)) + } + + x, y := elliptic.Unmarshal(c, keyData) + if x == nil { + return nil, NewError(DataError, "unable to import ECDSA public key data") + } + + return &ecdsa.PublicKey{ + Curve: c, + X: x, + Y: y, + }, nil +} + // ECKeyGenParams represents the object that should be passed as the algorithm // parameter into `SubtleCrypto.GenerateKey`, when generating any // elliptic-curve-based key pair: that is, when the algorithm is identified @@ -161,7 +251,7 @@ func newECKeyGenParams(rt *goja.Runtime, normalized Algorithm, params goja.Value }, nil } -// GenerateKey generates a new ECDH key pair, according to the algorithm +// GenerateKey generates a new ECDH/ECDSA key pair, according to the algorithm // described in the specification. // // [specification]: https://www.w3.org/TR/WebCryptoAPI/#dfn-EcKeyGenParams @@ -169,8 +259,23 @@ func (ecgp *ECKeyGenParams) GenerateKey( extractable bool, keyUsages []CryptoKeyUsage, ) (CryptoKeyGenerationResult, error) { - c, err := pickEllipticCurve(ecgp.NamedCurve) - if err != nil { + var keyPairGenerator func(curve EllipticCurveKind, keyUsages []CryptoKeyUsage) (any, any, error) + var privateKeyUsages, publicKeyUsages []CryptoKeyUsage + + switch ecgp.Algorithm.Name { + case ECDH: + keyPairGenerator = generateECDHKeyPair + privateKeyUsages = []CryptoKeyUsage{DeriveKeyCryptoKeyUsage, DeriveBitsCryptoKeyUsage} + publicKeyUsages = []CryptoKeyUsage{} + case ECDSA: + keyPairGenerator = generateECDSAKeyPair + privateKeyUsages = []CryptoKeyUsage{SignCryptoKeyUsage} + publicKeyUsages = []CryptoKeyUsage{VerifyCryptoKeyUsage} + default: + return nil, NewError(NotSupportedError, "unsupported elliptic algorithm: "+ecgp.Algorithm.Name) + } + + if !isValidEllipticCurve(ecgp.NamedCurve) { return nil, NewError(NotSupportedError, "invalid elliptic curve "+string(ecgp.NamedCurve)) } @@ -178,22 +283,6 @@ func (ecgp *ECKeyGenParams) GenerateKey( return nil, NewError(SyntaxError, "key usages cannot be empty") } - for _, usage := range keyUsages { - switch usage { - case DeriveKeyCryptoKeyUsage, DeriveBitsCryptoKeyUsage: - continue - default: - return nil, NewError(SyntaxError, "invalid key usage") - } - } - - // generate a private & public key - rawPrivateKey, err := c.GenerateKey(rand.Reader) - if err != nil { - return nil, NewError(OperationError, "unable to generate a key pair") - } - rawPublicKey := rawPrivateKey.PublicKey() - alg := &EcKeyAlgorithm{ KeyAlgorithm: KeyAlgorithm{ Algorithm: ecgp.Algorithm, @@ -208,20 +297,21 @@ func (ecgp *ECKeyGenParams) GenerateKey( Algorithm: alg, Usages: UsageIntersection( keyUsages, - []CryptoKeyUsage{ - DeriveKeyCryptoKeyUsage, - DeriveBitsCryptoKeyUsage, - }, + privateKeyUsages, ), - handle: rawPrivateKey, } publicKey := &CryptoKey{ Type: PublicCryptoKeyType, Extractable: true, Algorithm: alg, - Usages: []CryptoKeyUsage{}, - handle: rawPublicKey, + Usages: publicKeyUsages, + } + + var err error + privateKey.handle, publicKey.handle, err = keyPairGenerator(ecgp.NamedCurve, keyUsages) + if err != nil { + return nil, err } return &CryptoKeyPair{ @@ -230,10 +320,62 @@ func (ecgp *ECKeyGenParams) GenerateKey( }, nil } -// pickEllipticCurve returns the elliptic curve that corresponds to the given +func generateECDHKeyPair(curve EllipticCurveKind, keyUsages []CryptoKeyUsage) (any, any, error) { + for _, usage := range keyUsages { + switch usage { + case DeriveKeyCryptoKeyUsage, DeriveBitsCryptoKeyUsage: + continue + default: + return nil, nil, NewError(SyntaxError, "invalid key usage") + } + } + + c, err := pickECDHCurve(curve) + if err != nil { + return nil, nil, NewError(NotSupportedError, err.Error()) + } + + // generate a private & public key + rawPrivateKey, err := c.GenerateKey(rand.Reader) + if err != nil { + return nil, nil, NewError(OperationError, "unable to generate a ECDH key pair") + } + + return rawPrivateKey, rawPrivateKey.PublicKey(), nil +} + +func generateECDSAKeyPair(curve EllipticCurveKind, keyUsages []CryptoKeyUsage) (any, any, error) { + for _, usage := range keyUsages { + switch usage { + case SignCryptoKeyUsage, VerifyCryptoKeyUsage: + continue + default: + return nil, nil, NewError(SyntaxError, "invalid key usage") + } + } + + c, err := pickEllipticCurve(curve) + if err != nil { + return nil, nil, NewError(NotSupportedError, err.Error()) + } + + rawPrivateKey, err := ecdsa.GenerateKey(c, rand.Reader) + if err != nil { + return nil, nil, NewError(OperationError, "unable to generate a ECDSA key pair") + } + + return rawPrivateKey, &rawPrivateKey.PublicKey, nil +} + +// isValidEllipticCurve returns true if the given elliptic curve is supported, +func isValidEllipticCurve(curve EllipticCurveKind) bool { + return curve == EllipticCurveKindP256 || curve == EllipticCurveKindP384 || curve == EllipticCurveKindP521 +} + +// pickECDHCurve returns the elliptic curve that corresponds to the given // EllipticCurveKind. // If the curve is not supported, an error is returned. -func pickEllipticCurve(k EllipticCurveKind) (ecdh.Curve, error) { +func pickECDHCurve(k EllipticCurveKind) (ecdh.Curve, error) { switch k { case EllipticCurveKindP256: return ecdh.P256(), nil @@ -241,12 +383,25 @@ func pickEllipticCurve(k EllipticCurveKind) (ecdh.Curve, error) { return ecdh.P384(), nil case EllipticCurveKindP521: return ecdh.P521(), nil + default: + return nil, errors.New("invalid ECDH curve") + } +} + +func pickEllipticCurve(k EllipticCurveKind) (elliptic.Curve, error) { + switch k { + case EllipticCurveKindP256: + return elliptic.P256(), nil + case EllipticCurveKindP384: + return elliptic.P384(), nil + case EllipticCurveKindP521: + return elliptic.P521(), nil default: return nil, errors.New("invalid elliptic curve") } } -func exportECKey(ck *CryptoKey, format KeyFormat) ([]byte, error) { +func exportECKey(alg string, ck *CryptoKey, format KeyFormat) ([]byte, error) { if ck.handle == nil { return nil, NewError(OperationError, "key data is not accessible") } @@ -257,33 +412,61 @@ func exportECKey(ck *CryptoKey, format KeyFormat) ([]byte, error) { return nil, NewError(InvalidAccessError, "key is not a valid elliptic curve public key") } - k, ok := ck.handle.(*ecdh.PublicKey) - if !ok { - return nil, NewError(OperationError, "key data isn't a valid elliptic curve public key") + bytes, err := extractPublicKeyBytes(alg, ck.handle) + if err != nil { + return nil, NewError(OperationError, "unable to extract public key data: "+err.Error()) } - return k.Bytes(), nil + return bytes, nil + case SpkiKeyFormat: + if ck.Type != PublicCryptoKeyType { + return nil, NewError(InvalidAccessError, "key is not a valid elliptic curve public key") + } + + bytes, err := x509.MarshalPKIXPublicKey(ck.handle) + if err != nil { + return nil, NewError(OperationError, "unable to marshal key to SPKI format: "+err.Error()) + } + + return bytes, nil case Pkcs8KeyFormat: if ck.Type != PrivateCryptoKeyType { return nil, NewError(InvalidAccessError, "key is not a valid elliptic curve private key") } - k, ok := ck.handle.(*ecdh.PrivateKey) - if !ok { - return nil, NewError(OperationError, "key data isn't a valid elliptic curve private key") - } - - bytes, err := x509.MarshalPKCS8PrivateKey(k) + bytes, err := x509.MarshalPKCS8PrivateKey(ck.handle) if err != nil { return nil, NewError(OperationError, "unable to marshal key to PKCS8 format: "+err.Error()) } return bytes, nil default: - return nil, NewError(NotSupportedError, "unsupported key format "+format) + return nil, NewError(NotSupportedError, unsupportedKeyFormatErrorMsg+" "+format) } } +func extractPublicKeyBytes(alg string, handle any) ([]byte, error) { + if alg == ECDH { + k, ok := handle.(*ecdh.PublicKey) + if !ok { + return nil, NewError(OperationError, "key data isn't a valid elliptic curve public key") + } + + return k.Bytes(), nil + } + + if alg == ECDSA { + k, ok := handle.(*ecdsa.PublicKey) + if !ok { + return nil, NewError(OperationError, "key data isn't a valid elliptic curve public key") + } + + return elliptic.Marshal(k.Curve, k.X, k.Y), nil + } + + return nil, errors.New("unsupported algorithm " + alg) +} + func deriveBitsECDH(privateKey CryptoKey, publicKey CryptoKey) ([]byte, error) { pk, ok := privateKey.handle.(*ecdh.PrivateKey) if !ok { @@ -296,3 +479,112 @@ func deriveBitsECDH(privateKey CryptoKey, publicKey CryptoKey) ([]byte, error) { return pk.ECDH(pc) } + +// The ECDSAParams represents the object that should be passed as the algorithm +// parameter into `SubtleCrypto.Sign` or `SubtleCrypto.Verify“ when using the +// ECDSA algorithm. +type ECDSAParams struct { + // Name should be set to AlgorithmKindEcdsa. + Name AlgorithmIdentifier + + // Hash identifies the name of the digest algorithm to use. + // You can use any of the following: + // * [Sha256] + // * [Sha384] + // * [Sha512] + Hash Algorithm +} + +func newECDSAParams(rt *goja.Runtime, normalized Algorithm, params goja.Value) (*ECDSAParams, error) { + hashValue, err := traverseObject(rt, params, "hash") + if err != nil { + return nil, NewError(SyntaxError, "could not get hash from algorithm parameter") + } + + normalizedHash, err := normalizeAlgorithm(rt, hashValue, OperationIdentifierGenerateKey) + if err != nil { + return nil, err + } + + return &ECDSAParams{ + Name: normalized.Name, + Hash: normalizedHash, + }, nil +} + +// Sign . +func (edsa *ECDSAParams) Sign(key CryptoKey, data []byte) ([]byte, error) { + if key.Type != PrivateCryptoKeyType { + return nil, NewError(InvalidAccessError, "key is not a valid ECDSA private key") + } + + k, ok := key.handle.(*ecdsa.PrivateKey) + if !ok { + return nil, NewError(InvalidAccessError, "key is not a valid ECDSA private key") + } + + // TODO: explicitly check if the hash algorithm is defined + hashFn, ok := getHashFn(edsa.Hash.Name) + if !ok { + return nil, NewError(NotSupportedError, "unsupported hash algorithm: "+edsa.Hash.Name) + } + + hasher := hashFn() + hasher.Write(data) + + r, s, err := ecdsa.Sign(rand.Reader, k, hasher.Sum(nil)) + if err != nil { + return nil, NewError(OperationError, "unable to sign data: "+err.Error()) + } + + bitSize := k.Curve.Params().BitSize + n := (bitSize + 7) / 8 + + rBytes := ensureLength(r.Bytes(), n) + sBytes := ensureLength(s.Bytes(), n) + + return append(rBytes, sBytes...), nil +} + +// Helper function to ensure the byte slice has length n +// prepending it with zeros if necessary. +func ensureLength(b []byte, n int) []byte { + if len(b) == n { + return b + } + result := make([]byte, n) + copy(result[n-len(b):], b) + return result +} + +// Verify . +func (edsa *ECDSAParams) Verify(key CryptoKey, signature []byte, data []byte) (bool, error) { + if key.Type != PublicCryptoKeyType { + return false, NewError(InvalidAccessError, "key is not a valid ECDSA public key") + } + + k, ok := key.handle.(*ecdsa.PublicKey) + if !ok { + return false, NewError(InvalidAccessError, "key is not a valid ECDSA public key") + } + + hashFn, ok := getHashFn(edsa.Hash.Name) + if !ok { + return false, NewError(NotSupportedError, "unsupported hash algorithm: "+edsa.Hash.Name) + } + + bitSize := k.Curve.Params().BitSize + n := (bitSize + 7) / 8 + + if len(signature) != 2*n { + return false, nil + } + + hasher := hashFn() + hasher.Write(data) + + r := new(big.Int).SetBytes(signature[:n]) + s := new(big.Int).SetBytes(signature[n:]) + + return ecdsa.Verify(k, hasher.Sum(nil), r, s), nil +} diff --git a/webcrypto/hmac.go b/webcrypto/hmac.go index 78f9f25..bc93809 100644 --- a/webcrypto/hmac.go +++ b/webcrypto/hmac.go @@ -1,6 +1,7 @@ package webcrypto import ( + "crypto/hmac" "crypto/rand" "crypto/sha1" //nolint:gosec "crypto/sha256" @@ -350,3 +351,51 @@ func (hip *HMACImportParams) ImportKey( // Ensure that HMACImportParams implements the KeyImporter interface. var _ KeyImporter = &HMACImportParams{} + +type hmacSignerVerifier struct{} + +// Sign . +func (hmacSignerVerifier) Sign(key CryptoKey, data []byte) ([]byte, error) { + keyAlgorithm, ok := key.Algorithm.(hasHash) + if !ok { + return nil, NewError(InvalidAccessError, "key algorithm does not describe a HMAC key") + } + + keyHandle, ok := key.handle.([]byte) + if !ok { + return nil, NewError(InvalidAccessError, "key handle is of incorrect type") + } + + hashFn, ok := getHashFn(keyAlgorithm.hash()) + if !ok { + return nil, NewError(NotSupportedError, "unsupported hash algorithm "+keyAlgorithm.hash()) + } + + hasher := hmac.New(hashFn, keyHandle) + hasher.Write(data) + + return hasher.Sum(nil), nil +} + +// Verify . +func (hmacSignerVerifier) Verify(key CryptoKey, signature, data []byte) (bool, error) { + keyAlgorithm, ok := key.Algorithm.(hasHash) + if !ok { + return false, NewError(InvalidAccessError, "key algorithm does not describe a HMAC key") + } + + keyHandle, ok := key.handle.([]byte) + if !ok { + return false, NewError(InvalidAccessError, "key handle is of incorrect type") + } + + hashFn, ok := getHashFn(keyAlgorithm.hash()) + if !ok { + return false, NewError(InvalidAccessError, "key handle is of incorrect type") + } + + hasher := hmac.New(hashFn, keyHandle) + hasher.Write(data) + + return hmac.Equal(signature, hasher.Sum(nil)), nil +} diff --git a/webcrypto/key.go b/webcrypto/key.go index f0118e9..2b19c43 100644 --- a/webcrypto/key.go +++ b/webcrypto/key.go @@ -174,8 +174,10 @@ func newKeyGenerator(rt *goja.Runtime, normalized Algorithm, params goja.Value) kg, err = newAESKeyGenParams(rt, normalized, params) case HMAC: kg, err = newHMACKeyGenParams(rt, normalized, params) - case ECDH: + case ECDH, ECDSA: kg, err = newECKeyGenParams(rt, normalized, params) + default: + return nil, errors.New("key generation not implemented for algorithm " + normalized.Name) } if err != nil { @@ -200,8 +202,10 @@ func newKeyImporter(rt *goja.Runtime, normalized Algorithm, params goja.Value) ( ki = newAESImportParams(normalized) case HMAC: ki, err = newHMACImportParams(rt, normalized, params) - case ECDH: + case ECDH, ECDSA: ki, err = newEcKeyImportParams(rt, normalized, params) + default: + return nil, errors.New("key import not implemented for algorithm " + normalized.Name) } if err != nil { diff --git a/webcrypto/params.go b/webcrypto/params.go index 60a7dd5..8ae9bf8 100644 --- a/webcrypto/params.go +++ b/webcrypto/params.go @@ -18,21 +18,6 @@ type AESKwParams struct { Name AlgorithmIdentifier } -// The ECDSAParams represents the object that should be passed as the algorithm -// parameter into `SubtleCrypto.Sign` or `SubtleCrypto.Verify“ when using the -// ECDSA algorithm. -type ECDSAParams struct { - // Name should be set to AlgorithmKindEcdsa. - Name AlgorithmIdentifier - - // Hash identifies the name of the digest algorithm to use. - // You can use any of the following: - // * [Sha256] - // * [Sha384] - // * [Sha512] - Hash AlgorithmIdentifier -} - // HKDFParams represents the object that should be passed as the algorithm parameter // into `SubtleCrypto.DeriveKey`, when using the HKDF algorithm. type HKDFParams struct { diff --git a/webcrypto/signer.go b/webcrypto/signer.go new file mode 100644 index 0000000..095dd29 --- /dev/null +++ b/webcrypto/signer.go @@ -0,0 +1,20 @@ +package webcrypto + +import "github.com/dop251/goja" + +// SignerVerifier . +type SignerVerifier interface { + Sign(key CryptoKey, dataToSign []byte) ([]byte, error) + Verify(key CryptoKey, signature, dataToVerify []byte) (bool, error) +} + +func newSignerVerifier(rt *goja.Runtime, normalized Algorithm, params goja.Value) (SignerVerifier, error) { + switch normalized.Name { + case HMAC: + return &hmacSignerVerifier{}, nil + case ECDSA: + return newECDSAParams(rt, normalized, params) + default: + return nil, NewError(NotSupportedError, "unsupported algorithm for signing/verifying: "+normalized.Name) + } +} diff --git a/webcrypto/subtle_crypto.go b/webcrypto/subtle_crypto.go index 190e6f7..f6c433c 100644 --- a/webcrypto/subtle_crypto.go +++ b/webcrypto/subtle_crypto.go @@ -1,7 +1,6 @@ package webcrypto import ( - "crypto/hmac" "encoding/json" "errors" "fmt" @@ -58,7 +57,7 @@ func (sc *SubtleCrypto) Encrypt(algorithm, key, data goja.Value) *goja.Promise { var ck CryptoKey if err = rt.ExportTo(key, &ck); err != nil { - reject(NewError(TypeError, "key argument does hold not a valid CryptoKey object")) + reject(NewError(InvalidAccessError, "key argument does hold not a valid CryptoKey object")) return promise } @@ -238,6 +237,12 @@ func (sc *SubtleCrypto) Sign(algorithm, key, data goja.Value) *goja.Promise { return promise } + signer, err := newSignerVerifier(rt, normalized, algorithm) + if err != nil { + reject(err) + return promise + } + var ck CryptoKey if err = rt.ExportTo(key, &ck); err != nil { reject(NewError(InvalidAccessError, "key argument does hold not a valid CryptoKey object")) @@ -263,37 +268,13 @@ func (sc *SubtleCrypto) Sign(algorithm, key, data goja.Value) *goja.Promise { return } - // 10. - switch normalized.Name { - case HMAC: - keyAlgorithm, ok := ck.Algorithm.(hasHash) - if !ok { - reject(NewError(InvalidAccessError, "key algorithm does not describe a HMAC key")) - return - } - - keyHandle, ok := ck.handle.([]byte) - if !ok { - reject(NewError(InvalidAccessError, "key handle is of incorrect type")) - return - } - - hashFn, ok := getHashFn(keyAlgorithm.hash()) - if !ok { - reject(NewError(NotSupportedError, "unsupported hash algorithm "+keyAlgorithm.hash())) - return - } - - hasher := hmac.New(hashFn, keyHandle) - hasher.Write(dataToSign) - - // 10. - mac := hasher.Sum(nil) - - resolve(rt.NewArrayBuffer(mac)) - default: - reject(NewError(NotSupportedError, fmt.Sprintf("unsupported algorithm %q", normalized.Name))) + signature, err := signer.Sign(ck, dataToSign) + if err != nil { + reject(err) + return } + + resolve(rt.NewArrayBuffer(signature)) }() return promise @@ -347,6 +328,12 @@ func (sc *SubtleCrypto) Verify(algorithm, key, signature, data goja.Value) *goja return promise } + verifier, err := newSignerVerifier(rt, normalizedAlgorithm, algorithm) + if err != nil { + reject(err) + return promise + } + var ck CryptoKey if err = rt.ExportTo(key, &ck); err != nil { reject(NewError(InvalidAccessError, "key argument does hold not a valid CryptoKey object")) @@ -372,33 +359,13 @@ func (sc *SubtleCrypto) Verify(algorithm, key, signature, data goja.Value) *goja return } - switch normalizedAlgorithm.Name { - case HMAC: - keyAlgorithm, ok := ck.Algorithm.(hasHash) - if !ok { - reject(NewError(InvalidAccessError, "key algorithm does not describe a HMAC key")) - return - } - - keyHandle, ok := ck.handle.([]byte) - if !ok { - reject(NewError(InvalidAccessError, "key handle is of incorrect type")) - return - } - - hashFn, ok := getHashFn(keyAlgorithm.hash()) - if !ok { - reject(NewError(NotSupportedError, "unsupported hash algorithm "+keyAlgorithm.hash())) - return - } - - hasher := hmac.New(hashFn, keyHandle) - hasher.Write(signedData) - - resolve(hmac.Equal(signatureData, hasher.Sum(nil))) - default: - reject(NewError(NotSupportedError, fmt.Sprintf("unsupported algorithm %q", normalizedAlgorithm.Name))) + verified, err := verifier.Verify(ck, signatureData, signedData) + if err != nil { + reject(err) + return } + + resolve(verified) }() return promise @@ -708,7 +675,7 @@ func (sc *SubtleCrypto) ImportKey( // 2. switch format { - case Pkcs8KeyFormat, RawKeyFormat: + case Pkcs8KeyFormat, RawKeyFormat, SpkiKeyFormat: ab, err := exportArrayBuffer(rt, keyData) if err != nil { reject(err) @@ -841,8 +808,8 @@ func (sc *SubtleCrypto) ExportKey(format KeyFormat, key goja.Value) *goja.Promis reject(err) return } - case ECDH: - result, err = exportECKey(ck, format) + case ECDH, ECDSA: + result, err = exportECKey(keyAlgorithmName, ck, format) if err != nil { reject(err) return @@ -870,7 +837,7 @@ func (sc *SubtleCrypto) ExportKey(format KeyFormat, key goja.Value) *goja.Promis } func isBinaryExportedFormat(format KeyFormat) bool { - return format == RawKeyFormat || format == Pkcs8KeyFormat + return format == RawKeyFormat || format == Pkcs8KeyFormat || format == SpkiKeyFormat } // WrapKey "wraps" a key. diff --git a/webcrypto/subtle_crypto_test.go b/webcrypto/subtle_crypto_test.go index 0c0df71..4425c6c 100644 --- a/webcrypto/subtle_crypto_test.go +++ b/webcrypto/subtle_crypto_test.go @@ -177,6 +177,23 @@ func TestSubtleCryptoSignVerify(t *testing.T) { assert.NoError(t, gotErr) }) + + t.Run("ECDSA", func(t *testing.T) { + t.Parallel() + + ts := newConfiguredRuntime(t) + + gotErr := ts.EventLoop.Start(func() error { + err := executeTestScripts(ts.VU.Runtime(), "./tests/sign_verify", "ecdsa_vectors.js", "ecdsa.js") + require.NoError(t, err) + + _, err = ts.VU.Runtime().RunString(`run_test()`) + + return err + }) + + assert.NoError(t, gotErr) + }) } func executeTestScripts(rt *goja.Runtime, base string, scripts ...string) error { diff --git a/webcrypto/tests/generateKey/failures.js b/webcrypto/tests/generateKey/failures.js index b69a753..d0f3ef4 100644 --- a/webcrypto/tests/generateKey/failures.js +++ b/webcrypto/tests/generateKey/failures.js @@ -43,7 +43,7 @@ function run_test(algorithmNames) { // {name: "RSASSA-PKCS1-v1_5", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]}, // {name: "RSA-PSS", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]}, // {name: "RSA-OAEP", resultType: "CryptoKeyPair", usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"], mandatoryUsages: ["decrypt", "unwrapKey"]}, - // {name: "ECDSA", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]}, + {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: "Ed448", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]}, diff --git a/webcrypto/tests/generateKey/successes.js b/webcrypto/tests/generateKey/successes.js index 47a66fe..fa57167 100644 --- a/webcrypto/tests/generateKey/successes.js +++ b/webcrypto/tests/generateKey/successes.js @@ -34,7 +34,8 @@ function run_test(algorithmNames, slowTest) { // {name: "RSASSA-PKCS1-v1_5", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]}, // {name: "RSA-PSS", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]}, // {name: "RSA-OAEP", resultType: "CryptoKeyPair", usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"], mandatoryUsages: ["decrypt", "unwrapKey"]}, - // {name: "ECDSA", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]}, + // seems that ECDSA test case below is invalid, since private & public keys have different usages (private key has "sign" usage, public key has "verify" usage) + // {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: "Ed448", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]}, diff --git a/webcrypto/tests/import_export/ec_importKey.js b/webcrypto/tests/import_export/ec_importKey.js index 1e838ab..50a631e 100644 --- a/webcrypto/tests/import_export/ec_importKey.js +++ b/webcrypto/tests/import_export/ec_importKey.js @@ -65,7 +65,7 @@ var keyData = { // combinations to test var testVectors = [ - // {name: "ECDSA", privateUsages: ["sign"], publicUsages: ["verify"]}, + {name: "ECDSA", privateUsages: ["sign"], publicUsages: ["verify"]}, {name: "ECDH", privateUsages: ["deriveKey", "deriveBits"], publicUsages: []} ]; @@ -80,7 +80,7 @@ testVectors.forEach(function(vector) { [[]].forEach(function(usages) { // Only valid usages argument is empty array // TODO: return back formats after implementing them // 'spki', 'spki_compressed', 'jwk', 'raw_compressed' - ['raw'].forEach(function(format) { + ['raw', 'spki'].forEach(function(format) { var algorithm = {name: vector.name, namedCurve: curve}; var data = keyData[curve]; if (format === "jwk") { // Not all fields used for public keys @@ -116,7 +116,7 @@ function testFormat(format, algorithm, data, keySize, usages, extractable) { if (compressed) { [format] = format.split("_compressed"); } - // promise_test(function(test) { + promise_test(function(test) { return subtle.importKey(format, keyData, algorithm, extractable, usages). then(function(key) { // TODO: @olegbespalov consider workaround for making this test pass @@ -143,7 +143,7 @@ function testFormat(format, algorithm, data, keySize, usages, extractable) { assert_unreached("Threw an unexpected error : " + err.toString() + ", format: " + format + " keyData: " + JSON.stringify(keyData) + " algorithm: " + JSON.stringify(algorithm) + " extractable: " + extractable + " usages: " + usages); } }); - // }, "Good parameters: " + keySize.toString() + " bits " + parameterString(format, compressed, keyData, algorithm, extractable, usages)); + }, "Good parameters: " + keySize.toString() + " bits " + parameterString(format, compressed, keyData, algorithm, extractable, usages)); } @@ -272,6 +272,14 @@ function parameterString(format, compressed, data, algorithm, extractable, usage return result; } +function promise_test(fn, name) { + try { + fn(); + } catch (e) { + throw Error(`Error in test "${name}": ${e}`); + } + } + // Character representation of any object we may use as a parameter. function objectToString(obj) { var keyValuePairs = []; diff --git a/webcrypto/tests/sign_verify/ecdsa.js b/webcrypto/tests/sign_verify/ecdsa.js new file mode 100644 index 0000000..1cd91b1 --- /dev/null +++ b/webcrypto/tests/sign_verify/ecdsa.js @@ -0,0 +1,518 @@ + +function run_test() { + // setup({explicit_done: true}); + + var subtle = crypto.subtle; // Change to test prefixed implementations + + // When are all these tests really done? When all the promises they use have resolved. + var all_promises = []; + + // Source file [algorithm_name]_vectors.js provides the getTestVectors method + // for the algorithm that drives these tests. + var testVectors = getTestVectors(); + var invalidTestVectors = getInvalidTestVectors(); + + // Test verification first, because signing tests rely on that working + testVectors.forEach(function(vector) { + var promise = importVectorKeys(vector, ["verify"], ["sign"]) + .then(function(vectors) { + var algorithm = {name: vector.algorithmName, hash: vector.hashName}; + promise_test(function(test) { + var operation = subtle.verify(algorithm, vector.publicKey, vector.signature, vector.plaintext) + .then(function(is_verified) { + assert_true(is_verified, "Signature verified"); + }, function(err) { + assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'"); + }); + + return operation; + }, vector.name + " verification"); + + }, function(err) { + // We need a failed test if the importVectorKey operation fails, so + // we know we never tested verification. + promise_test(function(test) { + assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''"); + }, "importVectorKeys step: " + vector.name + " verification"); + }); + + all_promises.push(promise); + }); + + // Test verification with an altered buffer after call + testVectors.forEach(function(vector) { + var promise = importVectorKeys(vector, ["verify"], ["sign"]) + .then(function(vectors) { + var algorithm = {name: vector.algorithmName, hash: vector.hashName}; + promise_test(function(test) { + var signature = copyBuffer(vector.signature); + var operation = subtle.verify(algorithm, vector.publicKey, signature, vector.plaintext) + .then(function(is_verified) { + assert_true(is_verified, "Signature verified"); + }, function(err) { + assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'"); + }); + + signature[0] = 255 - signature[0]; + return operation; + }, vector.name + " verification with altered signature after call"); + }, function(err) { + promise_test(function(test) { + assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''"); + }, "importVectorKeys step: " + vector.name + " verification with altered signature after call"); + }); + + all_promises.push(promise); + }); + + // Check for successful verification even if plaintext is altered after call. + testVectors.forEach(function(vector) { + var promise = importVectorKeys(vector, ["verify"], ["sign"]) + .then(function(vectors) { + var algorithm = {name: vector.algorithmName, hash: vector.hashName}; + promise_test(function(test) { + var plaintext = copyBuffer(vector.plaintext); + var operation = subtle.verify(algorithm, vector.publicKey, vector.signature, plaintext) + .then(function(is_verified) { + assert_true(is_verified, "Signature verified 3" + JSON.stringify({algorithm, vector, plaintext})); + }, function(err) { + assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'"); + }); + + plaintext[0] = 255 - plaintext[0]; + return operation; + }, vector.name + " with altered plaintext after call"); + }, function(err) { + promise_test(function(test) { + assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''"); + }, "importVectorKeys step: " + vector.name + " with altered plaintext after call"); + }); + + all_promises.push(promise); + }); + + // Check for failures due to using privateKey to verify. + testVectors.forEach(function(vector) { + var promise = importVectorKeys(vector, ["verify"], ["sign"]) + .then(function(vectors) { + var algorithm = {name: vector.algorithmName, hash: vector.hashName}; + promise_test(function(test) { + return subtle.verify(algorithm, vector.privateKey, vector.signature, vector.plaintext) + .then(function(plaintext) { + assert_unreached("Should have thrown error for using privateKey to verify in " + vector.name + ": " + err.message + "'"); + }, function(err) { + assert_equals(err.name, "InvalidAccessError", "Should throw InvalidAccessError instead of '" + err.message + "'"); + }); + }, vector.name + " using privateKey to verify"); + + }, function(err) { + promise_test(function(test) { + assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''"); + }, "importVectorKeys step: " + vector.name + " using privateKey to verify"); + }); + + all_promises.push(promise); + }); + + // Check for failures due to using publicKey to sign. + testVectors.forEach(function(vector) { + var promise = importVectorKeys(vector, ["verify"], ["sign"]) + .then(function(vectors) { + var algorithm = {name: vector.algorithmName, hash: vector.hashName}; + promise_test(function(test) { + return subtle.sign(algorithm, vector.publicKey, vector.plaintext) + .then(function(signature) { + assert_unreached("Should have thrown error for using publicKey to sign in " + vector.name + ": " + err.message + "'"); + }, function(err) { + assert_equals(err.name, "InvalidAccessError", "Should throw InvalidAccessError instead of '" + err.message + "'"); + }); + }, vector.name + " using publicKey to sign"); + }, function(err) { + promise_test(function(test) { + assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''"); + }, "importVectorKeys step: " + vector.name + " using publicKey to sign"); + }); + + all_promises.push(promise); + }); + + // Check for failures due to no "verify" usage. + testVectors.forEach(function(originalVector) { + var vector = Object.assign({}, originalVector); + + var promise = importVectorKeys(vector, [], ["sign"]) + .then(function(vectors) { + var algorithm = {name: vector.algorithmName, hash: vector.hashName}; + promise_test(function(test) { + return subtle.verify(algorithm, vector.publicKey, vector.signature, vector.plaintext) + .then(function(plaintext) { + assert_unreached("Should have thrown error for no verify usage in " + vector.name + ": " + err.message + "'"); + }, function(err) { + assert_equals(err.name, "InvalidAccessError", "Should throw InvalidAccessError instead of '" + err.message + "'"); + }); + }, vector.name + " no verify usage"); + }, function(err) { + promise_test(function(test) { + assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''"); + }, "importVectorKeys step: " + vector.name + " no verify usage"); + }); + + all_promises.push(promise); + }); + + // Check for successful signing and verification. + testVectors.forEach(function(vector) { + var promise = importVectorKeys(vector, ["verify"], ["sign"]) + .then(function(vectors) { + var algorithm = {name: vector.algorithmName, hash: vector.hashName}; + promise_test(function(test) { + return subtle.sign(algorithm, vector.privateKey, vector.plaintext) + .then(function(signature) { + // Can we verify the signature? + return subtle.verify(algorithm, vector.publicKey, signature, vector.plaintext) + .then(function(is_verified) { + assert_true(is_verified, "Round trip verification works"); + return signature; + }, function(err) { + assert_unreached("verify error for test " + vector.name + ": " + err.message + "'"); + }); + }, function(err) { + assert_unreached("sign error for test " + vector.name + ": '" + err.message + "'"); + }); + }, vector.name + " round trip"); + + }, function(err) { + // We need a failed test if the importVectorKey operation fails, so + // we know we never tested signing or verifying + promise_test(function(test) { + assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''"); + }, "importVectorKeys step: " + vector.name + " round trip"); + }); + + all_promises.push(promise); + }); + + // Test signing with the wrong algorithm + testVectors.forEach(function(vector) { + // Want to get the key for the wrong algorithm + var promise = subtle.generateKey({name: "HMAC", hash: "SHA-1"}, false, ["sign", "verify"]) + .then(function(wrongKey) { + var algorithm = {name: vector.algorithmName, hash: vector.hashName}; + return importVectorKeys(vector, ["verify"], ["sign"]) + .then(function(vectors) { + promise_test(function(test) { + var operation = subtle.sign(algorithm, wrongKey, vector.plaintext) + .then(function(signature) { + assert_unreached("Signing should not have succeeded for " + vector.name); + }, function(err) { + assert_equals(err.name, "InvalidAccessError", "Should have thrown InvalidAccessError instead of '" + err.message + "'"); + }); + + return operation; + }, vector.name + " signing with wrong algorithm name"); + + }, function(err) { + // We need a failed test if the importVectorKey operation fails, so + // we know we never tested verification. + promise_test(function(test) { + assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''"); + }, "importVectorKeys step: " + vector.name + " signing with wrong algorithm name"); + }); + }, function(err) { + promise_test(function(test) { + assert_unreached("Generate wrong key for test " + vector.name + " failed: '" + err.message + "'"); + }, "generate wrong key step: " + vector.name + " signing with wrong algorithm name"); + }); + + all_promises.push(promise); + }); + + // Test verification with the wrong algorithm + testVectors.forEach(function(vector) { + // Want to get the key for the wrong algorithm + var promise = subtle.generateKey({name: "HMAC", hash: "SHA-1"}, false, ["sign", "verify"]) + .then(function(wrongKey) { + return importVectorKeys(vector, ["verify"], ["sign"]) + .then(function(vectors) { + var algorithm = {name: vector.algorithmName, hash: vector.hashName}; + promise_test(function(test) { + var operation = subtle.verify(algorithm, wrongKey, vector.signature, vector.plaintext) + .then(function(signature) { + assert_unreached("Verifying should not have succeeded for " + vector.name); + }, function(err) { + assert_equals(err.name, "InvalidAccessError", "Should have thrown InvalidAccessError instead of '" + err.message + "'"); + }); + + return operation; + }, vector.name + " verifying with wrong algorithm name"); + + }, function(err) { + // We need a failed test if the importVectorKey operation fails, so + // we know we never tested verification. + promise_test(function(test) { + assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''"); + }, "importVectorKeys step: " + vector.name + " verifying with wrong algorithm name"); + }); + }, function(err) { + promise_test(function(test) { + assert_unreached("Generate wrong key for test " + vector.name + " failed: '" + err.message + "'"); + }, "generate wrong key step: " + vector.name + " verifying with wrong algorithm name"); + }); + + all_promises.push(promise); + }); + + // Test verification fails with wrong signature + testVectors.forEach(function(vector) { + var promise = importVectorKeys(vector, ["verify"], ["sign"]) + .then(function(vectors) { + var algorithm = {name: vector.algorithmName, hash: vector.hashName}; + var signature = copyBuffer(vector.signature); + signature[0] = 255 - signature[0]; + promise_test(function(test) { + var operation = subtle.verify(algorithm, vector.publicKey, signature, vector.plaintext) + .then(function(is_verified) { + assert_false(is_verified, "Signature NOT verified"); + }, function(err) { + assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'"); + }); + + return operation; + }, vector.name + " verification failure due to altered signature"); + + }, function(err) { + // We need a failed test if the importVectorKey operation fails, so + // we know we never tested verification. + promise_test(function(test) { + assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''"); + }, "importVectorKeys step: " + vector.name + " verification failure due to altered signature"); + }); + + all_promises.push(promise); + }); + + // Test verification fails with wrong hash + testVectors.forEach(function(vector) { + var promise = importVectorKeys(vector, ["verify"], ["sign"]) + .then(function(vectors) { + var hashName = "SHA-1"; + if (vector.hashName === "SHA-1") { + hashName = "SHA-256" + } + var algorithm = {name: vector.algorithmName, hash: hashName}; + promise_test(function(test) { + var operation = subtle.verify(algorithm, vector.publicKey, vector.signature, vector.plaintext) + .then(function(is_verified) { + assert_false(is_verified, "Signature NOT verified"); + }, function(err) { + assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'"); + }); + + return operation; + }, vector.name + " verification failure due to wrong hash"); + + }, function(err) { + // We need a failed test if the importVectorKey operation fails, so + // we know we never tested verification. + promise_test(function(test) { + assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''"); + }, "importVectorKeys step: " + vector.name + " verification failure due to wrong hash"); + }); + + all_promises.push(promise); + }); + + // Test verification fails with bad hash name + testVectors.forEach(function(vector) { + var promise = importVectorKeys(vector, ["verify"], ["sign"]) + .then(function(vectors) { + // use the wrong name for the hash + var hashName = vector.hashName.substring(0, 3) + vector.hashName.substring(4); + var algorithm = {name: vector.algorithmName, hash: hashName}; + promise_test(function(test) { + var operation = subtle.verify(algorithm, vector.publicKey, vector.signature, vector.plaintext) + .then(function(is_verified) { + assert_unreached("Verification should throw an error"); + }, function(err) { + assert_equals(err.name, "NotSupportedError", "Correctly throws NotSupportedError for illegal hash name") + }); + + return operation; + }, vector.name + " verification failure due to bad hash name"); + + }, function(err) { + // We need a failed test if the importVectorKey operation fails, so + // we know we never tested verification. + promise_test(function(test) { + assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''"); + }, "importVectorKeys step: " + vector.name + " verification failure due to bad hash name"); + }); + + all_promises.push(promise); + }); + + // Test verification fails with short (odd length) signature + testVectors.forEach(function(vector) { + var promise = importVectorKeys(vector, ["verify"], ["sign"]) + .then(function(vectors) { + var algorithm = {name: vector.algorithmName, hash: vector.hashName}; + var signature = vector.signature.slice(1); // Skip the first byte + promise_test(function(test) { + var operation = subtle.verify(algorithm, vector.publicKey, signature, vector.plaintext) + .then(function(is_verified) { + assert_false(is_verified, "Signature NOT verified"); + }, function(err) { + assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'"); + }); + + return operation; + }, vector.name + " verification failure due to shortened signature"); + + }, function(err) { + // We need a failed test if the importVectorKey operation fails, so + // we know we never tested verification. + promise_test(function(test) { + assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''"); + }, "importVectorKeys step: " + vector.name + " verification failure due to shortened signature"); + }); + + all_promises.push(promise); + }); + + // Test verification fails with wrong plaintext + testVectors.forEach(function(vector) { + var promise = importVectorKeys(vector, ["verify"], ["sign"]) + .then(function(vectors) { + var algorithm = {name: vector.algorithmName, hash: vector.hashName}; + var plaintext = copyBuffer(vector.plaintext); + plaintext[0] = 255 - plaintext[0]; + promise_test(function(test) { + var operation = subtle.verify(algorithm, vector.publicKey, vector.signature, plaintext) + .then(function(is_verified) { + assert_false(is_verified, "Signature NOT verified"); + }, function(err) { + assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'"); + }); + + return operation; + }, vector.name + " verification failure due to altered plaintext"); + + }, function(err) { + // We need a failed test if the importVectorKey operation fails, so + // we know we never tested verification. + promise_test(function(test) { + assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''"); + }, "importVectorKeys step: " + vector.name + " verification failure due to altered plaintext"); + }); + + all_promises.push(promise); + }); + + // Test invalid signatures + invalidTestVectors.forEach(function(vector) { + var promise = importVectorKeys(vector, ["verify"], ["sign"]) + .then(function(vectors) { + var algorithm = {name: vector.algorithmName, hash: vector.hashName}; + promise_test(function(test) { + var operation = subtle.verify(algorithm, vector.publicKey, vector.signature, vector.plaintext) + .then(function(is_verified) { + + assert_false(is_verified, "Signature unexpectedly verified: " + JSON.stringify({algorithm, vector})); + }, function(err) { + assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'"); + }); + + return operation; + }, vector.name + " verification"); + + }, function(err) { + // We need a failed test if the importVectorKey operation fails, so + // we know we never tested verification. + promise_test(function(test) { + assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''"); + }, "importVectorKeys step: " + vector.name + " verification"); + }); + + all_promises.push(promise); + }); + + // promise_test(function() { + // return Promise.all(all_promises) + // .then(function() {done();}) + // .catch(function() {done();}) + // }, "setup"); + + // A test vector has all needed fields for signing and verifying, EXCEPT that the + // key field may be null. This function replaces that null with the Correct + // CryptoKey object. + // + // Returns a Promise that yields an updated vector on success. + function importVectorKeys(vector, publicKeyUsages, privateKeyUsages) { + var publicPromise, privatePromise; + + if (vector.publicKey !== null) { + publicPromise = new Promise(function(resolve, reject) { + resolve(vector); + }); + } else { + publicPromise = subtle.importKey(vector.publicKeyFormat, vector.publicKeyBuffer, {name: vector.algorithmName, namedCurve: vector.namedCurve}, false, publicKeyUsages) + .then(function(key) { + vector.publicKey = key; + return vector; + }); // Returns a copy of the sourceBuffer it is sent. + } + + if (vector.privateKey !== null) { + privatePromise = new Promise(function(resolve, reject) { + resolve(vector); + }); + } else { + privatePromise = subtle.importKey(vector.privateKeyFormat, vector.privateKeyBuffer, {name: vector.algorithmName, namedCurve: vector.namedCurve}, false, privateKeyUsages) + .then(function(key) { + vector.privateKey = key; + return vector; + }); + } + + return Promise.all([publicPromise, privatePromise]); + } + + // Returns a copy of the sourceBuffer it is sent. + function copyBuffer(sourceBuffer) { + var source = new Uint8Array(sourceBuffer); + var copy = new Uint8Array(sourceBuffer.byteLength) + + for (var i=0; i