Skip to content
File

Blob: src/workerd/api/crypto/impl.c++

14.0 KB
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 
24namespace workerd::api {
25namespace {
26kj::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 
65const SslArrayDisposer SslArrayDisposer::INSTANCE;
66 
67void 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.
77void 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 
133kj::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 
173kj::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 
193kj::String internalDescribeOpensslErrors() {
194 return errorsToString(consumeAllOpensslErrors().releaseAsArray(), "."_kj);
195}
196 
197std::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 
217kj::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 
226bool 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 
230kj::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 
244ZeroOnFree::~ZeroOnFree() noexcept(false) {
245 OPENSSL_cleanse(inner.begin(), inner.size());
246}
247 
248void 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 
257kj::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 
263BIGNUM* 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 
269UniqueBignum toBignumOwned(kj::ArrayPtr<const kj::byte> data) {
270 return UniqueBignum(toBignumUnowned(data), &BN_clear_free);
271}
272 
273kj::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 
279kj::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 
285kj::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 
293kj::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 
299kj::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 
308kj::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 
317kj::Own<BIGNUM> newBignum() {
318 return kj::Own<BIGNUM>(BN_new(), workerd::api::SslDisposer<BIGNUM, &BIGNUM_free>::INSTANCE);
319}
320 
321void CryptoKey::visitForMemoryInfo(jsg::MemoryTracker& tracker) const {
322 tracker.trackField("impl", impl);
323}
324 
325bool 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 
347kj::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 
367kj::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 
377kj::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 
388jsg::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