Skip to content
File

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

28.7 KB
1#include "x509.h"
2 
3#include "impl.h"
4 
5#include <openssl/bio.h>
6#include <openssl/pem.h>
7#include <openssl/x509v3.h>
8 
9KJ_DECLARE_NON_POLYMORPHIC(STACK_OF(ASN1_OBJECT));
10 
11namespace workerd::api {
12 
13namespace {
14 
15static constexpr int kX509NameFlagsMultiline = ASN1_STRFLGS_ESC_2253 | ASN1_STRFLGS_ESC_CTRL |
16 ASN1_STRFLGS_UTF8_CONVERT | XN_FLAG_SEP_MULTILINE | XN_FLAG_FN_SN;
17 
18static constexpr int kX509NameFlagsRFC2253WithinUtf8JSON =
19 XN_FLAG_RFC2253 & ~ASN1_STRFLGS_ESC_MSB & ~ASN1_STRFLGS_ESC_CTRL;
20 
21kj::Maybe<kj::Own<BIO>> newBio() {
22 auto ptr = BIO_new(BIO_s_mem());
23 if (ptr == nullptr) return kj::none;
24 return kj::disposeWith<BIO_free_all>(ptr);
25}
26 
27kj::Maybe<kj::Own<BIO>> loadBio(kj::ArrayPtr<const kj::byte> raw) {
28 static constexpr int32_t kMaxSize = kj::maxValue;
29 if (raw.size() > kMaxSize) return kj::none;
30 KJ_IF_SOME(bio, newBio()) {
31 int written = BIO_write(bio.get(), raw.begin(), raw.size());
32 if (written != raw.size()) return kj::none;
33 return kj::mv(bio);
34 }
35 return kj::none;
36}
37 
38int NoPasswordCallback(char* buf, int size, int rwflag, void* u) {
39 return 0;
40}
41 
42kj::String toString(BIO* bio) {
43 BUF_MEM* mem;
44 BIO_get_mem_ptr(bio, &mem);
45 auto result = kj::heapArray<char>(mem->length + 1);
46 kj::ArrayPtr<char> data(mem->data, mem->length);
47 result.first(data.size()).copyFrom(data);
48 result[result.size() - 1] = '\0'; // NUL-terminate.
49 return kj::String(kj::mv(result));
50}
51 
52bool isSafeAltName(const char* name, size_t length, bool utf8) {
53 for (size_t i = 0; i < length; i++) {
54 char c = name[i];
55 switch (c) {
56 case '"':
57 case '\\':
58 // These mess with encoding rules.
59 // Fall through.
60 case ',':
61 // Commas make it impossible to split the list of subject alternative
62 // names unambiguously, which is why we have to escape.
63 // Fall through.
64 case '\'':
65 // Single quotes are unlikely to appear in any legitimate values, but they
66 // could be used to make a value look like it was escaped (i.e., enclosed
67 // in single/double quotes).
68 return false;
69 default:
70 if (utf8) {
71 // In UTF8 strings, we require escaping for any ASCII control character,
72 // but NOT for non-ASCII characters. Note that all bytes of any code
73 // point that consists of more than a single byte have their MSB set.
74 if (static_cast<unsigned char>(c) < ' ' || c == '\x7f') {
75 return false;
76 }
77 } else {
78 // Check if the char is a control character or non-ASCII character. Note
79 // that char may or may not be a signed type. Regardless, non-ASCII
80 // values will always be outside of this range.
81 if (c < ' ' || c > '~') {
82 return false;
83 }
84 }
85 }
86 }
87 return true;
88}
89 
90void printAltName(BIO* out, const char* name, size_t length, bool utf8, const char* safe_prefix) {
91 if (isSafeAltName(name, length, utf8)) {
92 // For backward-compatibility, append "safe" names without any
93 // modifications.
94 if (safe_prefix != nullptr) {
95 BIO_printf(out, "%s:", safe_prefix);
96 }
97 BIO_write(out, name, length);
98 } else {
99 // If a name is not "safe", we cannot embed it without special
100 // encoding. This does not usually happen, but we don't want to hide
101 // it from the user either. We use JSON compatible escaping here.
102 BIO_write(out, "\"", 1);
103 if (safe_prefix != nullptr) {
104 BIO_printf(out, "%s:", safe_prefix);
105 }
106 for (size_t j = 0; j < length; j++) {
107 char c = static_cast<char>(name[j]);
108 if (c == '\\') {
109 BIO_write(out, "\\\\", 2);
110 } else if (c == '"') {
111 BIO_write(out, "\\\"", 2);
112 } else if ((c >= ' ' && c != ',' && c <= '~') || (utf8 && (c & 0x80))) {
113 // Note that the above condition explicitly excludes commas, which means
114 // that those are encoded as Unicode escape sequences in the "else"
115 // block. That is not strictly necessary, and Node.js itself would parse
116 // it correctly either way. We only do this to account for third-party
117 // code that might be splitting the string at commas (as Node.js itself
118 // used to do).
119 BIO_write(out, &c, 1);
120 } else {
121 // Control character or non-ASCII character. We treat everything as
122 // Latin-1, which corresponds to the first 255 Unicode code points.
123 const char hex[] = "0123456789abcdef";
124 char u[] = {'\\', 'u', '0', '0', hex[(c & 0xf0) >> 4], hex[c & 0x0f]};
125 BIO_write(out, u, sizeof(u));
126 }
127 }
128 BIO_write(out, "\"", 1);
129 }
130}
131 
132void printLatin1AltName(BIO* out, const ASN1_IA5STRING* name, const char* safe_prefix = nullptr) {
133 printAltName(out, reinterpret_cast<const char*>(name->data), name->length, false, safe_prefix);
134}
135 
136void printUtf8AltName(BIO* out, const ASN1_UTF8STRING* name, const char* safe_prefix = nullptr) {
137 printAltName(out, reinterpret_cast<const char*>(name->data), name->length, true, safe_prefix);
138}
139 
140bool printGeneralName(BIO* out, const GENERAL_NAME* gen) {
141 if (gen->type == GEN_DNS) {
142 ASN1_IA5STRING* name = gen->d.dNSName;
143 BIO_write(out, "DNS:", 4);
144 // Note that the preferred name syntax (see RFCs 5280 and 1034) with
145 // wildcards is a subset of what we consider "safe", so spec-compliant DNS
146 // names will never need to be escaped.
147 printLatin1AltName(out, name);
148 } else if (gen->type == GEN_EMAIL) {
149 ASN1_IA5STRING* name = gen->d.rfc822Name;
150 BIO_write(out, "email:", 6);
151 printLatin1AltName(out, name);
152 } else if (gen->type == GEN_URI) {
153 ASN1_IA5STRING* name = gen->d.uniformResourceIdentifier;
154 BIO_write(out, "URI:", 4);
155 // The set of "safe" names was designed to include just about any URI,
156 // with a few exceptions, most notably URIs that contains commas (see
157 // RFC 2396). In other words, most legitimate URIs will not require
158 // escaping.
159 printLatin1AltName(out, name);
160 } else if (gen->type == GEN_DIRNAME) {
161 // Earlier versions of Node.js used X509_NAME_oneline to print the X509_NAME
162 // object. The format was non standard and should be avoided. The use of
163 // X509_NAME_oneline is discouraged by OpenSSL but was required for backward
164 // compatibility. Conveniently, X509_NAME_oneline produced ASCII and the
165 // output was unlikely to contains commas or other characters that would
166 // require escaping. However, it SHOULD NOT produce ASCII output since an
167 // RFC5280 AttributeValue may be a UTF8String.
168 // Newer versions of Node.js have since switched to X509_NAME_print_ex to
169 // produce a better format at the cost of backward compatibility. The new
170 // format may contain Unicode characters and it is likely to contain commas,
171 // which require escaping. Fortunately, the recently safeguarded function
172 // PrintAltName handles all of that safely.
173 BIO_printf(out, "DirName:");
174 auto tmp = KJ_ASSERT_NONNULL(newBio());
175 if (X509_NAME_print_ex(tmp.get(), gen->d.dirn, 0, kX509NameFlagsRFC2253WithinUtf8JSON) < 0) {
176 return false;
177 }
178 char* oline = nullptr;
179 long n_bytes = BIO_get_mem_data(tmp.get(), &oline); // NOLINT(runtime/int)
180 KJ_REQUIRE(n_bytes >= 0);
181 if (n_bytes > 0) {
182 KJ_REQUIRE(oline != nullptr);
183 }
184 
185 printAltName(out, oline, static_cast<size_t>(n_bytes), true, nullptr);
186 } else if (gen->type == GEN_IPADD) {
187 BIO_printf(out, "IP Address:");
188 const ASN1_OCTET_STRING* ip = gen->d.ip;
189 const unsigned char* b = ip->data;
190 if (ip->length == 4) {
191 BIO_printf(out, "%d.%d.%d.%d", b[0], b[1], b[2], b[3]);
192 } else if (ip->length == 16) {
193 for (unsigned int j = 0; j < 8; j++) {
194 uint16_t pair = (b[2 * j] << 8) | b[2 * j + 1];
195 BIO_printf(out, (j == 0) ? "%X" : ":%X", pair);
196 }
197 } else {
198 BIO_printf(out, "<invalid>");
199 }
200 } else if (gen->type == GEN_RID) {
201 // Unlike OpenSSL's default implementation, never print the OID as text and
202 // instead always print its numeric representation.
203 char oline[256] = {0};
204 OBJ_obj2txt(oline, sizeof(oline), gen->d.rid, true);
205 BIO_printf(out, "Registered ID:%s", oline);
206 } else if (gen->type == GEN_OTHERNAME) {
207 // The format that is used here is based on OpenSSL's implementation of
208 // GENERAL_NAME_print (as of OpenSSL 3.0.1). Earlier versions of Node.js
209 // instead produced the same format as i2v_GENERAL_NAME, which was somewhat
210 // awkward, especially when passed to translatePeerCertificate.
211 bool unicode = true;
212 const char* prefix = nullptr;
213 // OpenSSL 1.1.1 does not support othername in GENERAL_NAME_print and may
214 // not define these NIDs.
215#if OPENSSL_VERSION_MAJOR >= 3
216 int nid = OBJ_obj2nid(gen->d.otherName->type_id);
217 switch (nid) {
218 case NID_id_on_SmtpUTF8Mailbox:
219 prefix = "SmtpUTF8Mailbox";
220 break;
221 case NID_XmppAddr:
222 prefix = "XmppAddr";
223 break;
224 case NID_SRVName:
225 prefix = "SRVName";
226 unicode = false;
227 break;
228 case NID_ms_upn:
229 prefix = "UPN";
230 break;
231 case NID_NAIRealm:
232 prefix = "NAIRealm";
233 break;
234 }
235#endif // OPENSSL_VERSION_MAJOR >= 3
236 int val_type = gen->d.otherName->value->type;
237 if (prefix == nullptr || (unicode && val_type != V_ASN1_UTF8STRING) ||
238 (!unicode && val_type != V_ASN1_IA5STRING)) {
239 BIO_printf(out, "othername:<unsupported>");
240 } else {
241 BIO_printf(out, "othername:");
242 if (unicode) {
243 printUtf8AltName(out, gen->d.otherName->value->value.utf8string, prefix);
244 } else {
245 printLatin1AltName(out, gen->d.otherName->value->value.ia5string, prefix);
246 }
247 }
248 } else if (gen->type == GEN_X400) {
249 // TODO(tniessen): this is what OpenSSL does, implement properly instead
250 BIO_printf(out, "X400Name:<unsupported>");
251 } else if (gen->type == GEN_EDIPARTY) {
252 // TODO(tniessen): this is what OpenSSL does, implement properly instead
253 BIO_printf(out, "EdiPartyName:<unsupported>");
254 } else {
255 // This is safe because X509V3_EXT_d2i would have returned nullptr in this
256 // case already.
257 KJ_UNREACHABLE;
258 }
259 
260 return true;
261}
262 
263bool safeX509SubjectAltNamePrint(BIO* out, X509_EXTENSION* ext) {
264 KJ_REQUIRE(OBJ_obj2nid(X509_EXTENSION_get_object(ext)) == NID_subject_alt_name);
265 
266 GENERAL_NAMES* names = static_cast<GENERAL_NAMES*>(X509V3_EXT_d2i(ext));
267 if (names == nullptr) return false;
268 
269 bool ok = true;
270 
271 for (int i = 0; i < sk_GENERAL_NAME_num(names); i++) {
272 GENERAL_NAME* gen = sk_GENERAL_NAME_value(names, i);
273 
274 if (i != 0) BIO_write(out, ", ", 2);
275 
276 if (!(ok = printGeneralName(out, gen))) {
277 break;
278 }
279 }
280 sk_GENERAL_NAME_pop_free(names, GENERAL_NAME_free);
281 
282 return ok;
283}
284 
285bool safeX509InfoAccessPrint(BIO* out, X509_EXTENSION* ext) {
286 KJ_REQUIRE(OBJ_obj2nid(X509_EXTENSION_get_object(ext)) == NID_info_access);
287 
288 AUTHORITY_INFO_ACCESS* descs = static_cast<AUTHORITY_INFO_ACCESS*>(X509V3_EXT_d2i(ext));
289 if (descs == nullptr) return false;
290 
291 bool ok = true;
292 
293 for (int i = 0; i < sk_ACCESS_DESCRIPTION_num(descs); i++) {
294 ACCESS_DESCRIPTION* desc = sk_ACCESS_DESCRIPTION_value(descs, i);
295 
296 if (i != 0) BIO_write(out, "\n", 1);
297 
298 char objtmp[80] = {0};
299 i2t_ASN1_OBJECT(objtmp, sizeof(objtmp), desc->method);
300 BIO_printf(out, "%s - ", objtmp);
301 if (!(ok = printGeneralName(out, desc->location))) {
302 break;
303 }
304 }
305 sk_ACCESS_DESCRIPTION_pop_free(descs, ACCESS_DESCRIPTION_free);
306 
307#if OPENSSL_VERSION_MAJOR < 3
308 BIO_write(out, "\n", 1);
309#endif
310 
311 return ok;
312}
313 
314void addFingerprintDigest(
315 const unsigned char* md, unsigned int md_size, char fingerprint[3 * EVP_MAX_MD_SIZE]) {
316 unsigned int i;
317 const char hex[] = "0123456789ABCDEF";
318 
319 for (i = 0; i < md_size; i++) {
320 fingerprint[3 * i] = hex[(md[i] & 0xf0) >> 4];
321 fingerprint[(3 * i) + 1] = hex[(md[i] & 0x0f)];
322 fingerprint[(3 * i) + 2] = ':';
323 }
324 fingerprint[(3 * (md_size - 1)) + 2] = '\0';
325}
326 
327kj::Maybe<kj::String> getFingerprintDigest(const EVP_MD* method, X509* cert) {
328 unsigned char md[EVP_MAX_MD_SIZE]{};
329 unsigned int md_size;
330 auto fingerprint = kj::heapArray<char>(EVP_MD_size(method) * 3);
331 if (X509_digest(cert, method, md, &md_size)) {
332 addFingerprintDigest(md, md_size, fingerprint.begin());
333 return kj::String(kj::mv(fingerprint));
334 }
335 return kj::none;
336}
337 
338int optionsToFlags(jsg::Optional<X509Certificate::CheckOptions>& options) {
339 X509Certificate::CheckOptions opts = kj::mv(options).orDefault({});
340 int flags = 0;
341 if (!opts.wildcards.orDefault(true)) {
342 flags |= X509_CHECK_FLAG_NO_WILDCARDS;
343 }
344 if (!opts.partialWildcards.orDefault(true)) {
345 flags |= X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS;
346 }
347 if (opts.multiLabelWildcards.orDefault(false)) {
348 flags |= X509_CHECK_FLAG_MULTI_LABEL_WILDCARDS;
349 }
350 if (opts.singleLabelSubdomains.orDefault(false)) {
351 flags |= X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS;
352 }
353 KJ_IF_SOME(subject, opts.subject) {
354 if (subject == "default"_kj) {
355 // nothing to do
356 } else if (subject == "always"_kj) {
357 flags |= X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT;
358 } else if (subject == "never"_kj) {
359 flags |= X509_CHECK_FLAG_NEVER_CHECK_SUBJECT;
360 } else {
361 JSG_FAIL_REQUIRE(Error, "Invalid subject option");
362 }
363 }
364 return flags;
365}
366 
367kj::Maybe<kj::Own<EVP_PKEY>> getInnerPublicKey(X509* cert) {
368 EVP_PKEY* pkey = X509_get_pubkey(cert);
369 if (pkey == nullptr) {
370 return kj::none;
371 }
372 return kj::disposeWith<EVP_PKEY_free>(pkey);
373}
374 
375kj::String getModulusString(BIO* bio, const BIGNUM* n) {
376 BIO_reset(bio);
377 BN_print(bio, n);
378 return toString(bio);
379}
380kj::String getExponentString(BIO* bio, const BIGNUM* e) {
381 BIO_reset(bio);
382 uint64_t exponent_word = static_cast<uint64_t>(BN_get_word(e));
383 BIO_printf(bio, "0x%" PRIx64, exponent_word);
384 return toString(bio);
385}
386 
387jsg::JsUint8Array getRsaPubKey(jsg::Lock& js, RSA* rsa) {
388 int size = i2d_RSA_PUBKEY(rsa, nullptr);
389 KJ_ASSERT(size >= 0);
390 
391 auto buf = jsg::JsUint8Array::create(js, size);
392 auto data = buf.asArrayPtr().begin();
393 KJ_ASSERT(i2d_RSA_PUBKEY(rsa, &data) >= 0);
394 
395 return buf;
396}
397 
398kj::Maybe<int32_t> getECGroupBits(const EC_GROUP* group) {
399 if (group == nullptr) return kj::none;
400 
401 int32_t bits = EC_GROUP_order_bits(group);
402 if (bits <= 0) return kj::none;
403 
404 return bits;
405}
406 
407kj::Maybe<jsg::JsUint8Array> eCPointToBuffer(
408 jsg::Lock& js, const EC_GROUP* group, const EC_POINT* point, point_conversion_form_t form) {
409 size_t len = EC_POINT_point2oct(group, point, form, nullptr, 0, nullptr);
410 if (len == 0) {
411 return kj::none;
412 }
413 
414 auto buffer = jsg::JsUint8Array::create(js, len);
415 
416 len = EC_POINT_point2oct(group, point, form, buffer.asArrayPtr().begin(), buffer.size(), nullptr);
417 if (len == 0) {
418 return kj::none;
419 }
420 
421 return buffer;
422}
423 
424template <const char* (*nid2string)(int nid)>
425kj::Maybe<kj::String> getCurveName(const int nid) {
426 const char* name = nid2string(nid);
427 if (name == nullptr) {
428 return kj::none;
429 }
430 return kj::str(name);
431}
432 
433kj::Maybe<jsg::JsUint8Array> getECPubKey(jsg::Lock& js, const EC_GROUP* group, EC_KEY* ec) {
434 const EC_POINT* pubkey = EC_KEY_get0_public_key(ec);
435 if (pubkey == nullptr) return kj::none;
436 
437 return eCPointToBuffer(js, group, pubkey, EC_KEY_get_conv_form(ec));
438}
439 
440template <X509_NAME* get_name(const X509*)>
441kj::Maybe<jsg::JsObject> getX509NameObject(jsg::Lock& js, X509* cert) {
442 auto obj = js.obj();
443 X509_NAME* name = get_name(cert);
444 KJ_ASSERT(name != nullptr);
445 
446 int cnt = X509_NAME_entry_count(name);
447 KJ_ASSERT(cnt >= 0);
448 
449 for (int i = 0; i < cnt; i++) {
450 X509_NAME_ENTRY* entry = X509_NAME_get_entry(name, i);
451 KJ_ASSERT(entry != nullptr);
452 
453 // We intentionally ignore the value of X509_NAME_ENTRY_set because the
454 // representation as an object does not allow grouping entries into sets
455 // anyway, and multi-value RDNs are rare, i.e., the vast majority of
456 // Relative Distinguished Names contains a single type-value pair only.
457 const ASN1_OBJECT* type = X509_NAME_ENTRY_get_object(entry);
458 ASN1_STRING* value = X509_NAME_ENTRY_get_data(entry);
459 
460 // If OpenSSL knows the type, use the short name of the type as the key, and
461 // the numeric representation of the type's OID otherwise.
462 int type_nid = OBJ_obj2nid(type);
463 char type_buf[80] = {0};
464 const char* type_str;
465 if (type_nid != NID_undef) {
466 type_str = OBJ_nid2sn(type_nid);
467 KJ_ASSERT(type_str != nullptr);
468 } else {
469 OBJ_obj2txt(type_buf, sizeof(type_buf), type, true);
470 type_str = type_buf;
471 }
472 
473 auto name = js.str(kj::StringPtr(type_str));
474 
475 // The previous implementation used X509_NAME_print_ex, which escapes some
476 // characters in the value. The old implementation did not decode/unescape
477 // values correctly though, leading to ambiguous and incorrect
478 // representations. The new implementation only converts to Unicode and does
479 // not escape anything.
480 unsigned char* value_str;
481 int value_str_size = ASN1_STRING_to_UTF8(&value_str, value);
482 if (value_str_size < 0) return kj::none;
483 auto v8_value = js.str(kj::StringPtr(reinterpret_cast<char*>(value_str), value_str_size));
484 OPENSSL_free(value_str);
485 
486 // For backward compatibility, we only create arrays if multiple values
487 // exist for the same key. That is not great but there is not much we can
488 // change here without breaking things. Note that this creates nested data
489 // structures, yet still does not allow representing Distinguished Names
490 // accurately.
491 if (obj.has(js, name)) {
492 auto existing = obj.get(js, name);
493 KJ_IF_SOME(a, existing.tryCast<jsg::JsArray>()) {
494 a.add(js, v8_value);
495 } else {
496 obj.set(js, name, js.arr(existing, v8_value));
497 }
498 } else {
499 obj.set(js, name, v8_value);
500 }
501 }
502 
503 return obj;
504}
505 
506struct StackOfXASN1Disposer: public kj::Disposer {
507 void disposeImpl(void* p) const override {
508 auto ptr = static_cast<STACK_OF(ASN1_OBJECT)*>(p);
509 sk_ASN1_OBJECT_pop_free(ptr, ASN1_OBJECT_free);
510 }
511};
512constexpr StackOfXASN1Disposer stackOfXASN1Disposer;
513} // namespace
514 
515kj::Maybe<jsg::Ref<X509Certificate>> X509Certificate::parse(
516 jsg::Lock& js, kj::Array<const kj::byte> raw) {
517 ClearErrorOnReturn clearErrorOnReturn;
518 KJ_IF_SOME(bio, loadBio(raw)) {
519 auto ptr = PEM_read_bio_X509_AUX(bio.get(), nullptr, NoPasswordCallback, nullptr);
520 if (ptr == nullptr) {
521 MarkPopErrorOnReturn mark_here;
522 auto data = raw.begin();
523 ptr = d2i_X509(nullptr, &data, raw.size());
524 if (ptr == nullptr) {
525 // Invalid certificate data is a user input error, not an internal error.
526 // Return kj::none and let the JS layer throw a user-facing error.
527 return kj::none;
528 }
529 }
530 return js.alloc<X509Certificate>(ptr);
531 }
532 return kj::none;
533}
534 
535kj::Maybe<kj::String> X509Certificate::getSubject() {
536 ClearErrorOnReturn clearErrorOnReturn;
537 KJ_IF_SOME(bio, newBio()) {
538 if (X509_NAME_print_ex(
539 bio.get(), X509_get_subject_name(cert_.get()), 0, kX509NameFlagsMultiline) > 0) {
540 return toString(bio.get());
541 }
542 }
543 return kj::none;
544}
545 
546kj::Maybe<kj::String> X509Certificate::getSubjectAltName() {
547 ClearErrorOnReturn clearErrorOnReturn;
548 KJ_IF_SOME(bio, newBio()) {
549 int index = X509_get_ext_by_NID(cert_.get(), NID_subject_alt_name, -1);
550 if (index < 0) return kj::none;
551 
552 X509_EXTENSION* ext = X509_get_ext(cert_.get(), index);
553 KJ_ASSERT(ext != nullptr);
554 
555 if (!safeX509SubjectAltNamePrint(bio, ext)) {
556 return kj::none;
557 }
558 
559 return toString(bio.get());
560 }
561 return kj::none;
562}
563 
564kj::Maybe<kj::String> X509Certificate::getInfoAccess() {
565 ClearErrorOnReturn clearErrorOnReturn;
566 KJ_IF_SOME(bio, newBio()) {
567 int index = X509_get_ext_by_NID(cert_.get(), NID_info_access, -1);
568 if (index < 0) return kj::none;
569 
570 X509_EXTENSION* ext = X509_get_ext(cert_.get(), index);
571 KJ_REQUIRE(ext != nullptr);
572 
573 if (!safeX509InfoAccessPrint(bio, ext)) {
574 return kj::none;
575 }
576 
577 return toString(bio.get());
578 }
579 return kj::none;
580}
581 
582kj::Maybe<kj::String> X509Certificate::getIssuer() {
583 ClearErrorOnReturn clearErrorOnReturn;
584 KJ_IF_SOME(bio, newBio()) {
585 if (X509_NAME_print_ex(
586 bio.get(), X509_get_issuer_name(cert_.get()), 0, kX509NameFlagsMultiline) > 0) {
587 return toString(bio.get());
588 }
589 }
590 return kj::none;
591}
592 
593kj::Maybe<jsg::Ref<X509Certificate>> X509Certificate::getIssuerCert() {
594 ClearErrorOnReturn clearErrorOnReturn;
595 return issuerCert_.map([](jsg::Ref<X509Certificate>& cert) mutable -> jsg::Ref<X509Certificate> {
596 return cert.addRef();
597 });
598}
599 
600kj::Maybe<kj::String> X509Certificate::getValidFrom() {
601 ClearErrorOnReturn clearErrorOnReturn;
602 KJ_IF_SOME(bio, newBio()) {
603 ASN1_TIME_print(bio.get(), X509_get0_notBefore(cert_.get()));
604 return toString(bio.get());
605 }
606 return kj::none;
607}
608 
609kj::Maybe<kj::String> X509Certificate::getValidTo() {
610 ClearErrorOnReturn clearErrorOnReturn;
611 KJ_IF_SOME(bio, newBio()) {
612 ASN1_TIME_print(bio.get(), X509_get0_notAfter(cert_.get()));
613 return toString(bio.get());
614 }
615 return kj::none;
616}
617 
618kj::Maybe<kj::Array<kj::String>> X509Certificate::getKeyUsage() {
619 ClearErrorOnReturn clearErrorOnReturn;
620 auto ptr = static_cast<STACK_OF(ASN1_OBJECT)*>(
621 X509_get_ext_d2i(cert_.get(), NID_ext_key_usage, nullptr, nullptr));
622 if (ptr == nullptr) return kj::none;
623 auto eku = kj::Own<STACK_OF(ASN1_OBJECT)>(ptr, stackOfXASN1Disposer);
624 const int count = sk_ASN1_OBJECT_num(eku.get());
625 kj::Vector<kj::String> ext_key_usage(count);
626 char buf[256]{};
627 
628 int j = 0;
629 for (int i = 0; i < count; i++) {
630 if (OBJ_obj2txt(buf, sizeof(buf), sk_ASN1_OBJECT_value(eku.get(), i), 1) >= 0) {
631 ext_key_usage[j++] = kj::str(buf);
632 }
633 }
634 
635 return ext_key_usage.releaseAsArray();
636}
637 
638kj::Maybe<kj::Array<const char>> X509Certificate::getSerialNumber() {
639 ClearErrorOnReturn clearErrorOnReturn;
640 if (ASN1_INTEGER* serial_number = X509_get_serialNumber(cert_.get())) {
641 BIGNUM* bn = ASN1_INTEGER_to_BN(serial_number, nullptr);
642 if (bn != nullptr) {
643 KJ_DEFER(BN_clear_free(bn));
644 char* data = BN_bn2hex(bn);
645 return kj::arrayPtr<const char>(data, strlen(data))
646 .attach(kj::defer([data, len = strlen(data)] { OPENSSL_clear_free(data, len); }));
647 }
648 }
649 
650 return kj::none;
651}
652 
653jsg::JsUint8Array X509Certificate::getRaw(jsg::Lock& js) {
654 ClearErrorOnReturn clearErrorOnReturn;
655 int size = i2d_X509(cert_.get(), nullptr);
656 auto buf = jsg::JsUint8Array::create(js, size);
657 auto data = buf.asArrayPtr().begin();
658 KJ_REQUIRE(i2d_X509(cert_.get(), &data) >= 0);
659 return buf;
660}
661 
662kj::Maybe<jsg::Ref<CryptoKey>> X509Certificate::getPublicKey(jsg::Lock& js) {
663 ClearErrorOnReturn clear_error_on_return;
664 auto ptr = X509_get_pubkey(cert_.get());
665 if (ptr == nullptr) return kj::none;
666 auto pkey = kj::disposeWith<EVP_PKEY_free>(ptr);
667 return js.alloc<CryptoKey>(CryptoKey::Impl::from(js, kj::mv(pkey)));
668}
669 
670kj::Maybe<kj::String> X509Certificate::getPem() {
671 ClearErrorOnReturn clearErrorOnReturn;
672 KJ_IF_SOME(bio, newBio()) {
673 if (PEM_write_bio_X509(bio.get(), cert_.get())) {
674 return toString(bio.get());
675 }
676 }
677 return kj::none;
678}
679 
680kj::Maybe<kj::String> X509Certificate::getFingerprint() {
681 ClearErrorOnReturn clearErrorOnReturn;
682 return getFingerprintDigest(EVP_sha1(), cert_.get());
683}
684 
685kj::Maybe<kj::String> X509Certificate::getFingerprint256() {
686 ClearErrorOnReturn clearErrorOnReturn;
687 return getFingerprintDigest(EVP_sha256(), cert_.get());
688}
689 
690kj::Maybe<kj::String> X509Certificate::getFingerprint512() {
691 ClearErrorOnReturn clearErrorOnReturn;
692 return getFingerprintDigest(EVP_sha512(), cert_.get());
693}
694 
695bool X509Certificate::getIsCA() {
696 ClearErrorOnReturn clearErrorOnReturn;
697 return X509_check_ca(cert_.get()) == 1;
698}
699 
700kj::Maybe<kj::String> X509Certificate::checkHost(
701 kj::String name, jsg::Optional<CheckOptions> options) {
702 ClearErrorOnReturn clearErrorOnReturn;
703 char* peername = nullptr;
704 switch (
705 X509_check_host(cert_.get(), name.begin(), name.size(), optionsToFlags(options), &peername)) {
706 case 1: { // Match!
707 if (peername != nullptr) {
708 KJ_DEFER(OPENSSL_free(peername));
709 return kj::str(peername);
710 }
711 return kj::mv(name);
712 }
713 case 0: // No Match!
714 return kj::none; // No return value is set
715 case -2: // Error!
716 JSG_FAIL_REQUIRE(Error, "Invalid name");
717 default: // Error!
718 JSG_FAIL_REQUIRE(Error, "Operation failed");
719 }
720 
721 KJ_UNREACHABLE;
722}
723 
724kj::Maybe<kj::String> X509Certificate::checkEmail(
725 kj::String email, jsg::Optional<CheckOptions> options) {
726 ClearErrorOnReturn clearErrorOnReturn;
727 switch (X509_check_email(cert_.get(), email.begin(), email.size(), optionsToFlags(options))) {
728 case 1: // Match!
729 return kj::mv(email);
730 case 0: // No Match!
731 return kj::none; // No return value is set
732 case -2: // Error!
733 JSG_FAIL_REQUIRE(Error, "Invalid name");
734 default: // Error!
735 JSG_FAIL_REQUIRE(Error, "Operation failed");
736 }
737 
738 KJ_UNREACHABLE;
739}
740 
741kj::Maybe<kj::String> X509Certificate::checkIp(kj::String ip, jsg::Optional<CheckOptions> options) {
742 ClearErrorOnReturn clearErrorOnReturn;
743 switch (X509_check_ip_asc(cert_.get(), ip.begin(), optionsToFlags(options))) {
744 case 1: // Match!
745 return kj::mv(ip);
746 case 0: // No Match!
747 return kj::none; // No return value is set
748 case -2: // Error!
749 JSG_FAIL_REQUIRE(Error, "Invalid IP");
750 default: // Error!
751 JSG_FAIL_REQUIRE(Error, "Operation failed");
752 }
753 
754 KJ_UNREACHABLE;
755}
756 
757bool X509Certificate::checkIssued(jsg::Ref<X509Certificate> other) {
758 ClearErrorOnReturn clearErrorOnReturn;
759 return X509_check_issued(other->cert_.get(), cert_.get()) == X509_V_OK;
760}
761 
762bool X509Certificate::checkPrivateKey(jsg::Ref<CryptoKey> privateKey) {
763 JSG_REQUIRE(privateKey->getType() == "private"_kj, Error, "Invalid key type");
764 return privateKey->verifyX509Private(cert_.get());
765}
766 
767bool X509Certificate::verify(jsg::Ref<CryptoKey> publicKey) {
768 JSG_REQUIRE(publicKey->getType() == "public"_kj, Error, "Invalid key type");
769 return publicKey->verifyX509Public(cert_.get());
770}
771 
772jsg::JsObject X509Certificate::toLegacyObject(jsg::Lock& js) {
773 ClearErrorOnReturn clearErrorOnReturn;
774 auto obj = js.obj();
775 KJ_IF_SOME(subject, getX509NameObject<X509_get_subject_name>(js, cert_.get())) {
776 obj.set(js, "subject", subject);
777 }
778 KJ_IF_SOME(issuer, getX509NameObject<X509_get_issuer_name>(js, cert_.get())) {
779 obj.set(js, "issuer", issuer);
780 }
781 obj.set(js, "subjectAltName", js.str(getSubjectAltName().orDefault(kj::String())));
782 obj.set(js, "infoAccess", js.str(getInfoAccess().orDefault(kj::String())));
783 obj.set(js, "ca", js.boolean(getIsCA()));
784 
785 KJ_IF_SOME(key, getInnerPublicKey(cert_.get())) {
786 auto bio = KJ_ASSERT_NONNULL(newBio());
787 switch (EVP_PKEY_id(key.get())) {
788 case EVP_PKEY_RSA: {
789 RSA* rsa = EVP_PKEY_get0_RSA(key.get());
790 KJ_ASSERT(rsa != nullptr);
791 obj.set(js, "modulus", js.str(getModulusString(bio.get(), RSA_get0_n(rsa))));
792 obj.set(js, "bits", js.num(RSA_bits(rsa)));
793 obj.set(js, "exponent", js.str(getExponentString(bio.get(), RSA_get0_e(rsa))));
794 obj.set(js, "pubkey", getRsaPubKey(js, rsa));
795 break;
796 }
797 case EVP_PKEY_EC: {
798 EC_KEY* ec = EVP_PKEY_get0_EC_KEY(key.get());
799 const EC_GROUP* group = EC_KEY_get0_group(ec);
800 KJ_ASSERT(ec != nullptr);
801 KJ_ASSERT(group != nullptr);
802 KJ_IF_SOME(bits, getECGroupBits(group)) {
803 obj.set(js, "bits", js.num(bits));
804 }
805 KJ_IF_SOME(pubkey, getECPubKey(js, group, ec)) {
806 obj.set(js, "pubkey", pubkey);
807 }
808 
809 const int nid = EC_GROUP_get_curve_name(group);
810 if (nid != 0) {
811 // Curve is well-known, get its OID and NIST nick-name (if it has one).
812 
813 KJ_IF_SOME(name, getCurveName<OBJ_nid2sn>(nid)) {
814 obj.set(js, "asn1Curve", js.str(name));
815 }
816 KJ_IF_SOME(name, getCurveName<EC_curve_nid2nist>(nid)) {
817 obj.set(js, "nistCurve", js.str(name));
818 }
819 } else {
820 // Unnamed curves can be described by their mathematical properties,
821 // but aren't used much (at all?) with X.509/TLS. Support later if needed.
822 }
823 break;
824 }
825 }
826 }
827 
828 KJ_IF_SOME(from, getValidFrom()) {
829 obj.set(js, "valid_from", js.str(from));
830 }
831 KJ_IF_SOME(to, getValidTo()) {
832 obj.set(js, "valid_to", js.str(to));
833 }
834 
835 KJ_IF_SOME(fingerprint, getFingerprint()) {
836 obj.set(js, "fingerprint", js.str(fingerprint));
837 }
838 KJ_IF_SOME(fingerprint256, getFingerprint256()) {
839 obj.set(js, "fingerprint256", js.str(fingerprint256));
840 }
841 KJ_IF_SOME(fingerprint512, getFingerprint512()) {
842 obj.set(js, "fingerprint512", js.str(fingerprint512));
843 }
844 KJ_IF_SOME(keyUsage, getKeyUsage()) {
845 obj.set(js, "ext_key_usage",
846 js.arr(keyUsage.asPtr(), [](jsg::Lock& js, const kj::String& val) { return js.str(val); }));
847 }
848 KJ_IF_SOME(serialNumber, getSerialNumber()) {
849 obj.set(js, "serialNumber", js.str(serialNumber));
850 }
851 obj.set(js, "raw", getRaw(js));
852 
853 return obj;
854}
855 
856} // namespace workerd::api