|
2 | 2 | #include "openssl/sha.h"
|
3 | 3 | #include <fmt/format.h>
|
4 | 4 | #include <iostream>
|
| 5 | +#include <openssl/ecdsa.h> |
5 | 6 | #include <optional>
|
6 | 7 | #include <span>
|
7 | 8 | #include <vector>
|
|
16 | 17 | namespace builtins {
|
17 | 18 |
|
18 | 19 | namespace {
|
| 20 | +int numBitsToBytes(int x) { return (x / 8) + (7 + (x % 8)) / 8; } |
| 21 | + |
| 22 | +std::pair<mozilla::UniquePtr<uint8_t[], JS::FreePolicy>, size_t> |
| 23 | +convertToBytesExpand(JSContext *cx, const BIGNUM *bignum, size_t minimumBufferSize) { |
| 24 | + int length = BN_num_bytes(bignum); |
| 25 | + |
| 26 | + size_t bufferSize = std::max<size_t>(length, minimumBufferSize); |
| 27 | + mozilla::UniquePtr<uint8_t[], JS::FreePolicy> bytes{ |
| 28 | + static_cast<uint8_t *>(JS_malloc(cx, bufferSize))}; |
| 29 | + |
| 30 | + size_t paddingLength = bufferSize - length; |
| 31 | + if (paddingLength > 0) { |
| 32 | + uint8_t padding = BN_is_negative(bignum) ? 0xFF : 0x00; |
| 33 | + std::fill_n(bytes.get(), paddingLength, padding); |
| 34 | + } |
| 35 | + BN_bn2bin(bignum, bytes.get() + paddingLength); |
| 36 | + return std::pair<mozilla::UniquePtr<uint8_t[], JS::FreePolicy>, size_t>(std::move(bytes), |
| 37 | + bufferSize); |
| 38 | +} |
| 39 | + |
| 40 | +const EVP_MD *createDigestAlgorithm(JSContext *cx, CryptoAlgorithmIdentifier hashIdentifier) { |
| 41 | + switch (hashIdentifier) { |
| 42 | + case CryptoAlgorithmIdentifier::MD5: { |
| 43 | + return EVP_md5(); |
| 44 | + } |
| 45 | + case CryptoAlgorithmIdentifier::SHA_1: { |
| 46 | + return EVP_sha1(); |
| 47 | + } |
| 48 | + case CryptoAlgorithmIdentifier::SHA_256: { |
| 49 | + return EVP_sha256(); |
| 50 | + } |
| 51 | + case CryptoAlgorithmIdentifier::SHA_384: { |
| 52 | + return EVP_sha384(); |
| 53 | + } |
| 54 | + case CryptoAlgorithmIdentifier::SHA_512: { |
| 55 | + return EVP_sha512(); |
| 56 | + } |
| 57 | + default: { |
| 58 | + DOMException::raise(cx, "NotSupportedError", "NotSupportedError"); |
| 59 | + return nullptr; |
| 60 | + } |
| 61 | + } |
| 62 | +} |
19 | 63 |
|
20 | 64 | const EVP_MD *createDigestAlgorithm(JSContext *cx, JS::HandleObject key) {
|
21 | 65 |
|
@@ -366,6 +410,30 @@ JS::Result<builtins::NamedCurve> toNamedCurve(JSContext *cx, JS::HandleValue val
|
366 | 410 | }
|
367 | 411 | }
|
368 | 412 |
|
| 413 | +JS::Result<size_t> curveSize(JSContext *cx, JS::HandleObject key) { |
| 414 | + |
| 415 | + JS::RootedObject alg(cx, CryptoKey::get_algorithm(key)); |
| 416 | + |
| 417 | + JS::RootedValue namedCurve_val(cx); |
| 418 | + JS_GetProperty(cx, alg, "namedCurve", &namedCurve_val); |
| 419 | + auto namedCurve_chars = core::encode(cx, namedCurve_val); |
| 420 | + if (!namedCurve_chars) { |
| 421 | + return JS::Result<size_t>(JS::Error()); |
| 422 | + } |
| 423 | + |
| 424 | + std::string_view namedCurve = namedCurve_chars; |
| 425 | + if (namedCurve == "P-256") { |
| 426 | + return 256; |
| 427 | + } else if (namedCurve == "P-384") { |
| 428 | + return 384; |
| 429 | + } else if (namedCurve == "P-521") { |
| 430 | + return 521; |
| 431 | + } |
| 432 | + |
| 433 | + MOZ_ASSERT_UNREACHABLE(); |
| 434 | + return 0; |
| 435 | +} |
| 436 | + |
369 | 437 | // This implements the first section of
|
370 | 438 | // https://w3c.github.io/webcrypto/#algorithm-normalization-normalize-an-algorithm which is shared
|
371 | 439 | // across all the diffent algorithms, but importantly does not implement the parts to do with the
|
@@ -755,12 +823,163 @@ JSObject *CryptoAlgorithmHMAC_Sign_Verify::toObject(JSContext *cx) {
|
755 | 823 | JSObject *CryptoAlgorithmECDSA_Sign_Verify::sign(JSContext *cx, JS::HandleObject key, std::span<uint8_t> data) {
|
756 | 824 | MOZ_ASSERT(CryptoKey::is_instance(key));
|
757 | 825 |
|
758 |
| - return nullptr; |
| 826 | + // 1. If the [[type]] internal slot of key is not "private", then throw an InvalidAccessError. |
| 827 | + if (CryptoKey::type(key) != CryptoKeyType::Private) { |
| 828 | + DOMException::raise(cx, "InvalidAccessError", "InvalidAccessError"); |
| 829 | + return nullptr; |
| 830 | + } |
| 831 | + |
| 832 | + // 2. Let hashAlgorithm be the hash member of normalizedAlgorithm. |
| 833 | + const EVP_MD* algorithm = createDigestAlgorithm(cx, this->hashIdentifier); |
| 834 | + if (!algorithm) { |
| 835 | + DOMException::raise(cx, "SubtleCrypto.sign: failed to sign", "OperationError"); |
| 836 | + return nullptr; |
| 837 | + } |
| 838 | + |
| 839 | + // 3. Let M be the result of performing the digest operation specified by hashAlgorithm using message. |
| 840 | + auto digestOption = ::builtins::rawDigest(cx, data, algorithm, EVP_MD_size(algorithm)); |
| 841 | + if (!digestOption.has_value()) { |
| 842 | + DOMException::raise(cx, "OperationError", "OperationError"); |
| 843 | + return nullptr; |
| 844 | + } |
| 845 | + |
| 846 | + auto digest = digestOption.value(); |
| 847 | + |
| 848 | + // 4. Let d be the ECDSA private key associated with key. |
| 849 | + #pragma clang diagnostic push |
| 850 | + #pragma clang diagnostic ignored "-Wdeprecated-declarations" |
| 851 | + const EC_KEY * ecKey = EVP_PKEY_get0_EC_KEY(CryptoKey::key(key)); |
| 852 | + #pragma clang diagnostic pop |
| 853 | + if (!ecKey) { |
| 854 | + DOMException::raise(cx, "SubtleCrypto.verify: failed to verify", "OperationError"); |
| 855 | + return nullptr; |
| 856 | + } |
| 857 | + |
| 858 | + // 5. Let params be the EC domain parameters associated with key. |
| 859 | + #pragma clang diagnostic push |
| 860 | + #pragma clang diagnostic ignored "-Wdeprecated-declarations" |
| 861 | + auto sig = ECDSA_do_sign(digest.data(), digest.size(), const_cast<EC_KEY*>(ecKey)); |
| 862 | + #pragma clang diagnostic pop |
| 863 | + if (!sig) { |
| 864 | + DOMException::raise(cx, "SubtleCrypto.verify: failed to verify", "OperationError"); |
| 865 | + return nullptr; |
| 866 | + } |
| 867 | + |
| 868 | + // 6. If the namedCurve attribute of the [[algorithm]] internal slot of key is "P-256", "P-384" or "P-521": |
| 869 | + // Perform the ECDSA signing process, as specified in [RFC6090], Section 5.4, with M as the message, using params as the EC domain parameters, and with d as the private key. |
| 870 | + // Let r and s be the pair of integers resulting from performing the ECDSA signing process. |
| 871 | + // Let result be an empty byte sequence. |
| 872 | + // Let n be the smallest integer such that n * 8 is greater than the logarithm to base 2 of the order of the base point of the elliptic curve identified by params. |
| 873 | + // Convert r to an octet string of length n and append this sequence of bytes to result. |
| 874 | + // Convert s to an octet string of length n and append this sequence of bytes to result. |
| 875 | + // Otherwise, the namedCurve attribute of the [[algorithm]] internal slot of key is a value specified in an applicable specification: |
| 876 | + // Perform the ECDSA signature steps specified in that specification, passing in M, params and d and resulting in result. |
| 877 | + const BIGNUM* r; |
| 878 | + const BIGNUM* s; |
| 879 | + ECDSA_SIG_get0(sig, &r, &s); |
| 880 | + auto keySize = curveSize(cx, key); |
| 881 | + if (keySize.isErr()) { |
| 882 | + return nullptr; |
| 883 | + } |
| 884 | + |
| 885 | + size_t keySizeInBytes = numBitsToBytes(keySize.unwrap()); |
| 886 | + |
| 887 | + auto rBytesAndSize = convertToBytesExpand(cx, r, keySizeInBytes); |
| 888 | + auto *rBytes = rBytesAndSize.first.get(); |
| 889 | + auto rBytesSize = rBytesAndSize.second; |
| 890 | + |
| 891 | + auto sBytesAndSize = convertToBytesExpand(cx, s, keySizeInBytes); |
| 892 | + auto *sBytes = sBytesAndSize.first.get(); |
| 893 | + auto sBytesSize = sBytesAndSize.second; |
| 894 | + |
| 895 | + auto resultSize = rBytesSize + sBytesSize; |
| 896 | + mozilla::UniquePtr<uint8_t[], JS::FreePolicy> result{ |
| 897 | + static_cast<uint8_t *>(JS_malloc(cx, resultSize))}; |
| 898 | + |
| 899 | + std::memcpy(result.get(), rBytes, rBytesSize); |
| 900 | + std::memcpy(result.get() + rBytesSize, sBytes, sBytesSize); |
| 901 | + |
| 902 | + // 7. Return the result of creating an ArrayBuffer containing result. |
| 903 | + JS::RootedObject buffer(cx, JS::NewArrayBufferWithContents(cx, resultSize, result.get())); |
| 904 | + if (!buffer) { |
| 905 | + // We can be here is the array buffer was too large -- if that was the case then a |
| 906 | + // JSMSG_BAD_ARRAY_LENGTH will have been created. No other failure scenarios in this path will |
| 907 | + // create a JS exception and so we need to create one. |
| 908 | + if (!JS_IsExceptionPending(cx)) { |
| 909 | + // TODO Rename error to InternalError |
| 910 | + JS_ReportErrorLatin1(cx, "InternalError"); |
| 911 | + } |
| 912 | + return nullptr; |
| 913 | + } |
| 914 | + |
| 915 | + // `signature` is now owned by `buffer` |
| 916 | + static_cast<void>(result.release()); |
| 917 | + |
| 918 | + return buffer; |
759 | 919 | };
|
760 | 920 | JS::Result<bool> CryptoAlgorithmECDSA_Sign_Verify::verify(JSContext *cx, JS::HandleObject key, std::span<uint8_t> signature, std::span<uint8_t> data) {
|
761 | 921 | MOZ_ASSERT(CryptoKey::is_instance(key));
|
762 |
| - DOMException::raise(cx, "SubtleCrypto.verify: failed to verify", "OperationError"); |
763 |
| - return JS::Result<bool>(JS::Error()); |
| 922 | + // 1. If the [[type]] internal slot of key is not "public", then throw an InvalidAccessError. |
| 923 | + if (CryptoKey::type(key) != CryptoKeyType::Public) { |
| 924 | + DOMException::raise(cx, "InvalidAccessError", "InvalidAccessError"); |
| 925 | + return JS::Result<bool>(JS::Error()); |
| 926 | + } |
| 927 | + |
| 928 | + // 2. Let hashAlgorithm be the hash member of normalizedAlgorithm. |
| 929 | + const EVP_MD* algorithm = createDigestAlgorithm(cx, this->hashIdentifier); |
| 930 | + if (!algorithm) { |
| 931 | + DOMException::raise(cx, "SubtleCrypto.verify: failed to verify", "OperationError"); |
| 932 | + return JS::Result<bool>(JS::Error()); |
| 933 | + } |
| 934 | + |
| 935 | + // 3. Let M be the result of performing the digest operation specified by hashAlgorithm using message. |
| 936 | + auto digestOption = ::builtins::rawDigest(cx, data, algorithm, EVP_MD_size(algorithm)); |
| 937 | + if (!digestOption.has_value()) { |
| 938 | + DOMException::raise(cx, "OperationError", "OperationError"); |
| 939 | + return JS::Result<bool>(JS::Error()); |
| 940 | + } |
| 941 | + |
| 942 | + auto digest = digestOption.value(); |
| 943 | + |
| 944 | + // 4. Let Q be the ECDSA public key associated with key. |
| 945 | + #pragma clang diagnostic push |
| 946 | + #pragma clang diagnostic ignored "-Wdeprecated-declarations" |
| 947 | + const EC_KEY * ecKey = EVP_PKEY_get0_EC_KEY(CryptoKey::key(key)); |
| 948 | + #pragma clang diagnostic pop |
| 949 | + if (!ecKey) { |
| 950 | + DOMException::raise(cx, "SubtleCrypto.verify: failed to verify", "OperationError"); |
| 951 | + return JS::Result<bool>(JS::Error()); |
| 952 | + } |
| 953 | + |
| 954 | + // 5. Let params be the EC domain parameters associated with key. |
| 955 | + // 6. If the namedCurve attribute of the [[algorithm]] internal slot of key is "P-256", "P-384" or "P-521": |
| 956 | + // Perform the ECDSA verifying process, as specified in RFC6090, Section 5.3, with M as the received message, signature as the received signature and using params as the EC domain parameters, and Q as the public key. |
| 957 | + // Otherwise, the namedCurve attribute of the [[algorithm]] internal slot of key is a value specified in an applicable specification: |
| 958 | + // Perform the ECDSA verification steps specified in that specification passing in M, signature, params and Q and resulting in an indication of whether or not the purported signature is valid. |
| 959 | + auto keySize = curveSize(cx, key); |
| 960 | + if (keySize.isErr()) { |
| 961 | + return JS::Result<bool>(JS::Error()); |
| 962 | + } |
| 963 | + |
| 964 | + size_t keySizeInBytes = numBitsToBytes(keySize.unwrap()); |
| 965 | + |
| 966 | + auto sig = ECDSA_SIG_new(); |
| 967 | + auto r = BN_bin2bn(signature.data(), keySizeInBytes, nullptr); |
| 968 | + auto s = BN_bin2bn(signature.data() + keySizeInBytes, keySizeInBytes, nullptr); |
| 969 | + |
| 970 | + if (!ECDSA_SIG_set0(sig, r, s)) { |
| 971 | + DOMException::raise(cx, "SubtleCrypto.verify: failed to verify", "OperationError"); |
| 972 | + return JS::Result<bool>(JS::Error()); |
| 973 | + } |
| 974 | + |
| 975 | + // 7. Let result be a boolean with the value true if the signature is valid and the value false otherwise. |
| 976 | + #pragma clang diagnostic push |
| 977 | + #pragma clang diagnostic ignored "-Wdeprecated-declarations" |
| 978 | + bool result = ECDSA_do_verify(digest.data(), digest.size(), sig, const_cast<EC_KEY*>(ecKey)) == 1; |
| 979 | + #pragma clang diagnostic pop |
| 980 | + |
| 981 | + // 8. Return result. |
| 982 | + return result; |
764 | 983 | };
|
765 | 984 | JSObject *CryptoAlgorithmECDSA_Sign_Verify::toObject(JSContext *cx) {
|
766 | 985 | return nullptr;
|
|
0 commit comments