Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 8 additions & 21 deletions src/jsc/bindings/JSX509CertificatePrototype.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
#include "webcrypto/CryptoKeyRSA.h"
#include "webcrypto/CryptoKeyOKP.h"
#include "webcrypto/CryptoKeyAES.h"
#include "wtf/DateMath.h"
#include "AsymmetricKeyValue.h"
#include <JavaScriptCore/DateInstance.h>
#include "JSKeyObject.h"
Expand Down Expand Up @@ -404,6 +403,12 @@ static JSValue undefinedIfEmpty(JSUint8Array* value)
return value;
}

static JSValue validityDate(VM& vm, JSGlobalObject* globalObject, std::optional<int64_t> seconds)
{
double ms = seconds ? static_cast<double>(*seconds) * 1000.0 : PNaN;
return DateInstance::create(vm, globalObject->dateStructure(), ms);
}

JSC_DEFINE_HOST_FUNCTION(jsX509CertificateProtoFuncVerify, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
VM& vm = globalObject->vm();
Expand Down Expand Up @@ -719,16 +724,7 @@ JSC_DEFINE_CUSTOM_GETTER(jsX509CertificateGetter_validToDate, (JSGlobalObject *
return {};
}

auto* validToDate = thisObject->validTo(globalObject);
RETURN_IF_EXCEPTION(scope, {});
auto view = validToDate->view(globalObject);
RETURN_IF_EXCEPTION(scope, {});
Bun::UTF8View validToDateView = Bun::UTF8View(view);
if (view->isEmpty())
return JSValue::encode(jsUndefined());
std::span<const Latin1Character> span = { reinterpret_cast<const Latin1Character*>(validToDateView.span().data()), validToDateView.span().size() };
double date = WTF::parseDate(span);
return JSValue::encode(JSC::DateInstance::create(vm, globalObject->dateStructure(), date));
return JSValue::encode(validityDate(vm, globalObject, thisObject->view().getValidToTime()));
}

JSC_DEFINE_CUSTOM_GETTER(jsX509CertificateGetter_validFromDate, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName))
Expand All @@ -742,15 +738,6 @@ JSC_DEFINE_CUSTOM_GETTER(jsX509CertificateGetter_validFromDate, (JSGlobalObject
return {};
}

auto* validFromDate = thisObject->validFrom(globalObject);
RETURN_IF_EXCEPTION(scope, {});
auto view = validFromDate->view(globalObject);
RETURN_IF_EXCEPTION(scope, {});
Bun::UTF8View validFromDateView = Bun::UTF8View(view);
if (view->isEmpty())
return JSValue::encode(jsUndefined());
std::span<const Latin1Character> span = { reinterpret_cast<const Latin1Character*>(validFromDateView.span().data()), validFromDateView.span().size() };
double date = WTF::parseDate(span);
return JSValue::encode(JSC::DateInstance::create(vm, globalObject->dateStructure(), date));
return JSValue::encode(validityDate(vm, globalObject, thisObject->view().getValidFromTime()));
}
} // namespace Bun
20 changes: 20 additions & 0 deletions src/jsc/bindings/ncrypto.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,26 @@ std::optional<std::string> X509View::getSignatureAlgorithmOID() const
return std::string(buf, static_cast<size_t>(len));
}

static std::optional<int64_t> asn1TimeToPosix(const ASN1_TIME* time)
{
int64_t seconds;
// _nonstandard: the X509 parser accepts a UTCTime "+hhmm" offset, and Node (OpenSSL) applies it.
if (time == nullptr || !ASN1_TIME_to_posix_nonstandard(time, &seconds)) return std::nullopt;
return seconds;
}

std::optional<int64_t> X509View::getValidFromTime() const
{
if (cert_ == nullptr) return std::nullopt;
return asn1TimeToPosix(X509_get0_notBefore(cert_));
}

std::optional<int64_t> X509View::getValidToTime() const
{
if (cert_ == nullptr) return std::nullopt;
return asn1TimeToPosix(X509_get0_notAfter(cert_));
}

DataPointer X509View::getSerialNumber() const
{
ClearErrorOnReturn clearErrorOnReturn;
Expand Down
3 changes: 3 additions & 0 deletions src/jsc/bindings/ncrypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -958,6 +958,9 @@ class X509View final {
BIOPointer getValidTo() const;
std::optional<std::string_view> getSignatureAlgorithm() const;
std::optional<std::string> getSignatureAlgorithmOID() const;
// Seconds since the Unix epoch.
std::optional<int64_t> getValidFromTime() const;
std::optional<int64_t> getValidToTime() const;
DataPointer getSerialNumber() const;
Result<EVPKeyPointer, int> getPublicKey() const;
StackOfASN1 getKeyUsage() const;
Expand Down
69 changes: 69 additions & 0 deletions test/js/node/crypto/x509.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,75 @@ describe("X509Certificate.prototype property descriptors", () => {
});
});

