\ Copyright (c) 2016 Thomas Pornin <pornin@bolet.org>
\
\ 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.
preamble {
#include "inner.h"
/*
* Implementation Notes
* --------------------
*
* The C code pushes the data by chunks; all decoding is done in the
* T0 code. The cert_length value is set to the certificate length when
* a new certificate is started; the T0 code picks it up as outer limit,
* and decoding functions use it to ensure that no attempt is made at
* reading past it. The T0 code also checks that once the certificate is
* decoded, there are no trailing bytes.
*
* The T0 code sets cert_length to 0 when the certificate is fully
* decoded.
*
* The C code must still perform two checks:
*
* -- If the certificate length is 0, then the T0 code will not be
* invoked at all. This invalid condition must thus be reported by the
* C code.
*
* -- When reaching the end of certificate, the C code must verify that
* the certificate length has been set to 0, thereby signaling that
* the T0 code properly decoded a certificate.
*
* Processing of a chain works in the following way:
*
* -- The error flag is set to a non-zero value when validation is
* finished. The value is either BR_ERR_X509_OK (validation is
* successful) or another non-zero error code. When a non-zero error
* code is obtained, the remaining bytes in the current certificate and
* the subsequent certificates (if any) are completely ignored.
*
* -- Each certificate is decoded in due course, with the following
* "interesting points":
*
* -- Start of the TBS: the multihash engine is reset and activated.
*
* -- Start of the issuer DN: the secondary hash engine is started,
* to process the encoded issuer DN.
*
* -- End of the issuer DN: the secondary hash engine is stopped. The
* resulting hash value is computed and then copied into the
* next_dn_hash[] buffer.
*
* -- Start of the subject DN: the secondary hash engine is started,
* to process the encoded subject DN.
*
* -- For the EE certificate only: the Common Name, if any, is matched
* against the expected server name.
*
* -- End of the subject DN: the secondary hash engine is stopped. The
* resulting hash value is computed into the pad. It is then processed:
*
* -- If this is the EE certificate, then the hash is ignored
* (except for direct trust processing, see later; the hash is
* simply left in current_dn_hash[]).
*
* -- Otherwise, the hashed subject DN is compared with the saved
* hash value (in saved_dn_hash[]). They must match.
*
* Either way, the next_dn_hash[] value is then copied into the
* saved_dn_hash[] value. Thus, at that point, saved_dn_hash[]
* contains the hash of the issuer DN for the current certificate,
* and current_dn_hash[] contains the hash of the subject DN for the
* current certificate.
*
* -- Public key: it is decoded into the cert_pkey[] buffer. Unknown
* key types are reported at that point.
*
* -- If this is the EE certificate, then the key type is compared
* with the expected key type (initialization parameter). The public
* key data is copied to ee_pkey_data[]. The key and hashed subject
* DN are also compared with the "direct trust" keys; if the key
* and DN are matched, then validation ends with a success.
*
* -- Otherwise, the saved signature (cert_sig[]) is verified
* against the saved TBS hash (tbs_hash[]) and that freshly
* decoded public key. Failure here ends validation with an error.
*
* -- Extensions: extension values are processed in due order.
*
* -- Basic Constraints: for all certificates except EE, must be
* present, indicate a CA, and have a path legnth compatible with
* the chain length so far.
*
* -- Key Usage: for the EE, if present, must allow signatures
* or encryption/key exchange, as required for the cipher suite.
* For non-EE, if present, must have the "certificate sign" bit.
*
* -- Subject Alt Name: for the EE, dNSName names are matched
* against the server name. Ignored for non-EE.
*
* -- Authority Key Identifier, Subject Key Identifier, Issuer
* Alt Name, Subject Directory Attributes, CRL Distribution Points
* Freshest CRL, Authority Info Access and Subject Info Access
* extensions are always ignored: they either contain only
* informative data, or they relate to revocation processing, which
* we explicitly do not support.
*
* -- All other extensions are ignored if non-critical. If a
* critical extension other than the ones above is encountered,
* then a failure is reported.
*
* -- End of the TBS: the multihash engine is stopped.
*
* -- Signature algorithm: the signature algorithm on the
* certificate is decoded. A failure is reported if that algorithm
* is unknown. The hashed TBS corresponding to the signature hash
* function is computed and stored in tbs_hash[] (if not supported,
* then a failure is reported). The hash OID and length are stored
* in cert_sig_hash_oid and cert_sig_hash_len.
*
* -- Signature value: the signature value is copied into the
* cert_sig[] array.
*
* -- Certificate end: the hashed issuer DN (saved_dn_hash[]) is
* looked up in the trust store (CA trust anchors only); for all
* that match, the signature (cert_sig[]) is verified against the
* anchor public key (hashed TBS is in tbs_hash[]). If one of these
* signatures is valid, then validation ends with a success.
*
* -- If the chain end is reached without obtaining a validation success,
* then validation is reported as failed.
*/
#if BR_USE_UNIX_TIME
#include <time.h>
#endif
#if BR_USE_WIN32_TIME
#include <windows.h>
#endif
/*
* The T0 compiler will produce these prototypes declarations in the
* header.
*
void br_x509_minimal_init_main(void *ctx);
void br_x509_minimal_run(void *ctx);
*/
/* see bearssl_x509.h */
void
br_x509_minimal_init(br_x509_minimal_context *ctx,
const br_hash_class *dn_hash_impl,
const br_x509_trust_anchor *trust_anchors, size_t trust_anchors_num)
{
memset(ctx, 0, sizeof *ctx);
ctx->vtable = &br_x509_minimal_vtable;
ctx->dn_hash_impl = dn_hash_impl;
ctx->trust_anchors = trust_anchors;
ctx->trust_anchors_num = trust_anchors_num;
}
static void
xm_start_chain(const br_x509_class **ctx, const char *server_name)
{
br_x509_minimal_context *cc;
size_t u;
cc = (br_x509_minimal_context *)(void *)ctx;
for (u = 0; u < cc->num_name_elts; u ++) {
cc->name_elts[u].status = 0;
cc->name_elts[u].buf[0] = 0;
}
memset(&cc->pkey, 0, sizeof cc->pkey);
cc->num_certs = 0;
cc->err = 0;
cc->cpu.dp = cc->dp_stack;
cc->cpu.rp = cc->rp_stack;
br_x509_minimal_init_main(&cc->cpu);
if (server_name == NULL || *server_name == 0) {
cc->server_name = NULL;
} else {
cc->server_name = server_name;
}
}
static void
xm_start_cert(const br_x509_class **ctx, uint32_t length)
{
br_x509_minimal_context *cc;
cc = (br_x509_minimal_context *)(void *)ctx;
if (cc->err != 0) {
return;
}
if (length == 0) {
cc->err = BR_ERR_X509_TRUNCATED;
return;
}
cc->cert_length = length;
}
static void
xm_append(const br_x509_class **ctx, const unsigned char *buf, size_t len)
{
br_x509_minimal_context *cc;
cc = (br_x509_minimal_context *)(void *)ctx;
if (cc->err != 0) {
return;
}
cc->hbuf = buf;
cc->hlen = len;
br_x509_minimal_run(&cc->cpu);
}
static void
xm_end_cert(const br_x509_class **ctx)
{
br_x509_minimal_context *cc;
cc = (br_x509_minimal_context *)(void *)ctx;
if (cc->err == 0 && cc->cert_length != 0) {
cc->err = BR_ERR_X509_TRUNCATED;
}
cc->num_certs ++;
}
static unsigned
xm_end_chain(const br_x509_class **ctx)
{
br_x509_minimal_context *cc;
cc = (br_x509_minimal_context *)(void *)ctx;
if (cc->err == 0) {
if (cc->num_certs == 0) {
cc->err = BR_ERR_X509_EMPTY_CHAIN;
} else {
cc->err = BR_ERR_X509_NOT_TRUSTED;
}
} else if (cc->err == BR_ERR_X509_OK) {
return 0;
}
return (unsigned)cc->err;
}
static const br_x509_pkey *
xm_get_pkey(const br_x509_class *const *ctx, unsigned *usages)
{
br_x509_minimal_context *cc;
cc = (br_x509_minimal_context *)(void *)ctx;
if (cc->err == BR_ERR_X509_OK
|| cc->err == BR_ERR_X509_NOT_TRUSTED)
{
if (usages != NULL) {
*usages = cc->key_usages;
}
return &((br_x509_minimal_context *)(void *)ctx)->pkey;
} else {
return NULL;
}
}
/* see bearssl_x509.h */
const br_x509_class br_x509_minimal_vtable = {
sizeof(br_x509_minimal_context),
xm_start_chain,
xm_start_cert,
xm_append,
xm_end_cert,
xm_end_chain,
xm_get_pkey
};
#define CTX ((br_x509_minimal_context *)(void *)((unsigned char *)t0ctx - offsetof(br_x509_minimal_context, cpu)))
#define CONTEXT_NAME br_x509_minimal_context
#define DNHASH_LEN ((CTX->dn_hash_impl->desc >> BR_HASHDESC_OUT_OFF) & BR_HASHDESC_OUT_MASK)
/*
* Hash a DN (from a trust anchor) into the provided buffer. This uses the
* DN hash implementation and context structure from the X.509 engine
* context.
*/
static void
hash_dn(br_x509_minimal_context *ctx, const void *dn, size_t len,
unsigned char *out)
{
ctx->dn_hash_impl->init(&ctx->dn_hash.vtable);
ctx->dn_hash_impl->update(&ctx->dn_hash.vtable, dn, len);
ctx->dn_hash_impl->out(&ctx->dn_hash.vtable, out);
}
/*
* Compare two big integers for equality. The integers use unsigned big-endian
* encoding; extra leading bytes (of value 0) are allowed.
*/
static int
eqbigint(const unsigned char *b1, size_t len1,
const unsigned char *b2, size_t len2)
{
while (len1 > 0 && *b1 == 0) {
b1 ++;
len1 --;
}
while (len2 > 0 && *b2 == 0) {
b2 ++;
len2 --;
}
if (len1 != len2) {
return 0;
}
return memcmp(b1, b2, len1) == 0;
}
/*
* Compare two strings for equality, in a case-insensitive way. This
* function handles casing only for ASCII letters.
*/
static int
eqnocase(const void *s1, const void *s2, size_t len)
{
const unsigned char *buf1, *buf2;
buf1 = s1;
buf2 = s2;
while (len -- > 0) {
int x1, x2;
x1 = *buf1 ++;
x2 = *buf2 ++;
if (x1 >= 'A' && x1 <= 'Z') {
x1 += 'a' - 'A';
}
if (x2 >= 'A' && x2 <= 'Z') {
x2 += 'a' - 'A';
}
if (x1 != x2) {
return 0;
}
}
return 1;
}
static int verify_signature(br_x509_minimal_context *ctx,
const br_x509_pkey *pk);
}
postamble {
/*
* Verify the signature on the certificate with the provided public key.
* This function checks the public key type with regards to the expected
* type. Returned value is either 0 on success, or a non-zero error code.
*/
static int
verify_signature(br_x509_minimal_context *ctx, const br_x509_pkey *pk)
{
int kt;
kt = ctx->cert_signer_key_type;
if ((pk->key_type & 0x0F) != kt) {
return BR_ERR_X509_WRONG_KEY_TYPE;
}
switch (kt) {
unsigned char tmp[64];
case BR_KEYTYPE_RSA:
if (ctx->irsa == 0) {
return BR_ERR_X509_UNSUPPORTED;
}
if (!ctx->irsa(ctx->cert_sig, ctx->cert_sig_len,
&t0_datablock[ctx->cert_sig_hash_oid],
ctx->cert_sig_hash_len, &pk->key.rsa, tmp))
{
return BR_ERR_X509_BAD_SIGNATURE;
}
if (memcmp(ctx->tbs_hash, tmp, ctx->cert_sig_hash_len) != 0) {
return BR_ERR_X509_BAD_SIGNATURE;
}
return 0;
case BR_KEYTYPE_EC:
if (ctx->iecdsa == 0) {
return BR_ERR_X509_UNSUPPORTED;
}
if (!ctx->iecdsa(ctx->iec, ctx->tbs_hash,
ctx->cert_sig_hash_len, &pk->key.ec,
ctx->cert_sig, ctx->cert_sig_len))
{
return BR_ERR_X509_BAD_SIGNATURE;
}
return 0;
default:
return BR_ERR_X509_UNSUPPORTED;
}
}
}
cc: read8-low ( -- x ) {
if (CTX->hlen == 0) {
T0_PUSHi(-1);
} else {
unsigned char x = *CTX->hbuf ++;
if (CTX->do_mhash) {
br_multihash_update(&CTX->mhash, &x, 1);
}
if (CTX->do_dn_hash) {
CTX->dn_hash_impl->update(&CTX->dn_hash.vtable, &x, 1);
}
CTX->hlen --;
T0_PUSH(x);
}
}
addr: cert_length
addr: num_certs
cc: read-blob-inner ( addr len -- addr len ) {
uint32_t len = T0_POP();
uint32_t addr = T0_POP();
size_t clen = CTX->hlen;
if (clen > len) {
clen = (size_t)len;
}
if (addr != 0) {
memcpy((unsigned char *)CTX + addr, CTX->hbuf, clen);
}
if (CTX->do_mhash) {
br_multihash_update(&CTX->mhash, CTX->hbuf, clen);
}
if (CTX->do_dn_hash) {
CTX->dn_hash_impl->update(
&CTX->dn_hash.vtable, CTX->hbuf, clen);
}
CTX->hbuf += clen;
CTX->hlen -= clen;
T0_PUSH(addr + clen);
T0_PUSH(len - clen);
}
\ Compute the TBS hash, using the provided hash ID. The hash value is
\ written in the tbs_hash[] array, and the hash length is returned. If
\ the requested hash function is not supported, then 0 is returned.
cc: compute-tbs-hash ( id -- hashlen ) {
int id = T0_POPi();
size_t len;
len = br_multihash_out(&CTX->mhash, id, CTX->tbs_hash);
T0_PUSH(len);
}
\ Push true (-1) if no server name is expected in the EE certificate.
cc: zero-server-name ( -- bool ) {
T0_PUSHi(-(CTX->server_name == NULL));
}
addr: key_usages
addr: cert_sig
addr: cert_sig_len
addr: cert_signer_key_type
addr: cert_sig_hash_oid
addr: cert_sig_hash_len
addr: tbs_hash
addr: min_rsa_size
\ Start TBS hash computation. The hash functions are reinitialised.
cc: start-tbs-hash ( -- ) {
br_multihash_init(&CTX->mhash);
CTX->do_mhash = 1;
}
\ Stop TBS hash computation.
cc: stop-tbs-hash ( -- ) {
CTX->do_mhash = 0;
}
\ Start DN hash computation.
cc: start-dn-hash ( -- ) {
CTX->dn_hash_impl->init(&CTX->dn_hash.vtable);
CTX->do_dn_hash = 1;
}
\ Terminate DN hash computation and write the DN hash into the
\ current_dn_hash buffer.
cc: compute-dn-hash ( -- ) {
CTX->dn_hash_impl->out(&CTX->dn_hash.vtable, CTX->current_dn_hash);
CTX->do_dn_hash = 0;
}
\ Get the length of hash values obtained with the DN hasher.
cc: dn-hash-length ( -- len ) {
T0_PUSH(DNHASH_LEN);
}
\ Copy data between two areas in the context.
cc: blobcopy ( addr-dst addr-src len -- ) {
size_t len = T0_POP();
unsigned char *src = (unsigned char *)CTX + T0_POP();
unsigned char *dst = (unsigned char *)CTX + T0_POP();
memcpy(dst, src, len);
}
addr: current_dn_hash
addr: next_dn_hash
addr: saved_dn_hash
\ Read a DN, hashing it into current_dn_hash. The DN contents are not
\ inspected (only the outer tag, for SEQUENCE, is checked).
: read-DN ( lim -- lim )
start-dn-hash
read-sequence-open skip-close-elt
compute-dn-hash ;
cc: offset-name-element ( san -- n ) {
unsigned san = T0_POP();
size_t u;
for (u = 0; u < CTX->num_name_elts; u ++) {
if (CTX->name_elts[u].status == 0) {
const unsigned char *oid;
size_t len, off;
oid = CTX->name_elts[u].oid;
if (san) {
if (oid[0] != 0 || oid[1] != 0) {
continue;
}
off = 2;
} else {
off = 0;
}
len = oid[off];
if (len != 0 && len == CTX->pad[0]
&& memcmp(oid + off + 1,
CTX->pad + 1, len) == 0)
{
T0_PUSH(u);
T0_RET();
}
}
}
T0_PUSHi(-1);
}
cc: copy-name-element ( bool offbuf -- ) {
size_t len;
int32_t off = T0_POPi();
int ok = T0_POPi();
if (off >= 0) {
br_name_element *ne = &CTX->name_elts[off];
if (ok) {
len = CTX->pad[0];
if (len < ne->len) {
memcpy(ne->buf, CTX->pad + 1, len);
ne->buf[len] = 0;
ne->status = 1;
} else {
ne->status = -1;
}
} else {
ne->status = -1;
}
}
}
cc: copy-name-SAN ( bool tag -- ) {
unsigned tag = T0_POP();
unsigned ok = T0_POP();
size_t u, len;
len = CTX->pad[0];
for (u = 0; u < CTX->num_name_elts; u ++) {
br_name_element *ne;
ne = &CTX->name_elts[u];
if (ne->status == 0 && ne->oid[0] == 0 && ne->oid[1] == tag) {
if (ok && ne->len > len) {
memcpy(ne->buf, CTX->pad + 1, len);
ne->buf[len] = 0;
ne->status = 1;
} else {
ne->status = -1;
}
break;
}
}
}
\ Read a value, decoding string types. If the string type is recognised
\ and the value could be converted to UTF-8 into the pad, then true (-1)
\ is returned; in all other cases, false (0) is returned. Either way, the
\ object is consumed.
: read-string ( lim -- lim bool )
read-tag case
\ UTF8String
12 of check-primitive read-value-UTF8 endof
\ NumericString
18 of check-primitive read-value-latin1 endof
\ PrintableString
19 of check-primitive read-value-latin1 endof
\ TeletexString
20 of check-primitive read-value-latin1 endof
\ IA5String
22 of check-primitive read-value-latin1 endof
\ BMPString
30 of check-primitive read-value-UTF16 endof
2drop read-length-skip 0 0
endcase ;
\ Read a DN for the EE. The normalized DN hash is computed and stored in the
\ current_dn_hash.
\ Name elements are gathered. Also, the Common Name is matched against the
\ intended server name.
\ Returned value is true (-1) if the CN matches the intended server name,
\ false (0) otherwise.
: read-DN-EE ( lim -- lim bool )
\ Flag will be set to true if there is a CN and it matches the
\ intended server name.
0 { eename-matches }
\ Activate DN hashing.
start-dn-hash
\ Parse the DN structure: it is a SEQUENCE of SET of
\ AttributeTypeAndValue. Each AttributeTypeAndValue is a
\ SEQUENCE { OBJECT IDENTIFIER, ANY }.
read-sequence-open
begin
dup while
read-tag 0x11 check-tag-constructed read-length-open-elt
dup ifnot ERR_X509_BAD_DN fail then
begin
dup while
read-sequence-open
\ Read the OID. If the OID could not be read (too
\ long) then the first pad byte will be 0.
read-OID drop
\ If it is the Common Name then we'll need to
\ match it against the intended server name (if
\ applicable).
id-at-commonName eqOID { isCN }
\ Get offset for reception buffer for that element
\ (or -1).
0 offset-name-element { offbuf }
\ Try to read the value as a string.
read-string
\ If the value could be decoded as a string,
\ copy it and/or match it, as appropriate.
dup isCN and if
match-server-name if
-1 >eename-matches
then
then
offbuf copy-name-element
\ Close the SEQUENCE
close-elt
repeat
close-elt
repeat
close-elt
\ Compute DN hash and deactivate DN hashing.
compute-dn-hash
\ Return the CN match flag.
eename-matches ;
\ Get the validation date and time from the context or system.
cc: get-system-date ( -- days seconds ) {
if (CTX->days == 0 && CTX->seconds == 0) {
#if BR_USE_UNIX_TIME
time_t x = time(NULL);
T0_PUSH((uint32_t)(x / 86400) + 719528);
T0_PUSH((uint32_t)(x % 86400));
#elif BR_USE_WIN32_TIME
FILETIME ft;
uint64_t x;
GetSystemTimeAsFileTime(&ft);
x = ((uint64_t)ft.dwHighDateTime << 32)
+ (uint64_t)ft.dwLowDateTime;
x = (x / 10000000);
T0_PUSH((uint32_t)(x / 86400) + 584754);
T0_PUSH((uint32_t)(x % 86400));
#else
CTX->err = BR_ERR_X509_TIME_UNKNOWN;
T0_CO();
#endif
} else {
T0_PUSH(CTX->days);
T0_PUSH(CTX->seconds);
}
}
\ Compare two dates (days+seconds) together.
: before ( days1 seconds1 days2 seconds2 -- bool )
{ d1 s1 d2 s2 }
d1 d2 = if s1 s2 < else d1 d2 < then ;
: after ( days1 seconds1 days2 seconds2 -- bool )
swap2 before ;
\ Swap the top two elements with the two elements immediately below.
: swap2 ( a b c d -- c d a b )
3 roll 3 roll ;
\ Match the name in the pad with the expected server name. Returned value
\ is true (-1) on match, false (0) otherwise. If there is no expected
\ server name, then 0 is returned.
\ Match conditions: either an exact match (case insensitive), or a
\ wildcard match, if the found name starts with "*.". We only match a
\ starting wildcard, and only against a complete DN name component.
cc: match-server-name ( -- bool ) {
size_t n1, n2;
if (CTX->server_name == NULL) {
T0_PUSH(0);
T0_RET();
}
n1 = strlen(CTX->server_name);
n2 = CTX->pad[0];
if (n1 == n2 && eqnocase(&CTX->pad[1], CTX->server_name, n1)) {
T0_PUSHi(-1);
T0_RET();
}
if (n2 >= 2 && CTX->pad[1] == '*' && CTX->pad[2] == '.') {
size_t u;
u = 0;
while (u < n1 && CTX->server_name[u] != '.') {
u ++;
}
u ++;
n1 -= u;
if ((n2 - 2) == n1
&& eqnocase(&CTX->pad[3], CTX->server_name + u, n1))
{
T0_PUSHi(-1);
T0_RET();
}
}
T0_PUSH(0);
}
\ Get the address and length for the pkey_data buffer.
: addr-len-pkey_data ( -- addr len )
CX 0 8191 { offsetof(br_x509_minimal_context, pkey_data) }
CX 0 8191 { BR_X509_BUFSIZE_KEY } ;
\ Copy the EE public key to the permanent buffer (RSA).
cc: copy-ee-rsa-pkey ( nlen elen -- ) {
size_t elen = T0_POP();
size_t nlen = T0_POP();
memcpy(CTX->ee_pkey_data, CTX->pkey_data, nlen + elen);
CTX->pkey.key_type = BR_KEYTYPE_RSA;
CTX->pkey.key.rsa.n = CTX->ee_pkey_data;
CTX->pkey.key.rsa.nlen = nlen;
CTX->pkey.key.rsa.e = CTX->ee_pkey_data + nlen;
CTX->pkey.key.rsa.elen = elen;
}
\ Copy the EE public key to the permanent buffer (EC).
cc: copy-ee-ec-pkey ( curve qlen -- ) {
size_t qlen = T0_POP();
uint32_t curve = T0_POP();
memcpy(CTX->ee_pkey_data, CTX->pkey_data, qlen);
CTX->pkey.key_type = BR_KEYTYPE_EC;
CTX->pkey.key.ec.curve = curve;
CTX->pkey.key.ec.q = CTX->ee_pkey_data;
CTX->pkey.key.ec.qlen = qlen;
}
\ Check whether the current certificate (EE) is directly trusted.
cc: check-direct-trust ( -- ) {
size_t u;
for (u = 0; u < CTX->trust_anchors_num; u ++) {
const br_x509_trust_anchor *ta;
unsigned char hashed_DN[64];
int kt;
ta = &CTX->trust_anchors[u];
if (ta->flags & BR_X509_TA_CA) {
continue;
}
hash_dn(CTX, ta->dn.data, ta->dn.len, hashed_DN);
if (memcmp(hashed_DN, CTX->current_dn_hash, DNHASH_LEN)) {
continue;
}
kt = CTX->pkey.key_type;
if ((ta->pkey.key_type & 0x0F) != kt) {
continue;
}
switch (kt) {
case BR_KEYTYPE_RSA:
if (!eqbigint(CTX->pkey.key.rsa.n,
CTX->pkey.key.rsa.nlen,
ta->pkey.key.rsa.n,
ta->pkey.key.rsa.nlen)
|| !eqbigint(CTX->pkey.key.rsa.e,
CTX->pkey.key.rsa.elen,
ta->pkey.key.rsa.e,
ta->pkey.key.rsa.elen))
{
continue;
}
break;
case BR_KEYTYPE_EC:
if (CTX->pkey.key.ec.curve != ta->pkey.key.ec.curve
|| CTX->pkey.key.ec.qlen != ta->pkey.key.ec.qlen
|| memcmp(CTX->pkey.key.ec.q,
ta->pkey.key.ec.q,
ta->pkey.key.ec.qlen) != 0)
{
continue;
}
break;
default:
continue;
}
/*
* Direct trust match!
*/
CTX->err = BR_ERR_X509_OK;
T0_CO();
}
}
\ Check the signature on the certificate with regards to all trusted CA.
\ We use the issuer hash (in saved_dn_hash[]) as CA identifier.
cc: check-trust-anchor-CA ( -- ) {
size_t u;
for (u = 0; u < CTX->trust_anchors_num; u ++) {
const br_x509_trust_anchor *ta;
unsigned char hashed_DN[64];
ta = &CTX->trust_anchors[u];
if (!(ta->flags & BR_X509_TA_CA)) {
continue;
}
hash_dn(CTX, ta->dn.data, ta->dn.len, hashed_DN);
if (memcmp(hashed_DN, CTX->saved_dn_hash, DNHASH_LEN)) {
continue;
}
if (verify_signature(CTX, &ta->pkey) == 0) {
CTX->err = BR_ERR_X509_OK;
T0_CO();
}
}
}
\ Verify RSA signature. This uses the public key that was just decoded
\ into CTX->pkey_data; the modulus and exponent length are provided as
\ parameters. The resulting hash value is compared with the one in
\ tbs_hash. Returned value is 0 on success, or a non-zero error code.
cc: do-rsa-vrfy ( nlen elen -- err ) {
size_t elen = T0_POP();
size_t nlen = T0_POP();
br_x509_pkey pk;
pk.key_type = BR_KEYTYPE_RSA;
pk.key.rsa.n = CTX->pkey_data;
pk.key.rsa.nlen = nlen;
pk.key.rsa.e = CTX->pkey_data + nlen;
pk.key.rsa.elen = elen;
T0_PUSH(verify_signature(CTX, &pk));
}
\ Verify ECDSA signature. This uses the public key that was just decoded
\ into CTX->pkey_dayta; the curve ID and public point length are provided
\ as parameters. The hash value in tbs_hash is used. Returned value is 0
\ on success, or non-zero error code.
cc: do-ecdsa-vrfy ( curve qlen -- err ) {
size_t qlen = T0_POP();
int curve = T0_POP();
br_x509_pkey pk;
pk.key_type = BR_KEYTYPE_EC;
pk.key.ec.curve = curve;
pk.key.ec.q = CTX->pkey_data;
pk.key.ec.qlen = qlen;
T0_PUSH(verify_signature(CTX, &pk));
}
cc: print-bytes ( addr len -- ) {
extern int printf(const char *fmt, ...);
size_t len = T0_POP();
unsigned char *buf = (unsigned char *)CTX + T0_POP();
size_t u;
for (u = 0; u < len; u ++) {
printf("%02X", buf[u]);
}
}
cc: printOID ( -- ) {
extern int printf(const char *fmt, ...);
size_t u, len;
len = CTX->pad[0];
if (len == 0) {
printf("*");
T0_RET();
}
printf("%u.%u", CTX->pad[1] / 40, CTX->pad[1] % 40);
u = 2;
while (u <= len) {
unsigned long ul;
ul = 0;
for (;;) {
int x;
if (u > len) {
printf("BAD");
T0_RET();
}
x = CTX->pad[u ++];
ul = (ul << 7) + (x & 0x7F);
if (!(x & 0x80)) {
break;
}
}
printf(".%lu", ul);
}
}
\ Extensions with specific processing.
OID: basicConstraints 2.5.29.19
OID: keyUsage 2.5.29.15
OID: subjectAltName 2.5.29.17
OID: certificatePolicies 2.5.29.32
\ Policy qualifier "pointer to CPS"
OID: id-qt-cps 1.3.6.1.5.5.7.2.1
\ Extensions which are ignored when encountered, even if critical.
OID: authorityKeyIdentifier 2.5.29.35
OID: subjectKeyIdentifier 2.5.29.14
OID: issuerAltName 2.5.29.18
OID: subjectDirectoryAttributes 2.5.29.9
OID: crlDistributionPoints 2.5.29.31
OID: freshestCRL 2.5.29.46
OID: authorityInfoAccess 1.3.6.1.5.5.7.1.1
OID: subjectInfoAccess 1.3.6.1.5.5.7.1.11
\ Process a Basic Constraints extension. This should be called only if
\ the certificate is not the EE. We check that the extension contains
\ the "CA" flag, and that the path length, if specified, is compatible
\ with the current chain length.
: process-basicConstraints ( lim -- lim )
read-sequence-open
read-tag-or-end
dup 0x01 = if
read-boolean ifnot ERR_X509_NOT_CA fail then
read-tag-or-end
else
ERR_X509_NOT_CA fail
then
dup 0x02 = if
drop check-primitive read-small-int-value
addr-num_certs get32 1- < if ERR_X509_NOT_CA fail then
read-tag-or-end
then
-1 <> if ERR_X509_UNEXPECTED fail then
drop
close-elt
;
\ Process a Key Usage extension.
\ For the EE certificate:
\ -- if the key usage contains keyEncipherment (2), dataEncipherment (3)
\ or keyAgreement (4), then the "key exchange" usage is allowed;
\ -- if the key usage contains digitalSignature (0) or nonRepudiation (1),
\ then the "signature" usage is allowed.
\ For CA certificates, the extension must contain keyCertSign (5).
: process-keyUsage ( lim ee -- lim )
{ ee }
\ Read tag for the BIT STRING and open it.
read-tag 0x03 check-tag-primitive
read-length-open-elt
\ First byte indicates number of ignored bits in the last byte. It
\ must be between 0 and 7.
read8 { ign }
ign 7 > if ERR_X509_UNEXPECTED fail then
\ Depending on length, we have either 0, 1 or more bytes to read.
dup case
0 of ERR_X509_FORBIDDEN_KEY_USAGE fail endof
1 of read8 ign >> ign << endof
drop read8 0
endcase
\ Check bits.
ee if
\ EE: get usages.
0
over 0x38 and if 0x10 or then
swap 0xC0 and if 0x20 or then
addr-key_usages set8
else
\ Not EE: keyCertSign must be set.
0x04 and ifnot ERR_X509_FORBIDDEN_KEY_USAGE fail then
then
\ We don't care about subsequent bytes.
skip-close-elt ;
\ Process a Certificate Policies extension.
\
\ Since we don't actually support full policies processing, this function
\ only checks that the extension contents can be safely ignored. Indeed,
\ we don't validate against a specific set of policies (in RFC 5280
\ terminology, user-initial-policy-set only contains the special value
\ any-policy). Moreover, we don't support policy constraints (if a
\ critical Policy Constraints extension is encountered, the validation
\ will fail). Therefore, we can safely ignore the contents of this
\ extension, except if it is critical AND one of the policy OID has a
\ qualifier which is distinct from id-qt-cps (because id-qt-cps is
\ specially designated by RFC 5280 has having no mandated action).
\
\ This function is called only if the extension is critical.
: process-certPolicies ( lim -- lim )
\ Extension value is a SEQUENCE OF PolicyInformation.
read-sequence-open
begin dup while
\ PolicyInformation ::= SEQUENCE {
\ policyIdentifier OBJECT IDENTIFIER,
\ policyQualifiers SEQUENCE OF PolicyQualifierInfo OPTIONAL
\ }
read-sequence-open
read-OID drop
dup if
read-sequence-open
begin dup while
\ PolicyQualifierInfo ::= SEQUENCE {
\ policyQualifierId OBJECT IDENTIFIER,
\ qualifier ANY
\ }
read-sequence-open
read-OID drop id-qt-cps eqOID ifnot
ERR_X509_CRITICAL_EXTENSION fail
then
skip-close-elt
repeat
close-elt
then
close-elt
repeat
close-elt ;
\ Process a Subject Alt Name extension. Returned value is a boolean set
\ to true if the expected server name was matched against a dNSName in
\ the extension.
: process-SAN ( lim -- lim bool )
0 { m }
read-sequence-open
begin dup while
\ Read the tag. If the tag is context-0, then parse an
\ 'otherName'. If the tag is context-2, then parse a
\ dNSName. If the tag is context-1 or context-6,
\ parse
read-tag case
\ OtherName
0x20 of
\ OtherName ::= SEQUENCE {
\ type-id OBJECT IDENTIFIER,
\ value [0] EXPLICIT ANY
\ }
check-constructed read-length-open-elt
read-OID drop
-1 offset-name-element { offbuf }
read-tag 0x20 check-tag-constructed
read-length-open-elt
read-string offbuf copy-name-element
close-elt
close-elt
endof
\ rfc822Name (IA5String)
0x21 of
check-primitive
read-value-UTF8 1 copy-name-SAN
endof
\ dNSName (IA5String)
0x22 of
check-primitive
read-value-UTF8
dup if match-server-name m or >m then
2 copy-name-SAN
endof
\ uniformResourceIdentifier (IA5String)
0x26 of
check-primitive
read-value-UTF8 6 copy-name-SAN
endof
2drop read-length-skip 0
endcase
\ We check only names of type dNSName; they use IA5String,
\ which is basically ASCII.
\ read-tag 0x22 = if
\ check-primitive
\ read-small-value drop
\ match-server-name m or >m
\ else
\ drop read-length-skip
\ then
repeat
close-elt
m ;
\ Decode a certificate. The "ee" boolean must be true for the EE.
: decode-certificate ( ee -- )
{ ee }
\ Obtain the total certificate length.
addr-cert_length get32
\ Open the outer SEQUENCE.
read-sequence-open
\ TBS
\ Activate hashing.
start-tbs-hash
read-sequence-open
\ First element may be an explicit version. We accept only
\ versions 0 to 2 (certificates v1 to v3).
read-tag dup 0x20 = if
drop check-constructed read-length-open-elt
read-tag
0x02 check-tag-primitive
read-small-int-value
2 > if ERR_X509_UNSUPPORTED fail then
close-elt
read-tag
then
\ Serial number. We just check that the tag is correct.
0x02 check-tag-primitive
read-length-skip
\ Signature algorithm. This structure is redundant with the one
\ on the outside; we just skip it.
read-sequence-open skip-close-elt
\ Issuer name: hashed, then copied into next_dn_hash[].
read-DN
addr-next_dn_hash addr-current_dn_hash dn-hash-length blobcopy
\ Validity dates.
read-sequence-open
read-date get-system-date after if ERR_X509_EXPIRED fail then
read-date get-system-date before if ERR_X509_EXPIRED fail then
close-elt
\ Subject name.
ee if
\ For the EE, we must check whether the Common Name, if
\ any, matches the expected server name.
read-DN-EE { eename }
else
\ For a non-EE certificate, the hashed subject DN must match
\ the saved hashed issuer DN from the previous certificate.
read-DN
addr-current_dn_hash addr-saved_dn_hash dn-hash-length eqblob
ifnot ERR_X509_DN_MISMATCH fail then
then
\ Move the hashed issuer DN for this certificate into the
\ saved_dn_hash[] array.
addr-saved_dn_hash addr-next_dn_hash dn-hash-length blobcopy
\ Public Key.
read-sequence-open
\ Algorithm Identifier. Right now we are only interested in the
\ OID, since we only support RSA keys.
read-sequence-open
read-OID ifnot ERR_X509_UNSUPPORTED fail then
{ ; pkey-type }
choice
\ RSA public key.
rsaEncryption eqOID uf
skip-close-elt
\ Public key itself: the BIT STRING contains bytes
\ (no partial byte) and these bytes encode the
\ actual value.
read-bits-open
\ RSA public key is a SEQUENCE of two
\ INTEGER. We get both INTEGER values into
\ the pkey_data[] buffer, if they fit.
read-sequence-open
addr-len-pkey_data
read-integer { nlen }
addr-len-pkey_data swap nlen + swap nlen -
read-integer { elen }
close-elt
\ Check that the public key fits our minimal
\ size requirements. Note that the integer
\ decoder already skipped the leading bytes
\ of value 0, so we are working on the true
\ modulus length here.
addr-min_rsa_size get16 128 + nlen > if
ERR_X509_WEAK_PUBLIC_KEY fail
then
close-elt
KEYTYPE_RSA >pkey-type
enduf
\ EC public key.
id-ecPublicKey eqOID uf
\ We support only named curves, for which the
\ "parameters" field in the AlgorithmIdentifier
\ field should be an OID.
read-OID ifnot ERR_X509_UNSUPPORTED fail then
choice
ansix9p256r1 eqOID uf 23 enduf
ansix9p384r1 eqOID uf 24 enduf
ansix9p521r1 eqOID uf 25 enduf
ERR_X509_UNSUPPORTED fail
endchoice
{ curve }
close-elt
read-bits-open
dup { qlen }
dup addr-len-pkey_data rot < if
ERR_X509_LIMIT_EXCEEDED fail
then
read-blob
KEYTYPE_EC >pkey-type
enduf
\ Not a recognised public key type.
ERR_X509_UNSUPPORTED fail
endchoice
close-elt
\ Process public key.
ee if
\ For the EE certificate, copy the key data to the
\ relevant buffer.
pkey-type case
KEYTYPE_RSA of nlen elen copy-ee-rsa-pkey endof
KEYTYPE_EC of curve qlen copy-ee-ec-pkey endof
ERR_X509_UNSUPPORTED fail
endcase
else
\ Verify signature on previous certificate. We invoke
\ the RSA implementation.
pkey-type case
KEYTYPE_RSA of nlen elen do-rsa-vrfy endof
KEYTYPE_EC of curve qlen do-ecdsa-vrfy endof
ERR_X509_UNSUPPORTED fail
endcase
dup if fail then
drop
then
\ This flag will be set to true if the Basic Constraints extension
\ is encountered.
0 { seenBC }
\ Skip issuerUniqueID and subjectUniqueID, and process extensions
\ if present. Extensions are an explicit context tag of value 3
\ around a SEQUENCE OF extensions. Each extension is a SEQUENCE
\ with an OID, an optional boolean, and a value; the value is
\ an OCTET STRING.
read-tag-or-end
0x21 iftag-skip
0x22 iftag-skip
dup 0x23 = if
drop
check-constructed read-length-open-elt
read-sequence-open
begin dup while
0 { critical }
read-sequence-open
read-OID drop
read-tag dup 0x01 = if
read-boolean >critical
read-tag
then
0x04 check-tag-primitive read-length-open-elt
choice
\ Extensions with specific processing.
basicConstraints eqOID uf
ee if
skip-remaining
else
process-basicConstraints
-1 >seenBC
then
enduf
keyUsage eqOID uf
ee process-keyUsage
enduf
subjectAltName eqOID uf
ee if
0 >eename
process-SAN >eename
else
skip-remaining
then
enduf
\ We don't implement full processing of
\ policies. The call below mostly checks
\ that the contents of the Certificate
\ Policies extension can be safely ignored.
certificatePolicies eqOID uf
critical if
process-certPolicies
else
skip-remaining
then
enduf
\ Extensions which are always ignored,
\ even if critical.
authorityKeyIdentifier eqOID uf
skip-remaining
enduf
subjectKeyIdentifier eqOID uf
skip-remaining
enduf
issuerAltName eqOID uf
skip-remaining
enduf
subjectDirectoryAttributes eqOID uf
skip-remaining
enduf
crlDistributionPoints eqOID uf
skip-remaining
enduf
freshestCRL eqOID uf
skip-remaining
enduf
authorityInfoAccess eqOID uf
skip-remaining
enduf
subjectInfoAccess eqOID uf
skip-remaining
enduf
\ Unrecognized extensions trigger a failure
\ if critical; otherwise, they are just
\ ignored.
critical if
ERR_X509_CRITICAL_EXTENSION fail
then
skip-remaining
endchoice
close-elt
close-elt
repeat
close-elt
close-elt
else
-1 = ifnot ERR_X509_UNEXPECTED fail then
drop
then
close-elt
\ Terminate hashing.
stop-tbs-hash
\ For the EE certificate, verify that the intended server name
\ was matched.
ee if
eename zero-server-name or ifnot
ERR_X509_BAD_SERVER_NAME fail
then
then
\ If this is the EE certificate, then direct trust may apply.
\ Note: we do this at this point, not immediately after decoding
\ the public key, because even in case of direct trust we still
\ want to check the server name with regards to the SAN extension.
\ However, we want to check direct trust before trying to decode
\ the signature algorithm, because it should work even if that
\ algorithm is not supported.
ee if check-direct-trust then
\ Non-EE certificates MUST have a Basic Constraints extension
\ (that marks them as being CA).
ee seenBC or ifnot ERR_X509_NOT_CA fail then
\ signature algorithm
read-tag check-sequence read-length-open-elt
\ Read and understand the OID. Right now, we support only
\ RSA with PKCS#1 v1.5 padding, and hash functions SHA-1,
\ SHA-224, SHA-256, SHA-384 and SHA-512. We purposely do NOT
\ support MD5 here.
\ TODO: add support for RSA/PSS
read-OID if
\ Based on the signature OID, we get:
\ -- the signing key type
\ -- the hash function numeric identifier
\ -- the hash function OID
choice
sha1WithRSAEncryption eqOID
uf 2 KEYTYPE_RSA id-sha1 enduf
sha224WithRSAEncryption eqOID
uf 3 KEYTYPE_RSA id-sha224 enduf
sha256WithRSAEncryption eqOID
uf 4 KEYTYPE_RSA id-sha256 enduf
sha384WithRSAEncryption eqOID
uf 5 KEYTYPE_RSA id-sha384 enduf
sha512WithRSAEncryption eqOID
uf 6 KEYTYPE_RSA id-sha512 enduf
ecdsa-with-SHA1 eqOID
uf 2 KEYTYPE_EC id-sha1 enduf
ecdsa-with-SHA224 eqOID
uf 3 KEYTYPE_EC id-sha224 enduf
ecdsa-with-SHA256 eqOID
uf 4 KEYTYPE_EC id-sha256 enduf
ecdsa-with-SHA384 eqOID
uf 5 KEYTYPE_EC id-sha384 enduf
ecdsa-with-SHA512 eqOID
uf 6 KEYTYPE_EC id-sha512 enduf
ERR_X509_UNSUPPORTED fail
endchoice
addr-cert_sig_hash_oid set16
addr-cert_signer_key_type set8
\ Compute the TBS hash into tbs_hash.
compute-tbs-hash
dup ifnot ERR_X509_UNSUPPORTED fail then
addr-cert_sig_hash_len set8
else
ERR_X509_UNSUPPORTED fail
then
\ We ignore the parameters, whether they are present or not,
\ because we got all the information from the OID.
skip-close-elt
\ signature value
read-bits-open
dup CX 0 8191 { BR_X509_BUFSIZE_SIG } > if
ERR_X509_LIMIT_EXCEEDED fail
then
dup addr-cert_sig_len set16
addr-cert_sig read-blob
\ Close the outer SEQUENCE.
close-elt
\ Close the advertised total certificate length. This checks that
\ there is no trailing garbage after the certificate.
close-elt
\ Flag the certificate as fully processed.
0 addr-cert_length set32
\ Check whether the issuer for the current certificate is known
\ as a trusted CA; in which case, verify the signature.
check-trust-anchor-CA ;
: main
\ Unless restricted by a Key Usage extension, all usages are
\ deemed allowed.
0x30 addr-key_usages set8
-1 decode-certificate
co
begin
0 decode-certificate co
again
;