File
Blob: src/workerd/api/crypto/impl.c++
| 1 | // Copyright (c) 2017-2022 Cloudflare, Inc. |
| 2 | // Licensed under the Apache 2.0 license found in the LICENSE file or at: |
| 3 | // https://opensource.org/licenses/Apache-2.0 |
| 4 | |
| 5 | #include "impl.h" |
| 6 | |
| 7 | #include "simdutf.h" |
| 8 | |
| 9 | #include <workerd/api/util.h> |
| 10 | #include <workerd/jsg/jsvalue.h> |
| 11 | #include <workerd/jsg/memory.h> |
| 12 | |
| 13 | #include <openssl/bn.h> |
| 14 | #include <openssl/crypto.h> |
| 15 | #include <openssl/ec.h> |
| 16 | #include <openssl/err.h> |
| 17 | #include <openssl/evp.h> |
| 18 | #include <openssl/rand.h> |
| 19 | #include <openssl/rsa.h> |
| 20 | |
| 21 | #include <algorithm> |
| 22 | #include <map> |
| 23 | |
| 24 | namespace workerd::api { |
| 25 | namespace { |
| 26 | kj::String errorsToString( |
| 27 | kj::Array<kj::OneOf<kj::StringPtr, OpensslUntranslatedError>> accumulatedErrors, |
| 28 | kj::StringPtr defaultIfNoError) { |
| 29 | if (accumulatedErrors.size() == 0) { |
| 30 | return kj::str(defaultIfNoError); |
| 31 | } |
| 32 | |
| 33 | if (accumulatedErrors.size() == 1) { |
| 34 | kj::String heap; |
| 35 | kj::StringPtr description; |
| 36 | KJ_SWITCH_ONEOF(accumulatedErrors[0]) { |
| 37 | KJ_CASE_ONEOF(e, kj::StringPtr) { |
| 38 | description = e; |
| 39 | } |
| 40 | KJ_CASE_ONEOF(e, OpensslUntranslatedError) { |
| 41 | heap = kj::str(e.library, " ", e.reasonName); |
| 42 | description = heap; |
| 43 | } |
| 44 | } |
| 45 | return kj::str(": ", description, "."); |
| 46 | } |
| 47 | |
| 48 | return kj::str(": ", |
| 49 | kj::strArray( |
| 50 | KJ_MAP(e, accumulatedErrors) { |
| 51 | KJ_SWITCH_ONEOF(accumulatedErrors[0]) { |
| 52 | KJ_CASE_ONEOF(e, kj::StringPtr) { |
| 53 | return e; |
| 54 | } |
| 55 | KJ_CASE_ONEOF(e, OpensslUntranslatedError) { |
| 56 | return e.reasonName; |
| 57 | } |
| 58 | } |
| 59 | KJ_UNREACHABLE; |
| 60 | }, " "), |
| 61 | "."); |
| 62 | } |
| 63 | } // namespace |
| 64 | |
| 65 | const SslArrayDisposer SslArrayDisposer::INSTANCE; |
| 66 | |
| 67 | void SslArrayDisposer::disposeImpl(void* firstElement, |
| 68 | size_t elementSize, |
| 69 | size_t elementCount, |
| 70 | size_t capacity, |
| 71 | void (*destroyElement)(void*)) const { |
| 72 | OPENSSL_free(firstElement); |
| 73 | } |
| 74 | |
| 75 | // Call when an OpenSSL function returns an error code to convert that into an exception and |
| 76 | // throw it. |
| 77 | void throwOpensslError(const char* file, int line, kj::StringPtr code) { |
| 78 | // Some error codes that we know are the application's fault are converted to app errors. |
| 79 | // We only attempt to convert the most-recent error in the queue this way, because other errors |
| 80 | // in the queue might have been accidentally left there by previous, unrelated operations. |
| 81 | // Unfortunately BoringSSL's ERR_error_string() and friends produce unfriendly strings that |
| 82 | // mostly just tell you the error constant name, which isn't what we want to throw at users. |
| 83 | switch (ERR_GET_LIB(ERR_peek_last_error())) { |
| 84 | // The error code defines overlap between the different BoringSSL libraries (for example, we |
| 85 | // have EC_R_INVALID_ENCODING == RSA_R_CANNOT_RECOVER_MULTI_PRIME_KEY), so we must check the |
| 86 | // library code. |
| 87 | case ERR_LIB_EC: |
| 88 | switch (ERR_GET_REASON(ERR_peek_last_error())) { |
| 89 | #define MAP_ERROR(CODE, TEXT) \ |
| 90 | case CODE: { \ |
| 91 | ClearErrorOnReturn clearErrorOnReturn; \ |
| 92 | kj::throwFatalException(kj::Exception(kj::Exception::Type::FAILED, file, line, \ |
| 93 | kj::str(JSG_EXCEPTION(DOMOperationError) ": ", TEXT))); \ |
| 94 | } |
| 95 | |
| 96 | MAP_ERROR(EC_R_INVALID_ENCODING, "Invalid point encoding.") |
| 97 | MAP_ERROR(EC_R_INVALID_COMPRESSED_POINT, "Invalid compressed point.") |
| 98 | MAP_ERROR(EC_R_POINT_IS_NOT_ON_CURVE, "Point is not on curve.") |
| 99 | default: |
| 100 | break; |
| 101 | }; |
| 102 | break; |
| 103 | case ERR_LIB_RSA: |
| 104 | switch (ERR_GET_REASON(ERR_peek_last_error())) { |
| 105 | MAP_ERROR(RSA_R_DATA_LEN_NOT_EQUAL_TO_MOD_LEN, "Invalid RSA signature."); |
| 106 | MAP_ERROR(RSA_R_KEY_SIZE_TOO_SMALL, "RSA key size is too small."); |
| 107 | MAP_ERROR(RSA_R_INTERNAL_ERROR, "RSA operation failed."); |
| 108 | #undef MAP_ERROR |
| 109 | |
| 110 | default: |
| 111 | break; |
| 112 | }; |
| 113 | break; |
| 114 | default: |
| 115 | // not an error code to be converted to app error, move on |
| 116 | break; |
| 117 | }; |
| 118 | |
| 119 | // We don't recognize the error as one that is the app's fault, so assume it is an internal |
| 120 | // error. Here we'll accept BoringSSL's ugly error strings as hopefully it's at least something |
| 121 | // we can decipher. |
| 122 | kj::Vector<kj::String> lines; |
| 123 | while (unsigned long long error = ERR_get_error()) { |
| 124 | char message[1024]{}; |
| 125 | ERR_error_string_n(error, message, sizeof(message)); |
| 126 | lines.add(kj::heapString(message)); |
| 127 | } |
| 128 | kj::throwFatalException(kj::Exception(kj::Exception::Type::FAILED, file, line, |
| 129 | kj::str("OpenSSL call failed: ", code, "; ", |
| 130 | lines.empty() ? "but ERR_get_error() returned 0"_kj : kj::strArray(lines, "; ")))); |
| 131 | } |
| 132 | |
| 133 | kj::Vector<kj::OneOf<kj::StringPtr, OpensslUntranslatedError>> consumeAllOpensslErrors() { |
| 134 | kj::Vector<kj::OneOf<kj::StringPtr, OpensslUntranslatedError>> accumulatedErrors; |
| 135 | |
| 136 | while (auto error = ERR_get_error()) { |
| 137 | accumulatedErrors.add([error]() -> kj::OneOf<kj::StringPtr, OpensslUntranslatedError> { |
| 138 | switch (ERR_GET_LIB(error)) { |
| 139 | case ERR_LIB_RSA: |
| 140 | switch (ERR_GET_REASON(error)) { |
| 141 | case RSA_R_DATA_LEN_NOT_EQUAL_TO_MOD_LEN: |
| 142 | return "Invalid RSA signature."_kj; |
| 143 | case RSA_R_KEY_SIZE_TOO_SMALL: |
| 144 | return "RSA key size is too small."_kj; |
| 145 | case RSA_R_INTERNAL_ERROR: |
| 146 | return "RSA operation failed."_kj; |
| 147 | } |
| 148 | break; |
| 149 | case ERR_LIB_EC: |
| 150 | switch (ERR_GET_REASON(error)) { |
| 151 | case EC_R_INVALID_ENCODING: |
| 152 | return "Invalid point encoding."_kj; |
| 153 | case EC_R_INVALID_COMPRESSED_POINT: |
| 154 | return "Invalid compressed point."_kj; |
| 155 | case EC_R_POINT_IS_NOT_ON_CURVE: |
| 156 | return "Point is not on curve."_kj; |
| 157 | case EC_R_UNKNOWN_GROUP: |
| 158 | return "Unsupported elliptic curve group."_kj; |
| 159 | } |
| 160 | break; |
| 161 | } |
| 162 | |
| 163 | return OpensslUntranslatedError{ |
| 164 | .library = ERR_lib_error_string(error), |
| 165 | .reasonName = ERR_reason_error_string(error), |
| 166 | }; |
| 167 | }()); |
| 168 | } |
| 169 | |
| 170 | return accumulatedErrors; |
| 171 | } |
| 172 | |
| 173 | kj::String tryDescribeOpensslErrors(kj::StringPtr defaultIfNoError) { |
| 174 | if (defaultIfNoError.size() == 0) { |
| 175 | defaultIfNoError = "."_kj; |
| 176 | } |
| 177 | |
| 178 | auto accumulatedErrors = consumeAllOpensslErrors(); |
| 179 | |
| 180 | // For now we only allow errors we explicitly map to friendly strings to be displayed to end |
| 181 | // users. #if 1 is convenient as it makes it easy to #if 0 to see the error codes printed when |
| 182 | // debugging issues. |
| 183 | #if 1 |
| 184 | auto removeBegin = std::remove_if(accumulatedErrors.begin(), accumulatedErrors.end(), |
| 185 | [](const auto& error) { return error.template is<OpensslUntranslatedError>(); }); |
| 186 | |
| 187 | accumulatedErrors.resize(removeBegin - accumulatedErrors.begin()); |
| 188 | #endif |
| 189 | |
| 190 | return errorsToString(accumulatedErrors.releaseAsArray(), defaultIfNoError); |
| 191 | } |
| 192 | |
| 193 | kj::String internalDescribeOpensslErrors() { |
| 194 | return errorsToString(consumeAllOpensslErrors().releaseAsArray(), "."_kj); |
| 195 | } |
| 196 | |
| 197 | std::pair<kj::StringPtr, const EVP_MD*> lookupDigestAlgorithm(kj::StringPtr algorithm) { |
| 198 | static const std::map<kj::StringPtr, const EVP_MD*, CiLess> registeredAlgorithms{ |
| 199 | {"SHA-1", EVP_sha1()}, |
| 200 | {"SHA-256", EVP_sha256()}, |
| 201 | {"SHA-384", EVP_sha384()}, |
| 202 | {"SHA-512", EVP_sha512()}, |
| 203 | |
| 204 | // MD5 is not supported by WebCrypto, presumably because the designers didn't want to |
| 205 | // support broken crypto. However, the reality is that people still use MD5 for things, and if |
| 206 | // we don't give them a native implementation, they're going to use a pure-JS implementation, |
| 207 | // leaving everyone worse-off. |
| 208 | {"MD5", EVP_md5()}, |
| 209 | }; |
| 210 | |
| 211 | auto algIter = registeredAlgorithms.find(algorithm); |
| 212 | JSG_REQUIRE(algIter != registeredAlgorithms.end(), DOMNotSupportedError, |
| 213 | "Unrecognized or unimplemented digest algorithm requested."); |
| 214 | return *algIter; |
| 215 | } |
| 216 | |
| 217 | kj::EncodingResult<kj::Array<kj::byte>> decodeBase64Url(kj::String text) { |
| 218 | // TODO(cleanup): Make a non-mutating version of this and put in kj-encoding. Or add a |
| 219 | // "bool urlEncoded = false" parameter to kj::decodeBase64()? |
| 220 | |
| 221 | std::replace(text.begin(), text.end(), '-', '+'); |
| 222 | std::replace(text.begin(), text.end(), '_', '/'); |
| 223 | return kj::decodeBase64(text); |
| 224 | } |
| 225 | |
| 226 | bool CryptoKey::Impl::equals(const kj::Array<kj::byte>& other) const { |
| 227 | KJ_FAIL_REQUIRE("Unable to compare raw key material for this key"); |
| 228 | } |
| 229 | |
| 230 | kj::Own<CryptoKey::Impl> CryptoKey::Impl::from(jsg::Lock& js, kj::Own<EVP_PKEY> key) { |
| 231 | switch (EVP_PKEY_id(key.get())) { |
| 232 | case EVP_PKEY_RSA: |
| 233 | return fromRsaKey(js, kj::mv(key)); |
| 234 | case EVP_PKEY_EC: |
| 235 | return fromEcKey(kj::mv(key)); |
| 236 | case EVP_PKEY_ED25519: |
| 237 | return fromEd25519Key(kj::mv(key)); |
| 238 | default: |
| 239 | JSG_FAIL_REQUIRE(TypeError, "Unsupported key type"); |
| 240 | } |
| 241 | KJ_UNREACHABLE; |
| 242 | } |
| 243 | |
| 244 | ZeroOnFree::~ZeroOnFree() noexcept(false) { |
| 245 | OPENSSL_cleanse(inner.begin(), inner.size()); |
| 246 | } |
| 247 | |
| 248 | void checkPbkdfLimits(jsg::Lock& js, size_t iterations) { |
| 249 | auto& limits = Worker::Isolate::from(js).getLimitEnforcer(); |
| 250 | KJ_IF_SOME(max, limits.checkPbkdfIterations(js, iterations)) { |
| 251 | JSG_FAIL_REQUIRE(DOMNotSupportedError, |
| 252 | kj::str("Pbkdf2 failed: iteration counts above ", max, " are not supported (requested ", |
| 253 | iterations, ").")); |
| 254 | } |
| 255 | } |
| 256 | |
| 257 | kj::Maybe<kj::Own<BIGNUM>> toBignum(kj::ArrayPtr<const kj::byte> data) { |
| 258 | BIGNUM* result = BN_bin2bn(data.begin(), data.size(), nullptr); |
| 259 | if (result == nullptr) return kj::none; |
| 260 | return kj::Own<BIGNUM>(result, workerd::api::SslDisposer<BIGNUM, &BIGNUM_free>::INSTANCE); |
| 261 | } |
| 262 | |
| 263 | BIGNUM* toBignumUnowned(kj::ArrayPtr<const kj::byte> data) { |
| 264 | auto result = BN_bin2bn(data.begin(), data.size(), nullptr); |
| 265 | JSG_REQUIRE(result != nullptr, DOMOperationError, "Error importing BIGNUM"); |
| 266 | return result; |
| 267 | } |
| 268 | |
| 269 | UniqueBignum toBignumOwned(kj::ArrayPtr<const kj::byte> data) { |
| 270 | return UniqueBignum(toBignumUnowned(data), &BN_clear_free); |
| 271 | } |
| 272 | |
| 273 | kj::Maybe<kj::Array<kj::byte>> bignumToArray(const BIGNUM& n) { |
| 274 | auto result = kj::heapArray<kj::byte>(BN_num_bytes(&n)); |
| 275 | if (BN_bn2bin(&n, result.begin()) != result.size()) return kj::none; |
| 276 | return kj::mv(result); |
| 277 | } |
| 278 | |
| 279 | kj::Maybe<kj::Array<kj::byte>> bignumToArrayPadded(const BIGNUM& n) { |
| 280 | auto result = kj::heapArray<kj::byte>(BN_num_bytes(&n)); |
| 281 | if (BN_bn2binpad(&n, result.begin(), result.size()) != result.size()) return kj::none; |
| 282 | return kj::mv(result); |
| 283 | } |
| 284 | |
| 285 | kj::Maybe<kj::Array<kj::byte>> bignumToArrayPadded(const BIGNUM& n, size_t paddedLength) { |
| 286 | auto result = kj::heapArray<kj::byte>(paddedLength); |
| 287 | if (BN_bn2bin_padded(result.begin(), paddedLength, &n) == 0) { |
| 288 | return kj::none; |
| 289 | } |
| 290 | return kj::mv(result); |
| 291 | } |
| 292 | |
| 293 | kj::Maybe<jsg::JsUint8Array> bignumToArray(jsg::Lock& js, const BIGNUM& n) { |
| 294 | auto buf = jsg::JsUint8Array::create(js, BN_num_bytes(&n)); |
| 295 | if (BN_bn2bin(&n, buf.asArrayPtr().begin()) != buf.asArrayPtr().size()) return kj::none; |
| 296 | return buf; |
| 297 | } |
| 298 | |
| 299 | kj::Maybe<jsg::JsUint8Array> bignumToArrayPadded(jsg::Lock& js, const BIGNUM& n) { |
| 300 | auto buf = jsg::JsUint8Array::create(js, BN_num_bytes(&n)); |
| 301 | if (BN_bn2binpad(&n, buf.asArrayPtr().begin(), buf.asArrayPtr().size()) != |
| 302 | buf.asArrayPtr().size()) { |
| 303 | return kj::none; |
| 304 | } |
| 305 | return buf; |
| 306 | } |
| 307 | |
| 308 | kj::Maybe<jsg::JsUint8Array> bignumToArrayPadded( |
| 309 | jsg::Lock& js, const BIGNUM& n, size_t paddedLength) { |
| 310 | auto buf = jsg::JsUint8Array::create(js, paddedLength); |
| 311 | if (BN_bn2bin_padded(buf.asArrayPtr().begin(), paddedLength, &n) == 0) { |
| 312 | return kj::none; |
| 313 | } |
| 314 | return buf; |
| 315 | } |
| 316 | |
| 317 | kj::Own<BIGNUM> newBignum() { |
| 318 | return kj::Own<BIGNUM>(BN_new(), workerd::api::SslDisposer<BIGNUM, &BIGNUM_free>::INSTANCE); |
| 319 | } |
| 320 | |
| 321 | void CryptoKey::visitForMemoryInfo(jsg::MemoryTracker& tracker) const { |
| 322 | tracker.trackField("impl", impl); |
| 323 | } |
| 324 | |
| 325 | bool CSPRNG(kj::ArrayPtr<kj::byte> buffer) { |
| 326 | do { |
| 327 | if (1 == RAND_status()) |
| 328 | if (1 == RAND_bytes(buffer.begin(), buffer.size())) return true; |
| 329 | #if OPENSSL_VERSION_MAJOR >= 3 |
| 330 | const auto code = ERR_peek_last_error(); |
| 331 | // A misconfigured OpenSSL 3 installation may report 1 from RAND_poll() |
| 332 | // and RAND_status() but fail in RAND_bytes() if it cannot look up |
| 333 | // a matching algorithm for the CSPRNG. |
| 334 | if (ERR_GET_LIB(code) == ERR_LIB_RAND) { |
| 335 | const auto reason = ERR_GET_REASON(code); |
| 336 | if (reason == RAND_R_ERROR_INSTANTIATING_DRBG || reason == RAND_R_UNABLE_TO_FETCH_DRBG || |
| 337 | reason == RAND_R_UNABLE_TO_CREATE_DRBG) { |
| 338 | return false; |
| 339 | } |
| 340 | } |
| 341 | #endif |
| 342 | } while (1 == RAND_poll()); |
| 343 | |
| 344 | return false; |
| 345 | } |
| 346 | |
| 347 | kj::Maybe<kj::ArrayPtr<const kj::byte>> tryGetAsn1Sequence(kj::ArrayPtr<const kj::byte> data) { |
| 348 | if (data.size() < 2 || data[0] != 0x30) return kj::none; |
| 349 | |
| 350 | if (data[1] & 0x80) { |
| 351 | // Long form. |
| 352 | size_t n_bytes = data[1] & ~0x80; |
| 353 | if (n_bytes + 2 > data.size() || n_bytes > sizeof(size_t)) return kj::none; |
| 354 | size_t length = 0; |
| 355 | for (size_t i = 0; i < n_bytes; i++) length = (length << 8) | data[i + 2]; |
| 356 | auto start = 2 + n_bytes; |
| 357 | auto end = start + kj::min(data.size() - 2 - n_bytes, length); |
| 358 | return data.slice(start, end); |
| 359 | } |
| 360 | |
| 361 | // Short form. |
| 362 | auto start = 2; |
| 363 | auto end = start + kj::min(data.size() - 2, data[1]); |
| 364 | return data.slice(start, end); |
| 365 | } |
| 366 | |
| 367 | kj::Maybe<kj::Array<kj::byte>> simdutfBase64UrlDecode(kj::StringPtr input) { |
| 368 | auto size = simdutf::maximal_binary_length_from_base64(input.begin(), input.size()); |
| 369 | auto buf = kj::heapArray<kj::byte>(size); |
| 370 | auto result = simdutf::base64_to_binary( |
| 371 | input.begin(), input.size(), buf.asChars().begin(), simdutf::base64_url); |
| 372 | if (result.error != simdutf::SUCCESS) return kj::none; |
| 373 | KJ_ASSERT(result.count <= size); |
| 374 | return buf.slice(0, result.count).attach(kj::mv(buf)); |
| 375 | } |
| 376 | |
| 377 | kj::Maybe<jsg::JsUint8Array> simdutfBase64UrlDecode(jsg::Lock& js, kj::StringPtr input) { |
| 378 | auto size = simdutf::maximal_binary_length_from_base64(input.begin(), input.size()); |
| 379 | KJ_STACK_ARRAY(kj::byte, buf, size, 1024, 4096); |
| 380 | auto result = simdutf::base64_to_binary( |
| 381 | input.begin(), input.size(), buf.asChars().begin(), simdutf::base64_url); |
| 382 | if (result.error != simdutf::SUCCESS) return kj::none; |
| 383 | KJ_ASSERT(result.count <= size); |
| 384 | |
| 385 | return jsg::JsUint8Array::create(js, buf.first(result.count)); |
| 386 | } |
| 387 | |
| 388 | jsg::JsUint8Array simdutfBase64UrlDecodeChecked( |
| 389 | jsg::Lock& js, kj::StringPtr input, kj::StringPtr error) { |
| 390 | return JSG_REQUIRE_NONNULL(simdutfBase64UrlDecode(js, input), Error, error); |
| 391 | } |
| 392 | } // namespace workerd::api |