describe("X509Certificate validFromDate / validToDate", () => {
// Self-signed, CN=localhost. Both times are GeneralizedTime, so another four-digit year can be
// written over them without changing any DER length. That breaks the signature, which
// X509Certificate does not check.
const notBefore = "00200101000000Z";
const notAfter = "00751231235959Z";
const templatePem = `-----BEGIN CERTIFICATE-----
MIIBPTCB46ADAgECAgIQADAKBggqhkjOPQQDAjAUMRIwEAYDVQQDDAlsb2NhbGhv
c3QwIhgPMDAyMDAxMDEwMDAwMDBaGA8wMDc1MTIzMTIzNTk1OVowFDESMBAGA1UE
AwwJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJC2RuReWuXPw
IXWc22JNB/Jwkm4uGiaO1cRhQh6gF7kTLflBzbp6vAnZIB3ukIRPh1syrqVuEPuB
ugLCAXxsoqMhMB8wHQYDVR0OBBYEFDYYVTNtjOn8v4bS9+yhJX5DS0A1MAoGCCqG
SM49BAMCA0kAMEYCIQDgLDP/kZnwdTEpvTsMQKhLWg4NeiH62qkajleA8tWF8QIh
AJSCZhtcwld0QcIt8+FsNj13xry7oItjmw9/n1dicxIW
-----END CERTIFICATE-----`;

// The same certificate with the validity rewritten as UTCTime "200101000000-0530" and
// "301231235959+0100". RFC 5280 forbids the offset, but BoringSSL and OpenSSL both parse it.
const utcOffsetPem = `-----BEGIN CERTIFICATE-----
MIIBQTCB56ADAgECAgIQADAKBggqhkjOPQQDAjAUMRIwEAYDVQQDDAlsb2NhbGhv
c3QwJhcRMjAwMTAxMDAwMDAwLTA1MzAXETMwMTIzMTIzNTk1OSswMTAwMBQxEjAQ
BgNVBAMMCWxvY2FsaG9zdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCQtkbkX
lrlz8CF1nNtiTQfycJJuLhomjtXEYUIeoBe5Ey35Qc26erwJ2SAd7pCET4dbMq6l
bhD7gboCwgF8bKKjITAfMB0GA1UdDgQWBBQ2GFUzbYzp/L+G0vfsoSV+Q0tANTAK
BggqhkjOPQQDAgNJADBGAiEA4Cwz/5GZ8HUxKb07DECoS1oODXoh+tqpGo5XgPLV
hfECIQCUgmYbXMJXdEHCLfPhbDY9d8a8u6CLY5sPf59XYnMSFg==
-----END CERTIFICATE-----`;

function certForYear(year: string) {
const der = Buffer.from(new X509Certificate(templatePem).raw);
der.write(year, der.indexOf(notBefore), "latin1");
der.write(year, der.indexOf(notAfter), "latin1");
return new X509Certificate(der);
}

// The printed year "30" used to be parsed back as 2030, so a certificate that expired in the
// year 30 looked valid to `cert.validToDate > new Date()`.
test("a year below 100 is not moved to 1950-2049", () => {
const cert = new X509Certificate(templatePem);
expect({
validFrom: cert.validFrom,
validFromDate: cert.validFromDate.toISOString(),
validTo: cert.validTo,
validToDate: cert.validToDate.toISOString(),
}).toEqual({
validFrom: "Jan 1 00:00:00 20 GMT",
validFromDate: "0020-01-01T00:00:00.000Z",
validTo: "Dec 31 23:59:59 75 GMT",
validToDate: "0075-12-31T23:59:59.000Z",
});
expect(certForYear("0030").validToDate < new Date()).toBe(true);
});

test.each(["0000", "0049", "0050", "0099", "0100", "1601", "1969", "1970", "2038", "2049", "2050", "9999"])(
"GeneralizedTime year %s",
year => {
const cert = certForYear(year);
expect(cert.validFromDate.toISOString()).toBe(`${year}-01-01T00:00:00.000Z`);
expect(cert.validToDate.toISOString()).toBe(`${year}-12-31T23:59:59.000Z`);
},
);

test("a UTCTime timezone offset is applied", () => {
const cert = new X509Certificate(utcOffsetPem);
expect(cert.validFromDate.toISOString()).toBe("2020-01-01T05:30:00.000Z");
expect(cert.validToDate.toISOString()).toBe("2030-12-31T22:59:59.000Z");
});
});

describe("X509Certificate with an empty subject/issuer DN", () => {
// Self-signed, `openssl req -x509 -subj "/"`: both names are empty sequences.
const emptyDN = `-----BEGIN CERTIFICATE-----
Expand Down
Loading