js: add function to validate a cert chain (#2579)

This commit is contained in:
Maik Riechert 2021-05-14 09:42:33 +01:00 коммит произвёл GitHub
Родитель 68a483e81c
Коммит 5f24ab38b9
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
17 изменённых файлов: 411 добавлений и 40 удалений

Просмотреть файл

@ -42,6 +42,11 @@ export const digest = ccf.digest;
*/
export const isValidX509CertBundle = ccf.isValidX509CertBundle;
/**
* @inheritDoc CCF.isValidX509CertChain
*/
export const isValidX509CertChain = ccf.isValidX509CertChain;
export {
WrapAlgoParams,
AesKwpParams,

Просмотреть файл

@ -245,6 +245,12 @@ export interface CCF {
*/
isValidX509CertBundle(pem: string): boolean;
/**
* Returns whether a certificate chain is valid given a set of trusted certificates.
* The chain and trusted certificates are PEM-encoded bundles of X.509 certificates.
*/
isValidX509CertChain(chain: string, trusted: string): boolean;
rpc: {
/**
* Set whether KV writes should be applied even if the response status is not 2xx.

Просмотреть файл

@ -201,6 +201,52 @@ class CCFPolyfill implements CCF {
);
}
}
isValidX509CertChain(chain: string, trusted: string): boolean {
if (!("X509Certificate" in crypto)) {
throw new Error(
"X509 validation unsupported, Node.js version too old (< 15.6.0)"
);
}
try {
const toX509Array = (pem: string) => {
const sep = "-----END CERTIFICATE-----";
const items = pem.split(sep);
if (items.length === 1) {
return [];
}
const pems = items.slice(0, -1).map((p) => p + sep);
const arr = pems.map((pem) => new (<any>crypto).X509Certificate(pem));
return arr;
};
const certsChain = toX509Array(chain);
const certsTrusted = toX509Array(trusted);
if (certsChain.length === 0) {
throw new Error("chain cannot be empty");
}
for (let i = 0; i < certsChain.length - 1; i++) {
if (!certsChain[i].checkIssued(certsChain[i + 1])) {
throw new Error(`chain[${i}] is not issued by chain[${i + 1}]`);
}
}
for (const certChain of certsChain) {
for (const certTrusted of certsTrusted) {
if (certChain.fingerprint === certTrusted.fingerprint) {
return true;
}
if (certChain.verify(certTrusted.publicKey)) {
return true;
}
}
}
throw new Error(
"none of the chain certificates are identical to or issued by a trusted certificate"
);
} catch (e) {
console.error(`certificate chain validation failed: ${e.message}`);
return false;
}
}
}
(<any>globalThis).ccf = new CCFPolyfill();

Просмотреть файл

@ -1,5 +1,5 @@
import * as crypto from "crypto";
import * as forge from "node-forge";
import forge from "node-forge";
import { WrapAlgoParams } from "../src/global.js";
function nodeBufToArrBuf(buf: Buffer): ArrayBuffer {
@ -82,3 +82,35 @@ export function generateSelfSignedCert(): string {
const certPem = forge.pki.certificateToPem(cert);
return certPem;
}
export function generateCertChain(len: number): string[] {
const keyPairs = [];
for (let i = 0; i < len; i++) {
keyPairs.push(
crypto.generateKeyPairSync("rsa", {
modulusLength: 2048,
publicKeyEncoding: {
type: "spki",
format: "pem",
},
privateKeyEncoding: {
type: "pkcs8",
format: "pem",
},
})
);
}
const certs = [];
for (let i = 0; i < len; i++) {
const cert = forge.pki.createCertificate();
cert.publicKey = forge.pki.publicKeyFromPem(keyPairs[i].publicKey);
const signer = i < len - 1 ? keyPairs[i + 1] : keyPairs[i];
cert.sign(
forge.pki.privateKeyFromPem(signer.privateKey),
forge.md.sha256.create()
);
const certPem = forge.pki.certificateToPem(cert);
certs.push(certPem);
}
return certs;
}

Просмотреть файл

@ -7,7 +7,11 @@ import {
RsaOaepAesKwpParams,
RsaOaepParams,
} from "../src/global.js";
import { unwrapKey, generateSelfSignedCert } from "./crypto.js";
import {
unwrapKey,
generateSelfSignedCert,
generateCertChain,
} from "./crypto.js";
beforeEach(function () {
// clear KV before each test
@ -121,6 +125,27 @@ describe("polyfill", function () {
assert.isFalse(ccf.isValidX509CertBundle("garbage"));
});
});
describe("isValidX509CertChain", function (this) {
const supported = "X509Certificate" in crypto;
it("returns true for valid cert chains", function () {
if (!supported) {
this.skip();
}
const pems = generateCertChain(3);
const chain = [pems[0], pems[1]].join("\n");
const trusted = pems[2];
assert.isTrue(ccf.isValidX509CertChain(chain, trusted));
});
it("returns false for invalid cert chains", function () {
if (!supported) {
this.skip();
}
const pems = generateCertChain(3);
const chain = pems[0];
const trusted = pems[2];
assert.isFalse(ccf.isValidX509CertChain(chain, trusted));
});
});
describe("kv", function () {
it("basic", function () {
const foo = ccf.kv["foo"];

Просмотреть файл

@ -50,6 +50,11 @@ namespace crypto
throw std::invalid_argument(
fmt::format("Failed to parse certificate: {}", error_string(rc)));
}
if (cert.get()->next != nullptr)
{
throw std::invalid_argument(
"PEM string contains more than one certificate");
}
// public_key expects to have unique ownership of the context and so does
// `cert`, so we duplicate the key context here.
@ -108,52 +113,67 @@ namespace crypto
class CertificateChain
{
public:
size_t n = 0;
mbedtls_x509_crt* raw = NULL;
mbedtls_x509_crt raw;
CertificateChain(const std::vector<const Pem*>& certs)
CertificateChain()
{
if (!certs.empty())
mbedtls_x509_crt_init(&raw);
}
void add(const std::vector<const Pem*>& certs)
{
for (auto& cert : certs)
{
n = certs.size();
raw = new mbedtls_x509_crt[certs.size()];
for (size_t i = 0; i < certs.size(); i++)
int rc = mbedtls_x509_crt_parse(&raw, cert->data(), cert->size());
if (rc != 0)
{
mbedtls_x509_crt_init(&raw[i]);
throw std::runtime_error(
"Could not parse PEM certificate: " + error_string(rc));
}
}
}
for (size_t i = 0; i < certs.size(); i++)
{
auto& tc = certs[i];
int rc = mbedtls_x509_crt_parse(&raw[i], tc->data(), tc->size());
if (rc != 0)
{
throw std::runtime_error(
"Could not parse PEM certificate: " + error_string(rc));
}
}
void add(const uint8_t* der, size_t len)
{
int rc = mbedtls_x509_crt_parse_der(&raw, der, len);
if (rc != 0)
{
throw std::runtime_error(
"Could not parse DER certificate: " + error_string(rc));
}
}
~CertificateChain()
{
for (size_t i = 0; i < n; i++)
{
mbedtls_x509_crt_free(&raw[i]);
}
delete[] raw;
mbedtls_x509_crt_free(&raw);
}
};
bool Verifier_mbedTLS::verify_certificate(
const std::vector<const Pem*>& trusted_certs)
const std::vector<const Pem*>& trusted_certs,
const std::vector<const Pem*>& chain)
{
CertificateChain chain(trusted_certs);
CertificateChain trusted;
trusted.add(trusted_certs);
mbedtls_x509_crt* crt;
CertificateChain target_and_chain;
if (chain.empty())
{
// Fast-path, avoids extra parse step.
crt = cert.get();
}
else
{
target_and_chain.add(cert.get()->raw.p, cert.get()->raw.len);
target_and_chain.add(chain);
crt = &target_and_chain.raw;
}
uint32_t flags;
int rc = mbedtls_x509_crt_verify(
cert.get(), chain.raw, NULL, NULL, &flags, NULL, NULL);
crt, &trusted.raw, NULL, NULL, &flags, NULL, NULL);
return rc == 0 && flags == 0;
}

Просмотреть файл

@ -24,7 +24,8 @@ namespace crypto
virtual Pem cert_pem() override;
virtual bool verify_certificate(
const std::vector<const Pem*>& trusted_certs) override;
const std::vector<const Pem*>& trusted_certs,
const std::vector<const Pem*>& chain = {}) override;
virtual bool is_self_signed() const override;

Просмотреть файл

@ -174,6 +174,22 @@ namespace crypto
}
};
class Unique_STACK_OF_X509
{
std::unique_ptr<STACK_OF(X509), void (*)(STACK_OF(X509)*)> p;
public:
Unique_STACK_OF_X509() :
p(sk_X509_new_null(), [](auto x) { sk_X509_pop_free(x, X509_free); })
{
OpenSSL::CHECKNULL(p.get());
}
operator STACK_OF(X509) * ()
{
return p.get();
}
};
inline std::string error_string(int ec)
{
return ERR_error_string((unsigned long)ec, NULL);

Просмотреть файл

@ -89,7 +89,8 @@ namespace crypto
}
bool Verifier_OpenSSL::verify_certificate(
const std::vector<const Pem*>& trusted_certs)
const std::vector<const Pem*>& trusted_certs,
const std::vector<const Pem*>& chain)
{
Unique_X509_STORE store;
Unique_X509_STORE_CTX store_ctx;
@ -101,8 +102,37 @@ namespace crypto
CHECK1(X509_STORE_add_cert(store, tc));
}
CHECK1(X509_STORE_CTX_init(store_ctx, store, cert, NULL));
return X509_verify_cert(store_ctx) == 1;
Unique_STACK_OF_X509 chain_stack;
for (auto& pem : chain)
{
Unique_BIO certbio(*pem);
Unique_X509 cert(certbio, true);
CHECK1(sk_X509_push(chain_stack, cert));
CHECK1(X509_up_ref(cert));
}
// Allow to use intermediate CAs as trust anchors
CHECK1(X509_STORE_set_flags(store, X509_V_FLAG_PARTIAL_CHAIN));
CHECK1(X509_STORE_CTX_init(store_ctx, store, cert, chain_stack));
auto valid = X509_verify_cert(store_ctx) == 1;
if (!valid)
{
auto error = X509_STORE_CTX_get_error(store_ctx);
auto msg = X509_verify_cert_error_string(error);
LOG_DEBUG_FMT("Failed to verify certificate: {}", msg);
LOG_DEBUG_FMT("Target: {}", cert_pem().str());
for (auto pem : chain)
{
LOG_DEBUG_FMT("Chain: {}", pem->str());
}
for (auto pem : trusted_certs)
{
LOG_DEBUG_FMT("Trusted: {}", pem->str());
}
}
return valid;
}
bool Verifier_OpenSSL::is_self_signed() const

Просмотреть файл

@ -27,7 +27,8 @@ namespace crypto
virtual Pem cert_pem() override;
virtual bool verify_certificate(
const std::vector<const Pem*>& trusted_certs) override;
const std::vector<const Pem*>& trusted_certs,
const std::vector<const Pem*>& chain = {}) override;
virtual bool is_self_signed() const override;

Просмотреть файл

@ -181,10 +181,13 @@ namespace crypto
/** Verify the certificate (held internally)
* @param trusted_certs Vector of trusted certificates
* @return true if the
* @param chain Vector of ordered untrusted certificates used to
* build a chain to trusted certificates
* @return true if the verification is successfull
*/
virtual bool verify_certificate(
const std::vector<const Pem*>& trusted_certs) = 0;
const std::vector<const Pem*>& trusted_certs,
const std::vector<const Pem*>& chain = {}) = 0;
/** Indicates whether the certificate (held intenally) is self-signed */
virtual bool is_self_signed() const = 0;

Просмотреть файл

@ -139,6 +139,80 @@ namespace js
tls::CA ca(pem);
}
catch (const std::logic_error& e)
{
LOG_DEBUG_FMT("isValidX509Bundle: {}", e.what());
return JS_FALSE;
}
return JS_TRUE;
}
static std::vector<crypto::Pem> split_x509_cert_bundle(
const std::string_view& pem)
{
std::string separator("-----END CERTIFICATE-----");
std::vector<crypto::Pem> pems;
auto separator_end = 0;
auto next_separator_start = pem.find(separator);
while (next_separator_start != std::string_view::npos)
{
pems.emplace_back(std::string(
pem.substr(separator_end, next_separator_start + separator.size())));
separator_end = next_separator_start + separator.size();
next_separator_start = pem.find(separator, separator_end);
}
return pems;
}
static JSValue js_is_valid_x509_cert_chain(
JSContext* ctx, JSValueConst, int argc, JSValueConst* argv)
{
// first arg: chain (concatenated PEM certs, first cert = target)
// second arg: trusted (concatenated PEM certs)
if (argc != 2)
return JS_ThrowTypeError(
ctx, "Passed %d arguments, but expected 2", argc);
auto chain_js = argv[0];
auto trusted_js = argv[1];
void* auto_free_ptr = JS_GetContextOpaque(ctx);
js::Context& auto_free = *(js::Context*)auto_free_ptr;
auto chain_cstr = auto_free(JS_ToCString(ctx, chain_js));
if (!chain_cstr)
{
js::js_dump_error(ctx);
return JS_EXCEPTION;
}
auto trusted_cstr = auto_free(JS_ToCString(ctx, trusted_js));
if (!trusted_cstr)
{
js::js_dump_error(ctx);
return JS_EXCEPTION;
}
try
{
auto chain_vec = split_x509_cert_bundle(chain_cstr);
auto trusted_vec = split_x509_cert_bundle(trusted_cstr);
if (chain_vec.empty() || trusted_vec.empty())
throw std::logic_error(
"chain/trusted arguments must contain at least one certificate");
auto& target_pem = chain_vec[0];
std::vector<const crypto::Pem*> chain_ptr;
for (auto it = chain_vec.begin() + 1; it != chain_vec.end(); it++)
chain_ptr.push_back(&*it);
std::vector<const crypto::Pem*> trusted_ptr;
for (auto& pem : trusted_vec)
trusted_ptr.push_back(&pem);
auto verifier = crypto::make_unique_verifier(target_pem);
if (!verifier->verify_certificate(trusted_ptr, chain_ptr))
throw std::logic_error("certificate chain is invalid");
}
catch (const std::logic_error& e)
{
LOG_DEBUG_FMT("isValidX509Chain: {}", e.what());
return JS_FALSE;

Просмотреть файл

@ -992,6 +992,12 @@ namespace js
"isValidX509CertBundle",
JS_NewCFunction(
ctx, js_is_valid_x509_cert_bundle, "isValidX509CertBundle", 1));
JS_SetPropertyStr(
ctx,
ccf,
"isValidX509CertChain",
JS_NewCFunction(
ctx, js_is_valid_x509_cert_chain, "isValidX509CertChain", 2));
JS_SetPropertyStr(
ctx, ccf, "pemToId", JS_NewCFunction(ctx, js_pem_to_id, "pemToId", 1));

Просмотреть файл

@ -79,15 +79,29 @@ def generate_rsa_keypair(key_size: int) -> Tuple[str, str]:
return priv_pem, pub_pem
def generate_cert(priv_key_pem: str, cn="dummy") -> str:
def generate_cert(
priv_key_pem: str, cn="dummy", issuer_priv_key_pem=None, issuer_cn=None, ca=False
) -> str:
if issuer_priv_key_pem is None:
issuer_priv_key_pem = priv_key_pem
if issuer_cn is None:
issuer_cn = cn
priv = load_pem_private_key(priv_key_pem.encode("ascii"), None, default_backend())
pub = priv.public_key()
subject = issuer = x509.Name(
issuer_priv = load_pem_private_key(
issuer_priv_key_pem.encode("ascii"), None, default_backend()
)
subject = x509.Name(
[
x509.NameAttribute(NameOID.COMMON_NAME, cn),
]
)
cert = (
issuer = x509.Name(
[
x509.NameAttribute(NameOID.COMMON_NAME, issuer_cn),
]
)
builder = (
x509.CertificateBuilder()
.subject_name(subject)
.issuer_name(issuer)
@ -95,8 +109,14 @@ def generate_cert(priv_key_pem: str, cn="dummy") -> str:
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.datetime.utcnow())
.not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=10))
.sign(priv, hashes.SHA256(), default_backend())
)
if ca:
builder = builder.add_extension(
x509.BasicConstraints(ca=True, path_length=None),
critical=True,
)
cert = builder.sign(issuer_priv, hashes.SHA256(), default_backend())
return cert.public_bytes(Encoding.PEM).decode("ascii")

Просмотреть файл

@ -357,6 +357,38 @@ def test_npm_app(network, args):
r = c.post("/app/isValidX509CertBundle", "garbage")
assert not r.body.json(), r.body
priv_key_pem1, _ = infra.crypto.generate_rsa_keypair(2048)
pem1 = infra.crypto.generate_cert(priv_key_pem1, cn="1", ca=True)
priv_key_pem2, _ = infra.crypto.generate_rsa_keypair(2048)
pem2 = infra.crypto.generate_cert(
priv_key_pem2,
cn="2",
ca=True,
issuer_priv_key_pem=priv_key_pem1,
issuer_cn="1",
)
priv_key_pem3, _ = infra.crypto.generate_rsa_keypair(2048)
pem3 = infra.crypto.generate_cert(
priv_key_pem3, cn="3", issuer_priv_key_pem=priv_key_pem2, issuer_cn="2"
)
# validates chains with target being trusted directly
r = c.post("/app/isValidX509CertChain", {"chain": pem3, "trusted": pem3})
assert r.body.json(), r.body
# validates chains without intermediates
r = c.post("/app/isValidX509CertChain", {"chain": pem2, "trusted": pem1})
assert r.body.json(), r.body
# validates chains with intermediates
r = c.post(
"/app/isValidX509CertChain", {"chain": pem3 + "\n" + pem2, "trusted": pem1}
)
assert r.body.json(), r.body
# validates partial chains (pem2 is an intermediate)
r = c.post("/app/isValidX509CertChain", {"chain": pem3, "trusted": pem2})
assert r.body.json(), r.body
# fails to reach trust anchor
r = c.post("/app/isValidX509CertChain", {"chain": pem3, "trusted": pem1})
assert not r.body.json(), r.body
r = c.get("/node/quotes/self")
primary_quote_info = r.body.json()
if not primary_quote_info["raw"]:

Просмотреть файл

@ -273,6 +273,48 @@
}
}
},
"/isValidX509CertChain": {
"post": {
"js_module": "endpoints/crypto.js",
"js_function": "isValidX509CertChain",
"forwarding_required": "always",
"execute_outside_consensus": "never",
"authn_policies": ["user_cert"],
"mode": "readonly",
"openapi": {
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"properties": {
"chain": {
"type": "string"
},
"trusted": {
"type": "string"
}
},
"type": "object"
}
}
}
},
"responses": {
"200": {
"description": "Ok",
"content": {
"application/json": {
"schema": {
"type": "boolean"
}
}
}
}
}
}
}
},
"/partition": {
"post": {
"js_module": "endpoints/partition.js",

Просмотреть файл

@ -120,6 +120,18 @@ export function isValidX509CertBundle(
return { body: ccfcrypto.isValidX509CertBundle(pem) };
}
interface IsValidX509CertChainRequest {
chain: string;
trusted: string;
}
export function isValidX509CertChain(
request: ccfapp.Request<IsValidX509CertChainRequest>
): ccfapp.Response<boolean> {
const { chain, trusted } = request.body.json();
return { body: ccfcrypto.isValidX509CertChain(chain, trusted) };
}
function b64ToBuf(b64: string): ArrayBuffer {
return Base64.toUint8Array(b64).buffer;
}