Skip to content

Commit

Permalink
Implement node:crypto X509Certificate
Browse files Browse the repository at this point in the history
  • Loading branch information
jasnell committed Jun 26, 2024
1 parent c96f1c8 commit 701ea59
Show file tree
Hide file tree
Showing 11 changed files with 1,320 additions and 2 deletions.
10 changes: 9 additions & 1 deletion src/node/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ import {
createSecretKey,
} from 'node-internal:crypto_keys';

import {
X509Certificate,
} from 'node-internal:crypto_x509';

export {
// DH
DiffieHellman,
Expand Down Expand Up @@ -118,6 +122,8 @@ export {
createPrivateKey,
createPublicKey,
createSecretKey,
// X509
X509Certificate,
}

export function getCiphers() {
Expand Down Expand Up @@ -220,6 +226,8 @@ export default {
// WebCrypto
subtle,
webcrypto,
// X509
X509Certificate,
};

// Classes
Expand All @@ -234,7 +242,7 @@ export default {
// * [ ] crypto.KeyObject
// * [ ] crypto.Sign
// * [ ] crypto.Verify
// * [ ] crypto.X509Certificate
// * [x] crypto.X509Certificate
// * [ ] crypto.constants
// * [ ] crypto.DEFAULT_ENCODING
// * Primes
Expand Down
36 changes: 36 additions & 0 deletions src/node/internal/crypto.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,42 @@ export function checkPrimeSync(candidate: ArrayBufferView, num_checks: number):
export function randomPrime(size: number, safe: boolean, add?: ArrayBufferView|undefined,
rem?: ArrayBufferView|undefined): ArrayBuffer;

// X509Certificate
export interface CheckOptions {
subject?: string;
wildcards?: boolean;
partialWildcards?: boolean;
multiLabelWildcards?: boolean;
singleLabelSubdomains?: boolean;
}

export class X509Certificate {
static parse(data: ArrayBuffer|ArrayBufferView): X509Certificate;
public get subject(): string|undefined;
public get subjectAltName(): string|undefined;
public get infoAccess(): string|undefined;
public get issuer(): string|undefined;
public get issuerCert(): X509Certificate|undefined;
public get validFrom(): string|undefined;
public get validTo(): string|undefined;
public get fingerprint(): string|undefined;
public get fingerprint256(): string|undefined;
public get fingerprint512(): string|undefined;
public get keyUsage(): string[]|undefined;
public get serialNumber(): string|undefined;
public get pem(): string|undefined;
public get raw(): ArrayBuffer|undefined;
public get publicKey(): CryptoKey|undefined;
public get isCA(): boolean;
public checkHost(host: string, options?: CheckOptions): string|undefined;
public checkEmail(email: string, options?: CheckOptions): string|undefined;
public checkIP(ip: string, options?: CheckOptions): string|undefined;
public checkIssued(cert: X509Certificate): boolean;
public checkPrivateKey(key: CryptoKey): boolean;
public verify(key: CryptoKey): boolean;
public toLegacyObject(): object;
}

// Hash and Hmac
export class HashHandle {
public constructor(algorithm: string, xofLen: number);
Expand Down
277 changes: 277 additions & 0 deletions src/node/internal/crypto_x509.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
// Copyright (c) 2017-2022 Cloudflare, Inc.
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
// https://opensource.org/licenses/Apache-2.0
//
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

/* todo: the following is adopted code, enabling linting one day */
/* eslint-disable */

import {
default as cryptoImpl,
CheckOptions,
} from 'node-internal:crypto';

import {
validateString,
} from 'node-internal:validators';

import {
isArrayBufferView,
} from 'node-internal:internal_types';

import {
Buffer,
} from 'node-internal:internal_buffer';

import {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
} from 'node-internal:internal_errors';

import {
kHandle,
} from 'node-internal:crypto_util';

import {
PublicKeyObject,
PrivateKeyObject,
} from 'node-internal:crypto_keys';

export class X509Certificate {
#handle?: cryptoImpl.X509Certificate = undefined;
#state = new Map();

constructor(buffer: ArrayBufferView | ArrayBuffer | cryptoImpl.X509Certificate) {
if (buffer instanceof cryptoImpl.X509Certificate) {
this.#handle = buffer;
return;
}
if (typeof buffer === 'string') {
buffer = Buffer.from(buffer);
}
if (!isArrayBufferView(buffer)) {
throw new ERR_INVALID_ARG_TYPE('buffer',
['string', 'Buffer', 'TypedArray', 'DataView'], buffer);
}
this.#handle = cryptoImpl.X509Certificate.parse(buffer);
}

get subject() {
let value = this.#state.get('subject');
if (value === undefined) {
value = this.#handle!.subject;
this.#state.set('subject', value);
}
return value;
}

get subjectAltName() {
let value = this.#state.get('subjectAltName');
if (value === undefined) {
value = this.#handle!.subjectAltName;
this.#state.set('subjectAltName', value);
}
return value;
}

get issuer() {
let value = this.#state.get('issuer');
if (value === undefined) {
value = this.#handle!.issuer;
this.#state.set('issuer', value);
}
return value;
}

get issuerCertificate() {
let value = this.#state.get('issuerCertificate');
if (value === undefined) {
const cert = this.#handle!.issuerCert;
if (cert)
value = new X509Certificate(cert);
this.#state.set('issuerCertificate', value);
}
return value;
}

get infoAccess() {
let value = this.#state.get('infoAccess');
if (value === undefined) {
value = this.#handle!.infoAccess;
this.#state.set('infoAccess', value);
}
return value;
}

get validFrom() {
let value = this.#state.get('validFrom');
if (value === undefined) {
value = this.#handle!.validFrom;
this.#state.set('validFrom', value);
}
return value;
}

get validTo() {
let value = this.#state.get('validTo');
if (value === undefined) {
value = this.#handle!.validTo;
this.#state.set('validTo', value);
}
return value;
}

get fingerprint() {
let value = this.#state.get('fingerprint');
if (value === undefined) {
value = this.#handle!.fingerprint;
this.#state.set('fingerprint', value);
}
return value;
}

get fingerprint256() {
let value = this.#state.get('fingerprint256');
if (value === undefined) {
value = this.#handle!.fingerprint256;
this.#state.set('fingerprint256', value);
}
return value;
}

get fingerprint512() {
let value = this.#state.get('fingerprint512');
if (value === undefined) {
value = this.#handle!.fingerprint512;
this.#state.set('fingerprint512', value);
}
return value;
}

get keyUsage() {
let value = this.#state.get('keyUsage');
if (value === undefined) {
value = this.#handle!.keyUsage;
this.#state.set('keyUsage', value);
}
return value;
}

get serialNumber() {
let value = this.#state.get('serialNumber');
if (value === undefined) {
value = this.#handle!.serialNumber;
this.#state.set('serialNumber', value);
}
return value;
}

get raw() {
let value = this.#state.get('raw');
if (value === undefined) {
value = this.#handle!.raw;
this.#state.set('raw', value);
}
return value;
}

get publicKey() {
let value = this.#state.get('publicKey');
if (value === undefined) {
const inner = this.#handle!.publicKey;
if (inner !== undefined) {
value = PublicKeyObject.from(inner);
this.#state.set('publicKey', value);
}
}
return value;
}

toString() {
let value = this.#state.get('pem');
if (value === undefined) {
value = this.#handle!.pem;
this.#state.set('pem', value);
}
return value;
}

// There's no standardized JSON encoding for X509 certs so we
// fallback to providing the PEM encoding as a string.
toJSON() { return this.toString(); }

get ca() {
let value = this.#state.get('ca');
if (value === undefined) {
value = this.#handle!.isCA;
this.#state.set('ca', value);
}
return value;
}

checkHost(name: string, options?: CheckOptions) {
validateString(name, 'name');
return this.#handle!.checkHost(name, options);
}

checkEmail(email: string, options?: CheckOptions) {
validateString(email, 'email');
return this.#handle!.checkEmail(email, options);
}

checkIP(ip: string, options?: CheckOptions) {
validateString(ip, 'ip');
// The options argument is currently undocumented since none of the options
// have any effect on the behavior of this function. However, we still parse
// the options argument in case OpenSSL adds flags in the future that do
// affect the behavior of X509_check_ip. This ensures that no invalid values
// are passed as the second argument in the meantime.
return this.#handle!.checkIP(ip, options);
}

checkIssued(otherCert: X509Certificate) {
if (!(otherCert instanceof X509Certificate))
throw new ERR_INVALID_ARG_TYPE('otherCert', 'X509Certificate', otherCert);
return this.#handle!.checkIssued(otherCert.#handle!);
}

checkPrivateKey(pkey: PrivateKeyObject) {
if (!(pkey instanceof PrivateKeyObject))
throw new ERR_INVALID_ARG_TYPE('pkey', 'KeyObject', pkey);
if (pkey.type !== 'private')
throw new ERR_INVALID_ARG_VALUE('pkey', pkey);
return this.#handle!.checkPrivateKey(pkey[kHandle]);
}

verify(pkey: PublicKeyObject) {
if (!(pkey instanceof PublicKeyObject))
throw new ERR_INVALID_ARG_TYPE('pkey', 'KeyObject', pkey);
if (pkey.type !== 'public')
throw new ERR_INVALID_ARG_VALUE('pkey', pkey);
return this.#handle!.verify(pkey[kHandle]);
}

toLegacyObject() {
return this.#handle!.toLegacyObject();
}
}
35 changes: 35 additions & 0 deletions src/workerd/api/crypto/asymmetric.c++
Original file line number Diff line number Diff line change
Expand Up @@ -2309,4 +2309,39 @@ kj::Own<CryptoKey::Impl> CryptoKey::Impl::importEddsa(
// In X25519 we ignore the id-X25519 identifier, as with id-ecDH above.
return kj::heap<EdDsaKey>(kj::mv(evpPkey), normalizedName, keyType, extractable, usages);
}

kj::Own<CryptoKey::Impl> fromRsaKey(kj::Own<EVP_PKEY> key) {
return kj::heap<RsassaPkcs1V15Key>(kj::mv(key),
CryptoKey::RsaKeyAlgorithm {.name = "RSA"_kj },
"public"_kj, true,
CryptoKeyUsageSet::decrypt() | CryptoKeyUsageSet::sign() | CryptoKeyUsageSet::verify());
}

kj::Own<CryptoKey::Impl> fromEcKey(kj::Own<EVP_PKEY> key) {
auto nid = EVP_PKEY_id(key.get());
if (nid == NID_X25519 || nid == NID_ED25519) {
return fromEd25519Key(kj::mv(key));
}

auto curveName = OBJ_nid2sn(nid);
if (curveName == nullptr) {
curveName = "unknown";
}

auto [normalizedNamedCurve, curveId, rsSize] = lookupEllipticCurve(curveName);

return kj::heap<EllipticKey>(kj::mv(key),
CryptoKey::EllipticKeyAlgorithm {
.name = "ECDSA"_kj,
.namedCurve = normalizedNamedCurve
},
"public"_kj, rsSize, true,
CryptoKeyUsageSet::verify());
}

kj::Own<CryptoKey::Impl> fromEd25519Key(kj::Own<EVP_PKEY> key) {
return kj::heap<EdDsaKey>(kj::mv(key), "Ed25519"_kj, "public"_kj, true,
CryptoKeyUsageSet::sign() | CryptoKeyUsageSet::verify());
}

} // namespace workerd::api
Loading

0 comments on commit 701ea59

Please sign in to comment.