Skip to content
File

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

16.6 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 "digest.h"
6 
7#include "impl.h"
8#include "util.h"
9 
10#include <workerd/api/crypto/crypto.h>
11#include <workerd/io/io-context.h>
12 
13#include <openssl/hmac.h>
14#include <openssl/mem.h>
15 
16namespace workerd::api {
17namespace {
18 
19class HmacKey final: public CryptoKey::Impl {
20 public:
21 explicit HmacKey(kj::Array<kj::byte> keyData,
22 CryptoKey::HmacKeyAlgorithm keyAlgorithm,
23 bool extractable,
24 CryptoKeyUsageSet usages)
25 : CryptoKey::Impl(extractable, usages),
26 keyData(kj::mv(keyData)),
27 keyAlgorithm(kj::mv(keyAlgorithm)) {}
28 
29 kj::StringPtr jsgGetMemoryName() const override {
30 return "HmacKey";
31 }
32 size_t jsgGetMemorySelfSize() const override {
33 return sizeof(HmacKey);
34 }
35 void jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const override {
36 tracker.trackFieldWithSize("keyData", keyData.size());
37 tracker.trackField("keyAlgorithm", keyAlgorithm);
38 }
39 
40 private:
41 jsg::JsArrayBuffer sign(jsg::Lock& js,
42 SubtleCrypto::SignAlgorithm&& algorithm,
43 kj::ArrayPtr<const kj::byte> data) const override {
44 return computeHmac(js, kj::mv(algorithm), data);
45 }
46 
47 bool verify(jsg::Lock& js,
48 SubtleCrypto::SignAlgorithm&& algorithm,
49 kj::ArrayPtr<const kj::byte> signature,
50 kj::ArrayPtr<const kj::byte> data) const override {
51 auto messageDigest = computeHmac(js, kj::mv(algorithm), data);
52 return messageDigest.size() == signature.size() &&
53 CRYPTO_memcmp(messageDigest.asArrayPtr().begin(), signature.begin(), signature.size()) == 0;
54 }
55 
56 jsg::JsArrayBuffer computeHmac(jsg::Lock& js,
57 SubtleCrypto::SignAlgorithm&& algorithm,
58 kj::ArrayPtr<const kj::byte> data) const {
59 // For HMAC, the hash is specified when creating the key, not at call time.
60 auto type = lookupDigestAlgorithm(keyAlgorithm.hash.name).second;
61 auto buf = jsg::JsArrayBuffer::create(js, EVP_MD_size(type));
62 
63 uint messageDigestSize = 0;
64 auto ptr = HMAC(type, keyData.begin(), keyData.size(), data.begin(), data.size(),
65 buf.asArrayPtr().begin(), &messageDigestSize);
66 JSG_REQUIRE(ptr != nullptr, DOMOperationError, "HMAC computation failed.");
67 
68 KJ_ASSERT(messageDigestSize == buf.size());
69 return buf;
70 }
71 
72 SubtleCrypto::ExportKeyData exportKey(jsg::Lock& js, kj::StringPtr format) const override {
73 JSG_REQUIRE(format == "raw" || format == "jwk", DOMNotSupportedError,
74 "Unimplemented key export format \"", format, "\".");
75 
76 if (format == "jwk") {
77 // This assert enforces that the slice logic to fill in `.alg` below is safe.
78 JSG_REQUIRE(keyAlgorithm.hash.name.first(4) == "SHA-"_kj, DOMNotSupportedError,
79 "Unimplemented JWK key export format for key algorithm \"", keyAlgorithm.hash.name,
80 "\".");
81 
82 SubtleCrypto::JsonWebKey jwk;
83 jwk.kty = kj::str("oct");
84 jwk.k = fastEncodeBase64Url(keyData);
85 jwk.alg = kj::str("HS", keyAlgorithm.hash.name.slice(4));
86 jwk.key_ops = getUsages().map([](auto usage) { return kj::str(usage.name()); });
87 // I don't know why the spec says:
88 // Set the ext attribute of jwk to equal the [[extractable]] internal slot of key.
89 // Earlier in the normative part of the spec it says:
90 // 6. If the [[extractable]] internal slot of key is false, then throw an InvalidAccessError.
91 // 7. Let result be the result of performing the export key operation specified by the
92 // [[algorithm]] internal slot of key using key and format.
93 // So there's not really any other value that `ext` can have here since this code is the
94 // implementation of step 7 (see SubtleCrypto::exportKey where you can confirm it is
95 // enforcing step 6).
96 jwk.ext = true;
97 
98 return jwk;
99 }
100 
101 return jsg::JsArrayBuffer::create(js, keyData).addRef(js);
102 }
103 
104 kj::StringPtr getAlgorithmName() const override {
105 return "HMAC";
106 }
107 CryptoKey::AlgorithmVariant getAlgorithm(jsg::Lock& js) const override {
108 return keyAlgorithm;
109 }
110 
111 bool equals(const CryptoKey::Impl& other) const override final {
112 return this == &other || (other.getType() == "secret"_kj && other.equals(keyData));
113 }
114 
115 bool equals(const kj::Array<kj::byte>& other) const override final {
116 return keyData.size() == other.size() &&
117 CRYPTO_memcmp(keyData.begin(), other.begin(), keyData.size()) == 0;
118 }
119 
120 ZeroOnFree keyData;
121 CryptoKey::HmacKeyAlgorithm keyAlgorithm;
122};
123 
124void zeroOutTrailingKeyBits(kj::Array<kj::byte>& keyDataArray, int keyBitLength) {
125 // We zero out the least-significant bits of the last byte, matching Chrome's
126 // big-endian behavior when generating keys.
127 int arrayBitLength = keyDataArray.size() * 8;
128 KJ_REQUIRE(arrayBitLength >= keyBitLength);
129 KJ_REQUIRE(arrayBitLength - 8 < keyBitLength);
130 
131 if (auto difference = keyBitLength - (arrayBitLength - 8); difference > 0) {
132 keyDataArray.back() &= 0xff00 >> difference;
133 }
134}
135 
136kj::Own<HMAC_CTX> initHmacContext(
137 jsg::Lock& js, kj::StringPtr algorithm, HmacContext::KeyData& key) {
138 static constexpr auto handle = [](kj::StringPtr algorithm, kj::ArrayPtr<kj::byte> key) {
139 ClearErrorOnReturn clearErrorOnReturn;
140 JSG_REQUIRE(key.size() <= INT_MAX, RangeError, "key is too long");
141 const EVP_MD* md = EVP_get_digestbyname(algorithm.begin());
142 JSG_REQUIRE(md != nullptr, Error, "Digest method not supported");
143 static constexpr auto mt = ""_kjc;
144 auto hmac_ctx = OSSL_NEW(HMAC_CTX);
145 JSG_REQUIRE(HMAC_Init_ex(hmac_ctx.get(), key.size() ? key.asChars().begin() : mt.begin(),
146 key.size(), md, nullptr),
147 Error, "Failed to initalize HMAC");
148 return kj::mv(hmac_ctx);
149 };
150 
151 KJ_SWITCH_ONEOF(key) {
152 KJ_CASE_ONEOF(buf, kj::ArrayPtr<kj::byte>) {
153 return handle(algorithm, buf);
154 }
155 KJ_CASE_ONEOF(key2, CryptoKey::Impl*) {
156 // We already checked that the key is a secret key, so the following should succeed.
157 SubtleCrypto::ExportKeyData keyData = key2->exportKey(js, "raw"_kj);
158 
159 KJ_SWITCH_ONEOF(keyData) {
160 KJ_CASE_ONEOF(key_data, jsg::JsRef<jsg::JsArrayBuffer>) {
161 auto buf = key_data.getHandle(js);
162 return handle(algorithm, buf.asArrayPtr());
163 }
164 KJ_CASE_ONEOF(jwk, SubtleCrypto::JsonWebKey) {
165 KJ_UNREACHABLE;
166 }
167 }
168 }
169 }
170 KJ_UNREACHABLE;
171}
172} // namespace
173 
174HmacContext::HmacContext(jsg::Lock& js, kj::StringPtr algorithm, KeyData key)
175 : state(initHmacContext(js, algorithm, key)) {}
176 
177void HmacContext::update(kj::ArrayPtr<kj::byte> data) {
178 KJ_SWITCH_ONEOF(state) {
179 KJ_CASE_ONEOF(ctx, kj::Own<HMAC_CTX>) {
180 JSG_REQUIRE(data.size() <= INT_MAX, RangeError, "data is too long");
181 KJ_ASSERT(HMAC_Update(ctx.get(), data.begin(), data.size()) == 1);
182 }
183 KJ_CASE_ONEOF(digest, jsg::JsRef<jsg::JsUint8Array>) {
184 JSG_FAIL_REQUIRE(DOMOperationError, "HMAC context has already been finalized.");
185 }
186 }
187}
188 
189jsg::JsUint8Array HmacContext::digest(jsg::Lock& js) {
190 KJ_SWITCH_ONEOF(state) {
191 KJ_CASE_ONEOF(ctx, kj::Own<HMAC_CTX>) {
192 auto theCtx = kj::mv(ctx);
193 unsigned len;
194 auto buf = jsg::JsUint8Array::create(js, HMAC_size(theCtx.get()));
195 JSG_REQUIRE(HMAC_Final(theCtx.get(), buf.asArrayPtr().begin(), &len), Error,
196 "Failed to finalize HMAC");
197 KJ_ASSERT(len == buf.size());
198 state = buf.addRef(js);
199 return buf;
200 }
201 KJ_CASE_ONEOF(digest, jsg::JsRef<jsg::JsUint8Array>) {
202 auto cached = digest.getHandle(js);
203 return jsg::JsUint8Array::create(js, cached.asArrayPtr());
204 }
205 KJ_UNREACHABLE;
206 }
207 return jsg::JsUint8Array::create(js, 0);
208}
209 
210size_t HmacContext::size() const {
211 KJ_SWITCH_ONEOF(state) {
212 KJ_CASE_ONEOF(ctx, kj::Own<HMAC_CTX>) {
213 return 0;
214 }
215 KJ_CASE_ONEOF(digest, jsg::JsRef<jsg::JsUint8Array>) {
216 // JsRef doesn't expose size() without a lock. Return 0 for memory tracking;
217 // the JsRef itself is tracked separately by the GC visitor.
218 return 0;
219 }
220 }
221 KJ_UNREACHABLE;
222}
223 
224kj::OneOf<jsg::Ref<CryptoKey>, CryptoKeyPair> CryptoKey::Impl::generateHmac(jsg::Lock& js,
225 kj::StringPtr normalizedName,
226 SubtleCrypto::GenerateKeyAlgorithm&& algorithm,
227 bool extractable,
228 kj::ArrayPtr<const kj::String> keyUsages) {
229 KJ_REQUIRE(normalizedName == "HMAC");
230 kj::StringPtr hash = api::getAlgorithmName(
231 JSG_REQUIRE_NONNULL(algorithm.hash, TypeError, "Missing field \"hash\" in \"algorithm\"."));
232 
233 auto [normalizedHashName, hashEvpMd] = lookupDigestAlgorithm(hash);
234 auto usages = CryptoKeyUsageSet::validate(normalizedName, CryptoKeyUsageSet::Context::generate,
235 keyUsages, CryptoKeyUsageSet::sign() | CryptoKeyUsageSet::verify());
236 
237 // If the user requested a specific HMAC key length, honor it.
238 auto length = algorithm.length.orDefault(EVP_MD_block_size(hashEvpMd) * 8);
239 JSG_REQUIRE(length > 0, DOMOperationError,
240 "HMAC key length must be a non-zero unsigned long integer (requested ", length, ").");
241 
242 auto keyDataArray = kj::heapArray<kj::byte>(
243 integerCeilDivision<std::make_unsigned_t<decltype(length)>>(length, 8u));
244 IoContext::current().getEntropySource().generate(keyDataArray);
245 zeroOutTrailingKeyBits(keyDataArray, length);
246 
247 auto keyAlgorithm = CryptoKey::HmacKeyAlgorithm{
248 normalizedName, {normalizedHashName}, static_cast<uint16_t>(length)};
249 
250 return js.alloc<CryptoKey>(
251 kj::heap<HmacKey>(kj::mv(keyDataArray), kj::mv(keyAlgorithm), extractable, usages));
252}
253 
254kj::Own<CryptoKey::Impl> CryptoKey::Impl::importHmac(jsg::Lock& js,
255 kj::StringPtr normalizedName,
256 kj::StringPtr format,
257 SubtleCrypto::ImportKeyData keyData,
258 SubtleCrypto::ImportKeyAlgorithm&& algorithm,
259 bool extractable,
260 kj::ArrayPtr<const kj::String> keyUsages) {
261 auto usages =
262 CryptoKeyUsageSet::validate(normalizedName, CryptoKeyUsageSet::Context::importSecret,
263 keyUsages, CryptoKeyUsageSet::sign() | CryptoKeyUsageSet::verify());
264 
265 kj::Array<kj::byte> keyDataArray;
266 kj::StringPtr hash = api::getAlgorithmName(
267 JSG_REQUIRE_NONNULL(algorithm.hash, TypeError, "Missing field \"hash\" in \"algorithm\"."));
268 
269 if (format == "raw") {
270 // NOTE: Checked in SubtleCrypto::importKey().
271 keyDataArray = kj::mv(keyData.get<kj::Array<kj::byte>>());
272 } else if (format == "jwk") {
273 auto& keyDataJwk = keyData.get<SubtleCrypto::JsonWebKey>();
274 JSG_REQUIRE(keyDataJwk.kty == "oct", DOMDataError,
275 "HMAC \"jwk\" key import requires a JSON Web Key with Key Type parameter "
276 "(\"kty\") equal to \"oct\" (encountered \"",
277 keyDataJwk.kty, "\").");
278 // https://www.rfc-editor.org/rfc/rfc7518.txt Section 6.1
279 keyDataArray = UNWRAP_JWK_BIGNUM(kj::mv(keyDataJwk.k), DOMDataError,
280 "HMAC \"jwk\" key import requires a base64Url encoding of the key");
281 
282 KJ_IF_SOME(alg, keyDataJwk.alg) {
283 if (hash.startsWith("SHA-")) {
284 auto expectedAlg = kj::str("HS", hash.slice(4));
285 JSG_REQUIRE(alg == expectedAlg, DOMDataError,
286 "HMAC \"jwk\" key import specifies \"alg\" that is incompatible with the hash name "
287 "(encountered \"",
288 alg, "\", expected \"", expectedAlg, "\").");
289 } else {
290 // TODO(conform): Spec says this for non-SHA hashes:
291 // > Perform any key import steps defined by other applicable specifications, passing
292 // > format, jwk and hash and obtaining hash.
293 // What other hashes should be supported (if any)? For example, technically we support MD5
294 // below in `lookupDigestAlgorithm` for "raw" keys...
295 JSG_FAIL_REQUIRE(
296 DOMNotSupportedError, "Unrecognized or unimplemented hash algorithm requested", alg);
297 }
298 }
299 } else {
300 JSG_FAIL_REQUIRE(DOMNotSupportedError, "Unrecognized key import format \"", format, "\".");
301 }
302 
303 // The spec claims the length of an HMAC key can be up to 7 bits less than the bit length of the
304 // raw key data passed in to `importKey()`. Since the raw key data comes in bytes, that means that
305 // HMAC keys can have non-multiple-of-8 bit lengths. I dutifully implemented this check, but it
306 // seems rather pointless: the OpenSSL HMAC interface only supports key lengths in bytes ...
307 auto keySize = keyDataArray.size() * 8;
308 auto length = algorithm.length.orDefault(keySize);
309 if (length == 0 || length > keySize || length <= keySize - 8) {
310 JSG_FAIL_REQUIRE(DOMDataError, "Imported HMAC key length (", length,
311 ") must be a non-zero value up to 7 bits less than, "
312 "and no greater than, the bit length of the raw key data (",
313 keySize, ").");
314 }
315 
316 // Not required by the spec, but zeroing out the unused bits makes me feel better.
317 zeroOutTrailingKeyBits(keyDataArray, length);
318 
319 auto normalizedHashName = lookupDigestAlgorithm(hash).first;
320 auto keyAlgorithm = CryptoKey::HmacKeyAlgorithm{
321 normalizedName, {normalizedHashName}, static_cast<uint16_t>(length)};
322 return kj::heap<HmacKey>(kj::mv(keyDataArray), kj::mv(keyAlgorithm), extractable, usages);
323}
324 
325// ======================================================================================
326 
327namespace {
328kj::Own<EVP_MD_CTX> initDigestCtx(kj::StringPtr algorithm) {
329 const EVP_MD* md = EVP_get_digestbyname(algorithm.begin());
330 JSG_REQUIRE(md != nullptr, Error, "Digest method not supported");
331 auto ctx = OSSL_NEW(EVP_MD_CTX);
332 OSSLCALL(EVP_DigestInit(ctx.get(), md));
333 return kj::mv(ctx);
334}
335 
336void checkXofLen(EVP_MD_CTX* ctx, kj::Maybe<uint32_t>& maybeXof) {
337 KJ_IF_SOME(xof, maybeXof) {
338 auto md = EVP_MD_CTX_md(ctx);
339 if (xof != EVP_MD_size(md)) {
340 JSG_REQUIRE((EVP_MD_flags(md) & EVP_MD_FLAG_XOF) != 0, Error, "invalid digest size");
341 }
342 }
343}
344} // namespace
345 
346HashContext::HashContext(kj::OneOf<kj::Own<EVP_MD_CTX>, jsg::JsRef<jsg::JsUint8Array>> state,
347 kj::Maybe<uint32_t> maybeXof)
348 : state(kj::mv(state)),
349 maybeXof(kj::mv(maybeXof)) {
350 checkXofLen(this->state.get<kj::Own<EVP_MD_CTX>>().get(), this->maybeXof);
351}
352 
353HashContext::HashContext(kj::StringPtr algorithm, kj::Maybe<uint32_t> maybeXof)
354 : HashContext(initDigestCtx(algorithm), kj::mv(maybeXof)) {}
355 
356void HashContext::update(kj::ArrayPtr<kj::byte> data) {
357 KJ_SWITCH_ONEOF(state) {
358 KJ_CASE_ONEOF(ctx, kj::Own<EVP_MD_CTX>) {
359 JSG_REQUIRE(data.size() <= INT_MAX, RangeError, "data is too long");
360 OSSLCALL(EVP_DigestUpdate(ctx.get(), data.begin(), data.size()));
361 }
362 KJ_CASE_ONEOF(digest, jsg::JsRef<jsg::JsUint8Array>) {
363 JSG_FAIL_REQUIRE(DOMOperationError, "Hash context has already been finalized.");
364 }
365 }
366}
367 
368jsg::JsUint8Array HashContext::digest(jsg::Lock& js) {
369 KJ_SWITCH_ONEOF(state) {
370 KJ_CASE_ONEOF(ctx, kj::Own<EVP_MD_CTX>) {
371 auto theCtx = kj::mv(ctx);
372 uint32_t len = EVP_MD_size(EVP_MD_CTX_md(theCtx.get()));
373 KJ_IF_SOME(xof, maybeXof) {
374 if (xof == len) {
375 auto buf = jsg::JsUint8Array::create(js, len);
376 JSG_REQUIRE(EVP_DigestFinal_ex(theCtx.get(), buf.asArrayPtr().begin(), &len) == 1, Error,
377 "Failed to compute hash digest");
378 KJ_ASSERT(len == buf.size());
379 state = buf.addRef(js);
380 return buf;
381 }
382 
383 auto buf = jsg::JsUint8Array::create(js, xof);
384 JSG_REQUIRE(EVP_DigestFinalXOF(theCtx.get(), buf.asArrayPtr().begin(), xof) == 1, Error,
385 "Failed to compute XOF hash digest");
386 state = buf.addRef(js);
387 return buf;
388 }
389 
390 auto buf = jsg::JsUint8Array::create(js, len);
391 JSG_REQUIRE(EVP_DigestFinal_ex(theCtx.get(), buf.asArrayPtr().begin(), &len) == 1, Error,
392 "Failed to compute hash digest");
393 KJ_ASSERT(len == buf.size());
394 state = buf.addRef(js);
395 return buf;
396 }
397 KJ_CASE_ONEOF(digest, jsg::JsRef<jsg::JsUint8Array>) {
398 auto cached = digest.getHandle(js);
399 return jsg::JsUint8Array::create(js, cached.asArrayPtr());
400 }
401 KJ_UNREACHABLE
402 }
403 
404 return jsg::JsUint8Array::create(js, 0);
405}
406 
407HashContext HashContext::clone(jsg::Lock& js, kj::Maybe<uint32_t> xofLen) {
408 KJ_SWITCH_ONEOF(state) {
409 KJ_CASE_ONEOF(ctx, kj::Own<EVP_MD_CTX>) {
410 auto newCtx = OSSL_NEW(EVP_MD_CTX);
411 OSSLCALL(EVP_MD_CTX_copy_ex(newCtx, ctx.get()));
412 return HashContext(kj::mv(newCtx), kj::mv(xofLen));
413 }
414 KJ_CASE_ONEOF(digest, jsg::JsRef<jsg::JsUint8Array>) {
415 JSG_FAIL_REQUIRE(DOMOperationError, "Hash context has already been finalized.");
416 }
417 }
418 KJ_UNREACHABLE;
419}
420 
421size_t HashContext::size() const {
422 KJ_SWITCH_ONEOF(state) {
423 KJ_CASE_ONEOF(ctx, kj::Own<EVP_MD_CTX>) {
424 return 0;
425 }
426 KJ_CASE_ONEOF(digest, jsg::JsRef<jsg::JsUint8Array>) {
427 // JsRef doesn't expose size() without a lock. Return 0 for memory tracking;
428 // the JsRef itself is tracked separately by the GC visitor.
429 return 0;
430 }
431 }
432 KJ_UNREACHABLE;
433}
434 
435} // namespace workerd::api