Skip to content

Commit

Permalink
Implement Node.js spkac APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
jasnell committed Jun 27, 2024
1 parent 900909e commit ddd1542
Show file tree
Hide file tree
Showing 11 changed files with 349 additions and 3 deletions.
6 changes: 5 additions & 1 deletion src/node/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ import {
createSecretKey,
} from 'node-internal:crypto_keys';

import { Certificate } from 'node-internal:crypto_spkac';

export {
// DH
DiffieHellman,
Expand Down Expand Up @@ -126,6 +128,8 @@ export {
createPrivateKey,
createPublicKey,
createSecretKey,
// Spkac
Certificate,
}

export function getCiphers() {
Expand Down Expand Up @@ -234,7 +238,7 @@ export default {
};

// Classes
// * [ ] crypto.Certificate
// * [x] crypto.Certificate
// * [ ] crypto.Cipher
// * [ ] crypto.Decipher
// * [x] crypto.DiffieHellman
Expand Down
5 changes: 5 additions & 0 deletions src/node/internal/crypto.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ export function createSecretKey(key: ArrayBuffer | ArrayBufferView): CryptoKey;
export function createPrivateKey(key: InnerCreateAsymmetricKeyOptions): CryptoKey;
export function createPublicKey(key: InnerCreateAsymmetricKeyOptions): CryptoKey;

// Spkac
export function verifySpkac(input: ArrayBufferView|ArrayBuffer): boolean;
export function exportPublicKey(input: ArrayBufferView|ArrayBuffer): null | ArrayBuffer;
export function exportChallenge(input: ArrayBufferView|ArrayBuffer): null | ArrayBuffer;

export type KeyData = string | ArrayBuffer | ArrayBufferView;

export interface RsaKeyAlgorithm {
Expand Down
74 changes: 74 additions & 0 deletions src/node/internal/crypto_spkac.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// 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 } from 'node-internal:crypto';
import {
StringLike,
Buffer,
} from 'node-internal:internal_buffer';

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

type ArrayLike = cryptoImpl.ArrayLike;

export function verifySpkac(spkac: StringLike | ArrayLike, encoding?: string) {
return cryptoImpl.verifySpkac(getArrayBufferOrView(spkac as any, 'spkac', encoding));
}

export function exportPublicKey(spkac: StringLike | ArrayLike, encoding?: string) {
const ret = cryptoImpl.exportPublicKey(getArrayBufferOrView(spkac as any, 'spkac', encoding));
return ret ? Buffer.from(ret) : Buffer.alloc(0);
}

export function exportChallenge(spkac: StringLike | ArrayLike, encoding?: string) {
const ret = cryptoImpl.exportChallenge(getArrayBufferOrView(spkac as any, 'spkac', encoding));
return ret ? Buffer.from(ret) : Buffer.alloc(0);
}

// The legacy implementation of this exposed the Certificate
// object and required that users create an instance before
// calling the member methods. This API pattern has been
// deprecated, however, as the method implementations do not
// rely on any object state.

// For backwards compatibility reasons, this cannot be converted into a
// ES6 Class.
export function Certificate(this: any) {
if (!(this instanceof Certificate))
return new (Certificate as any)();
}

Certificate.prototype.verifySpkac = verifySpkac;
Certificate.prototype.exportPublicKey = exportPublicKey;
Certificate.prototype.exportChallenge = exportChallenge;

Certificate.exportChallenge = exportChallenge;
Certificate.exportPublicKey = exportPublicKey;
Certificate.verifySpkac = verifySpkac;
2 changes: 1 addition & 1 deletion src/node/internal/internal_buffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ function createBuffer(length: number) : Buffer {
}

type WithImplicitCoercion<T> = | T | { valueOf(): T; };
type StringLike = WithImplicitCoercion<string> | { [Symbol.toPrimitive](hint: "string"): string; };
export type StringLike = WithImplicitCoercion<string> | { [Symbol.toPrimitive](hint: "string"): string; };
type ArrayBufferLike = WithImplicitCoercion<ArrayBuffer| SharedArrayBuffer>;
type BufferSource = StringLike|ArrayBufferLike|Uint8Array|ReadonlyArray<number>;

Expand Down
1 change: 0 additions & 1 deletion src/node/internal/internal_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,4 +208,3 @@ export function createDeferredPromise() {
reject,
};
}

106 changes: 106 additions & 0 deletions src/workerd/api/crypto/spkac.c++
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#include "spkac.h"
#include "impl.h"
#include <workerd/jsg/jsg.h>
#include <workerd/io/io-context.h>
#include <openssl/x509.h>
#include <openssl/pem.h>

namespace workerd::api {
namespace {
kj::ArrayPtr<const kj::byte> trim(kj::ArrayPtr<const kj::byte> input) {
size_t length = input.size();
for (auto i = length - 1; i >= 0; --i) {
if (input[i] != ' ' && input[i] != '\n' && input[i] != '\r' && input[i] != '\t') {
break;
}
}
return input.first(length);
}

kj::Array<kj::byte> toArray(BIO* bio) {
BUF_MEM* bptr;
BIO_get_mem_ptr(bio, &bptr);
auto buf = kj::heapArray<char>(bptr->length);
auto aptr = kj::arrayPtr(bptr->data, bptr->length);
buf.asPtr().copyFrom(aptr);
return buf.releaseAsBytes();
}

kj::Maybe<kj::Own<NETSCAPE_SPKI>> tryGetSpki(kj::ArrayPtr<const kj::byte> input) {
static constexpr int32_t kMaxLength = kj::maxValue;
JSG_REQUIRE(input.size() <= kMaxLength, RangeError, "spkac is too large");
input = trim(input);
auto ptr = NETSCAPE_SPKI_b64_decode(input.asChars().begin(), input.size());
if (!ptr) return kj::none;
return kj::disposeWith<NETSCAPE_SPKI_free>(ptr);
}

kj::Maybe<kj::Own<EVP_PKEY>> tryOwnPkey(kj::Own<NETSCAPE_SPKI>& spki) {
auto pkey = NETSCAPE_SPKI_get_pubkey(spki.get());
if (!pkey) return kj::none;
return kj::disposeWith<EVP_PKEY_free>(pkey);
}

kj::Maybe<kj::Own<BIO>> tryNewBio() {
auto bioptr = BIO_new(BIO_s_mem());
if (!bioptr) return kj::none;
return kj::disposeWith<BIO_free_all>(bioptr);
}
} // namespace

bool verifySpkac(kj::ArrayPtr<const kj::byte> input) {
// So, this is fun. SPKAC uses MD5 as the digest algorithm. This is a problem because
// using MD5 for signature verification is not allowed in FIPS mode, which means that
// although we have a working implementation here, the result of this call is always
// going to false even if the input signature is correct. So this is a bit of a dead
// end that isn't going to be super useful. Fortunately but the exportPublicKey and
// exportChallenge functions both work correctly and are useful. Unfortunately, this
// likely means users would need to implement their own verification, which sucks.
//
// Alternatively we could choose to implement our own version of the validation that
// bypasses BoringSSL's FIPS configuration. For now tho, this does end up matching
// Node.js' behavior when FIPS is enabled so I guess that's something.
ClearErrorOnReturn clearErrorOnReturn;
if (IoContext::hasCurrent()) {
IoContext::current().logWarningOnce(
"The verifySpkac function is currently of limited value in workers because "
"the SPKAC signature verification uses MD5, which is not supported in FIPS mode. "
"All workers run in FIPS mode. Accordingly, this method will currentlyu always "
"return false even if the SPKAC signature is valid. This is a known limitation.");
}
KJ_IF_SOME(spki, tryGetSpki(input)) {
KJ_IF_SOME(key, tryOwnPkey(spki)) {
return NETSCAPE_SPKI_verify(spki.get(), key.get()) > 0;
}
}
return false;
}

kj::Maybe<kj::Array<kj::byte>> exportPublicKey(kj::ArrayPtr<const kj::byte> input) {
ClearErrorOnReturn clearErrorOnReturn;
KJ_IF_SOME(spki, tryGetSpki(input)) {
KJ_IF_SOME(bio, tryNewBio()) {
KJ_IF_SOME(key, tryOwnPkey(spki)) {
if (PEM_write_bio_PUBKEY(bio.get(), key.get()) > 0) {
return toArray(bio.get());
}
}
}
}
return kj::none;
}

kj::Maybe<kj::Array<kj::byte>> exportChallenge(kj::ArrayPtr<const kj::byte> input) {
ClearErrorOnReturn clearErrorOnReturn;
KJ_IF_SOME(spki, tryGetSpki(input)) {
kj::byte* buf = nullptr;
int buf_size = ASN1_STRING_to_UTF8(&buf, spki->spkac->challenge);
if (buf_size < 0 || buf == nullptr) return kj::none;
// Pay attention to how the buffer is freed below...
return kj::arrayPtr(buf, buf_size).attach(kj::defer([buf]() {
OPENSSL_free(buf);
}));
}
return kj::none;
}
}
13 changes: 13 additions & 0 deletions src/workerd/api/crypto/spkac.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#pragma once

#include <kj/common.h>

namespace workerd::api {

bool verifySpkac(kj::ArrayPtr<const kj::byte> input);

kj::Maybe<kj::Array<kj::byte>> exportPublicKey(kj::ArrayPtr<const kj::byte> input);

kj::Maybe<kj::Array<kj::byte>> exportChallenge(kj::ArrayPtr<const kj::byte> input);

}
14 changes: 14 additions & 0 deletions src/workerd/api/node/crypto.c++
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <workerd/api/crypto/impl.h>
#include <workerd/api/crypto/kdf.h>
#include <workerd/jsg/jsg.h>
#include <workerd/api/crypto/spkac.h>

namespace workerd::api::node {

Expand Down Expand Up @@ -84,4 +85,17 @@ kj::Array<kj::byte> CryptoImpl::getScrypt(jsg::Lock& js,
return JSG_REQUIRE_NONNULL(scrypt(keylen, N, r, p, maxmem, password, salt),
Error, "Scrypt failed");
}

bool CryptoImpl::verifySpkac(kj::Array<const kj::byte> input) {
return workerd::api::verifySpkac(input);
}

kj::Maybe<kj::Array<kj::byte>> CryptoImpl::exportPublicKey(kj::Array<const kj::byte> input) {
return workerd::api::exportPublicKey(input);
}

kj::Maybe<kj::Array<kj::byte>> CryptoImpl::exportChallenge(kj::Array<const kj::byte> input) {
return workerd::api::exportChallenge(input);
}

} // namespace workerd::api::node
8 changes: 8 additions & 0 deletions src/workerd/api/node/crypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,10 @@ class CryptoImpl final: public jsg::Object {
jsg::Ref<CryptoKey> createPrivateKey(jsg::Lock& js, CreateAsymmetricKeyOptions options);
jsg::Ref<CryptoKey> createPublicKey(jsg::Lock& js, CreateAsymmetricKeyOptions options);

bool verifySpkac(kj::Array<const kj::byte> input);
kj::Maybe<kj::Array<kj::byte>> exportPublicKey(kj::Array<const kj::byte> input);
kj::Maybe<kj::Array<kj::byte>> exportChallenge(kj::Array<const kj::byte> input);

JSG_RESOURCE_TYPE(CryptoImpl) {
// DH
JSG_NESTED_TYPE(DiffieHellmanHandle);
Expand All @@ -236,6 +240,10 @@ class CryptoImpl final: public jsg::Object {
JSG_METHOD(createSecretKey);
JSG_METHOD(createPrivateKey);
JSG_METHOD(createPublicKey);
// Spkac
JSG_METHOD(verifySpkac);
JSG_METHOD(exportPublicKey);
JSG_METHOD(exportChallenge);
}
};

Expand Down
Loading

0 comments on commit ddd1542

Please sign in to comment.