File
Blob: src/workerd/api/crypto/dh.c++
| 1 | #include "dh.h" |
| 2 | |
| 3 | #include "impl.h" |
| 4 | |
| 5 | #include <workerd/io/io-context.h> |
| 6 | |
| 7 | #include <ncrypto.h> |
| 8 | #include <openssl/bn.h> |
| 9 | #include <openssl/dh.h> |
| 10 | |
| 11 | #include <kj/one-of.h> |
| 12 | #include <kj/string.h> |
| 13 | |
| 14 | namespace workerd::api { |
| 15 | |
| 16 | namespace { |
| 17 | |
| 18 | // Maximum DH prime size, adapted from BoringSSL. This is already defined in more recent versions. |
| 19 | #ifndef OPENSSL_DH_MAX_MODULUS_BITS |
| 20 | #define OPENSSL_DH_MAX_MODULUS_BITS 10000 |
| 21 | #endif |
| 22 | |
| 23 | // Returns a function that can be used to create an instance of a standardized |
| 24 | // Diffie-Hellman group. |
| 25 | BIGNUM* (*findDiffieHellmanGroup(const char* name))(BIGNUM*) { |
| 26 | #if _WIN32 |
| 27 | #define V(n, p) \ |
| 28 | if (_strnicmp(name, n, 7) == 0) { \ |
| 29 | return p; \ |
| 30 | } |
| 31 | #else |
| 32 | #define V(n, p) \ |
| 33 | if (strncasecmp(name, n, 7) == 0) { \ |
| 34 | return p; \ |
| 35 | } |
| 36 | #endif |
| 37 | // Only the following primes are supported based on security concerns about the smaller prime |
| 38 | // groups (https://www.rfc-editor.org/rfc/rfc8247#section-2.4). |
| 39 | V("modp14", BN_get_rfc3526_prime_2048); |
| 40 | V("modp15", BN_get_rfc3526_prime_3072); |
| 41 | V("modp16", BN_get_rfc3526_prime_4096); |
| 42 | V("modp17", BN_get_rfc3526_prime_6144); |
| 43 | V("modp18", BN_get_rfc3526_prime_8192); |
| 44 | #undef V |
| 45 | |
| 46 | return nullptr; |
| 47 | } |
| 48 | |
| 49 | kj::Own<DH> initDhGroup(kj::StringPtr name) { |
| 50 | auto group = findDiffieHellmanGroup(name.begin()); |
| 51 | JSG_REQUIRE(group != nullptr, Error, |
| 52 | "Failed to init DiffieHellmanGroup: invalid group. Only " |
| 53 | "groups {modp14, modp15, modp16, modp17, modp18} are supported."); |
| 54 | auto groupKey = group(nullptr); |
| 55 | KJ_ASSERT(groupKey != nullptr); |
| 56 | |
| 57 | const int kStandardizedGenerator = 2; |
| 58 | auto dh = OSSL_NEW(DH); |
| 59 | |
| 60 | // Note: We're deliberately not using kj::Own/OSSL_NEW() here as DH_set0_pqg() takes ownership |
| 61 | // of the key, so there is no need to free it if the operation succeeds. |
| 62 | UniqueBignum bn_g(BN_new(), &BN_clear_free); |
| 63 | if (!BN_set_word(bn_g.get(), kStandardizedGenerator) || |
| 64 | !DH_set0_pqg(dh, groupKey, nullptr, bn_g.get())) { |
| 65 | JSG_FAIL_REQUIRE(Error, "DiffieHellmanGroup init failed: could not set keys"); |
| 66 | } |
| 67 | bn_g.release(); |
| 68 | return kj::mv(dh); |
| 69 | } |
| 70 | |
| 71 | kj::Own<DH> initDh(kj::OneOf<kj::Array<kj::byte>, int>& sizeOrKey, |
| 72 | kj::OneOf<kj::Array<kj::byte>, int>& generator) { |
| 73 | KJ_SWITCH_ONEOF(sizeOrKey) { |
| 74 | KJ_CASE_ONEOF(size, int) { |
| 75 | KJ_SWITCH_ONEOF(generator) { |
| 76 | KJ_CASE_ONEOF(gen, int) { |
| 77 | // Generating a DH key with a reasonable size can be expensive. |
| 78 | // We will only allow it if there is an active IoContext so that |
| 79 | // we can enforce a timeout associate with the limit enforcer. |
| 80 | auto& ioContext = JSG_REQUIRE_NONNULL(IoContext::tryCurrent(), Error, |
| 81 | "DiffieHellman key generation requires an active request"); |
| 82 | |
| 83 | struct Status { |
| 84 | IoContext& context; |
| 85 | kj::Maybe<EventOutcome> status; |
| 86 | } status{.context = ioContext}; |
| 87 | |
| 88 | auto dh = OSSL_NEW(DH); |
| 89 | BN_GENCB cb; |
| 90 | cb.arg = &status; |
| 91 | // This callback is called many times during the key generation process. |
| 92 | // We use it because key generation is expensive and may run over the |
| 93 | // CPU limits for the request. As this method can itself contribute to |
| 94 | // running over the CPU limit, it is important to do as little as possible. |
| 95 | cb.callback = [](int a, int b, BN_GENCB* cb) -> int { |
| 96 | Status& status = *static_cast<Status*>(cb->arg); |
| 97 | KJ_IF_SOME(outcome, status.context.getLimitEnforcer().getLimitsExceeded()) { |
| 98 | status.status = outcome; |
| 99 | return 0; |
| 100 | } |
| 101 | return 1; |
| 102 | }; |
| 103 | // Operations on an "egregiously large" prime will throw with recent BoringSSL. |
| 104 | JSG_REQUIRE(size <= OPENSSL_DH_MAX_MODULUS_BITS, RangeError, |
| 105 | "DiffieHellman init failed: requested prime size too large"); |
| 106 | if (!DH_generate_parameters_ex(dh.get(), size, gen, &cb)) { |
| 107 | KJ_IF_SOME(outcome, status.status) { |
| 108 | if (outcome == EventOutcome::EXCEEDED_CPU) { |
| 109 | JSG_FAIL_REQUIRE( |
| 110 | Error, "DiffieHellman init failed: key generation exceeded CPU limit"); |
| 111 | } else if (outcome == EventOutcome::EXCEEDED_MEMORY) { |
| 112 | JSG_FAIL_REQUIRE( |
| 113 | Error, "DiffieHellman init failed: key generation exceeded memory limit"); |
| 114 | } |
| 115 | } |
| 116 | JSG_FAIL_REQUIRE(Error, "DiffieHellman init failed: could not generate parameters"); |
| 117 | } |
| 118 | // Boringssl throws on DH with g >= p or p | 2 since g can't be an element of p's |
| 119 | // multiplicative group in that case. |
| 120 | JSG_REQUIRE(BN_is_odd(DH_get0_p(dh)) && BN_ucmp(DH_get0_g(dh), DH_get0_p(dh)) < 0, Error, |
| 121 | "DiffieHellman init failed: Invalid DH prime generated"); |
| 122 | return kj::mv(dh); |
| 123 | } |
| 124 | KJ_CASE_ONEOF(gen, kj::Array<kj::byte>) { |
| 125 | // Node.js does not support generating Diffie-Hellman keys from an int prime |
| 126 | // and byte-array generator. This could change in the future. |
| 127 | JSG_FAIL_REQUIRE(Error, "DiffieHellman init failed: invalid parameters"); |
| 128 | } |
| 129 | } |
| 130 | } |
| 131 | KJ_CASE_ONEOF(key, kj::Array<kj::byte>) { |
| 132 | // Operations on an "egregiously large" prime will throw with BoringSSL. |
| 133 | JSG_REQUIRE(key.size() <= OPENSSL_DH_MAX_MODULUS_BITS / CHAR_BIT, RangeError, |
| 134 | "DiffieHellman init failed: key is too large"); |
| 135 | JSG_REQUIRE(key.size() > 0, Error, "DiffieHellman init failed: invalid key"); |
| 136 | auto dh = OSSL_NEW(DH); |
| 137 | |
| 138 | // We use a std::unique_ptr here instead of a kj::Own because DH_set0_pqg takes ownership |
| 139 | // and we need to be able to release ownership if the operation succeeds but want the |
| 140 | // BIGNUMs to be appropriately freed if the operations fail. |
| 141 | using UniqueBignum = std::unique_ptr<BIGNUM, void (*)(BIGNUM*)>; |
| 142 | UniqueBignum bn_g(nullptr, &BN_clear_free); |
| 143 | |
| 144 | KJ_SWITCH_ONEOF(generator) { |
| 145 | KJ_CASE_ONEOF(gen, int) { |
| 146 | JSG_REQUIRE(gen >= 2, RangeError, "DiffieHellman init failed: generator too small"); |
| 147 | bn_g.reset(BN_new()); |
| 148 | if (!BN_set_word(bn_g.get(), gen)) { |
| 149 | JSG_FAIL_REQUIRE(Error, "DiffieHellman init failed: could not set keys"); |
| 150 | } |
| 151 | } |
| 152 | KJ_CASE_ONEOF(gen, kj::Array<kj::byte>) { |
| 153 | JSG_REQUIRE(gen.size() <= OPENSSL_DH_MAX_MODULUS_BITS / CHAR_BIT, RangeError, |
| 154 | "DiffieHellman init failed: generator is too large"); |
| 155 | JSG_REQUIRE(gen.size() > 0, Error, "DiffieHellman init failed: invalid generator"); |
| 156 | |
| 157 | bn_g.reset(toBignumUnowned(gen)); |
| 158 | if (BN_is_zero(bn_g.get()) || BN_is_one(bn_g.get())) { |
| 159 | JSG_FAIL_REQUIRE(Error, "DiffieHellman init failed: invalid generator"); |
| 160 | } |
| 161 | } |
| 162 | } |
| 163 | UniqueBignum bn_p(toBignumUnowned(key), &BN_clear_free); |
| 164 | JSG_REQUIRE(bn_p != nullptr, Error, |
| 165 | "DiffieHellman init failed: could not convert key representation"); |
| 166 | // Boringssl throws on DH with g >= p or p | 2 since g can't be an element of p's |
| 167 | // multiplicative group in that case. |
| 168 | JSG_REQUIRE(BN_is_odd(bn_p.get()) && BN_ucmp(bn_g.get(), bn_p.get()) < 0, Error, |
| 169 | "DiffieHellman init failed: Invalid DH prime generated"); |
| 170 | JSG_REQUIRE(DH_set0_pqg(dh, bn_p.get(), nullptr, bn_g.get()), Error, |
| 171 | "DiffieHellman init failed: could not set keys"); |
| 172 | bn_g.release(); |
| 173 | bn_p.release(); |
| 174 | return kj::mv(dh); |
| 175 | } |
| 176 | } |
| 177 | KJ_UNREACHABLE; |
| 178 | } |
| 179 | |
| 180 | void zeroPadDiffieHellmanSecret(size_t remainder_size, unsigned char* data, size_t prime_size) { |
| 181 | // DH_size returns number of bytes in a prime number. |
| 182 | // DH_compute_key returns number of bytes in a remainder of exponent, which |
| 183 | // may have less bytes than a prime number. Therefore add 0-padding to the |
| 184 | // allocated buffer. |
| 185 | if (remainder_size != prime_size) { |
| 186 | KJ_ASSERT(remainder_size < prime_size); |
| 187 | const size_t padding = prime_size - remainder_size; |
| 188 | memmove(data + padding, data, remainder_size); |
| 189 | kj::arrayPtr(data, padding).fill(0); |
| 190 | } |
| 191 | } |
| 192 | } // namespace |
| 193 | |
| 194 | DiffieHellman::DiffieHellman(kj::StringPtr group): dh(initDhGroup(group)) {} |
| 195 | |
| 196 | DiffieHellman::DiffieHellman( |
| 197 | kj::OneOf<kj::Array<kj::byte>, int>& sizeOrKey, kj::OneOf<kj::Array<kj::byte>, int>& generator) |
| 198 | : dh(initDh(sizeOrKey, generator)) {} |
| 199 | |
| 200 | kj::Maybe<int> DiffieHellman::check() { |
| 201 | ClearErrorOnReturn clearErrorOnReturn; |
| 202 | int codes; |
| 203 | if (!DH_check(dh.get(), &codes)) { |
| 204 | return kj::none; |
| 205 | } |
| 206 | return codes; |
| 207 | } |
| 208 | |
| 209 | void DiffieHellman::setPrivateKey(kj::ArrayPtr<kj::byte> key) { |
| 210 | auto bn = toBignumOwned(key); |
| 211 | OSSLCALL(DH_set0_key(dh, nullptr, bn.get())); |
| 212 | bn.release(); |
| 213 | } |
| 214 | |
| 215 | void DiffieHellman::setPublicKey(kj::ArrayPtr<kj::byte> key) { |
| 216 | auto bn = toBignumOwned(key); |
| 217 | |
| 218 | int checkResult; |
| 219 | JSG_REQUIRE(DH_check_pub_key(dh, bn.get(), &checkResult), Error, |
| 220 | "DiffieHellman setPublicKey() failed: could not validate public key"); |
| 221 | if (checkResult) { |
| 222 | JSG_REQUIRE(!(checkResult & DH_CHECK_PUBKEY_TOO_SMALL), RangeError, |
| 223 | "DiffieHellman setPublicKey() failed: key is too small"); |
| 224 | JSG_REQUIRE(!(checkResult & DH_CHECK_PUBKEY_TOO_LARGE), RangeError, |
| 225 | "DiffieHellman setPublicKey() failed: key is too large"); |
| 226 | JSG_FAIL_REQUIRE(Error, "DiffieHellman setPublicKey() failed: invalid public key"); |
| 227 | } |
| 228 | |
| 229 | OSSLCALL(DH_set0_key(dh, bn.get(), nullptr)); |
| 230 | bn.release(); |
| 231 | } |
| 232 | |
| 233 | jsg::JsUint8Array DiffieHellman::getPublicKey(jsg::Lock& js) { |
| 234 | const BIGNUM* pub_key = DH_get0_pub_key(dh); |
| 235 | JSG_REQUIRE(pub_key != nullptr, Error, "No public key"); |
| 236 | return JSG_REQUIRE_NONNULL( |
| 237 | bignumToArrayPadded(js, *pub_key), Error, "Error while retrieving DiffieHellman public key"); |
| 238 | } |
| 239 | |
| 240 | jsg::JsUint8Array DiffieHellman::getPrivateKey(jsg::Lock& js) { |
| 241 | const BIGNUM* priv_key = DH_get0_priv_key(dh); |
| 242 | JSG_REQUIRE(priv_key != nullptr, Error, "No private key"); |
| 243 | return JSG_REQUIRE_NONNULL(bignumToArrayPadded(js, *priv_key), Error, |
| 244 | "Error while retrieving DiffieHellman private key"); |
| 245 | } |
| 246 | |
| 247 | jsg::JsUint8Array DiffieHellman::getGenerator(jsg::Lock& js) { |
| 248 | const BIGNUM* g = DH_get0_g(dh); |
| 249 | JSG_REQUIRE(g != nullptr, Error, "No DiffieHellman generator"); |
| 250 | return JSG_REQUIRE_NONNULL( |
| 251 | bignumToArrayPadded(js, *g), Error, "Error while retrieving DiffieHellman generator"); |
| 252 | } |
| 253 | |
| 254 | jsg::JsUint8Array DiffieHellman::getPrime(jsg::Lock& js) { |
| 255 | const BIGNUM* p = DH_get0_p(dh); |
| 256 | JSG_REQUIRE(p != nullptr, Error, "No DiffieHellman prime"); |
| 257 | return JSG_REQUIRE_NONNULL( |
| 258 | bignumToArrayPadded(js, *p), Error, "Error while retrieving DiffieHellman prime"); |
| 259 | } |
| 260 | |
| 261 | jsg::JsUint8Array DiffieHellman::computeSecret(jsg::Lock& js, kj::ArrayPtr<kj::byte> key) { |
| 262 | JSG_REQUIRE(key.size() <= INT32_MAX, RangeError, |
| 263 | "DiffieHellman computeSecret() failed: key is too large"); |
| 264 | JSG_REQUIRE(key.size() > 0, Error, "DiffieHellman computeSecret() failed: invalid key"); |
| 265 | |
| 266 | ClearErrorOnReturn clear_error_on_return; |
| 267 | auto k = JSG_REQUIRE_NONNULL( |
| 268 | toBignum(key), Error, "Error getting key while computing DiffieHellman secret"); |
| 269 | |
| 270 | // Validate the peer's public key before computing the shared secret. |
| 271 | int checkResult; |
| 272 | JSG_REQUIRE(DH_check_pub_key(dh, k, &checkResult), Error, |
| 273 | "DiffieHellman computeSecret() failed: could not validate peer public key"); |
| 274 | if (checkResult) { |
| 275 | JSG_REQUIRE(!(checkResult & DH_CHECK_PUBKEY_TOO_SMALL), RangeError, |
| 276 | "DiffieHellman computeSecret() failed: Supplied key is too small"); |
| 277 | JSG_REQUIRE(!(checkResult & DH_CHECK_PUBKEY_TOO_LARGE), RangeError, |
| 278 | "DiffieHellman computeSecret() failed: Supplied key is too large"); |
| 279 | JSG_FAIL_REQUIRE(Error, "DiffieHellman computeSecret() failed: invalid peer public key"); |
| 280 | } |
| 281 | |
| 282 | size_t prime_size = DH_size(dh); |
| 283 | auto buf = jsg::JsUint8Array::create(js, prime_size); |
| 284 | |
| 285 | int size = DH_compute_key(buf.asArrayPtr().begin(), k.get(), dh); |
| 286 | JSG_REQUIRE( |
| 287 | size != -1, Error, "DiffieHellman computeSecret() failed: error computing shared secret"); |
| 288 | |
| 289 | KJ_ASSERT(size >= 0); |
| 290 | zeroPadDiffieHellmanSecret(size, buf.asArrayPtr().begin(), prime_size); |
| 291 | return buf; |
| 292 | } |
| 293 | |
| 294 | jsg::JsUint8Array DiffieHellman::generateKeys(jsg::Lock& js) { |
| 295 | ClearErrorOnReturn clear_error_on_return; |
| 296 | OSSLCALL(DH_generate_key(dh)); |
| 297 | const BIGNUM* pub_key = DH_get0_pub_key(dh); |
| 298 | return JSG_REQUIRE_NONNULL( |
| 299 | bignumToArrayPadded(js, *pub_key), Error, "Error while generating DiffieHellman keys"); |
| 300 | } |
| 301 | |
| 302 | } // namespace workerd::api |