blob: 2cc93ece6b00516d4c0a1691b68ea9dc1d7de0f2 [file] [log] [blame]
/*
* Copyright 2022 The HIBA Authors
*
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file or at
* https://developers.google.com/open-source/licenses/bsd
*/
#include <inttypes.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "errors.h"
#include "log.h"
#include "revocations.h"
#define HIBA_CURRENT_GRL_VERSION 0x1
#define HIBA_MIN_SUPPORTED_GRL_VERSION 0x1
/* Stores a list of all the certificates with revoked grants.
* This structure is meant to be mapped directly on top of a mmapped revocation
* file on top of the serial list section (see PROTOCOL.grl). */
struct serials {
u_int64_t serial;
u_int64_t offset;
} __attribute__((packed));
/* Parsed list of revocations. */
struct parsed_revocations {
u_int64_t serial;
u_int16_t size;
char *map;
struct parsed_revocations *next;
};
/* In memory representation of the GRL.
* The 'parsed' field is a linked list to revocations entries. The first
* element of that list (if it exists) is the head and doesn't contain any
* revocation information. */
struct hibagrl {
u_int32_t version;
u_int32_t min_version;
u_int64_t timestamp;
u_int32_t n_serials;
u_int64_t bitmaps_size;
char *comment;
const struct serials *serials;
const unsigned char *bitmaps;
struct parsed_revocations *parsed;
};
void
hibagrl_free(struct hibagrl *grl) {
struct parsed_revocations *r;
if (grl == NULL)
return;
r = grl->parsed;
while (r != NULL) {
struct parsed_revocations *tbd = r;
r = r->next;
free(tbd->map);
free(tbd);
}
free(grl->comment);
free(grl);
}
struct hibagrl*
hibagrl_new() {
struct hibagrl *grl = calloc(sizeof(struct hibagrl), 1);
return grl;
}
void
hibagrl_init(struct hibagrl *grl, const char *comment) {
if (grl == NULL)
return;
memset(grl, 0, sizeof(struct hibagrl));
grl->version = HIBA_CURRENT_GRL_VERSION;
grl->min_version = HIBA_MIN_SUPPORTED_GRL_VERSION;
grl->comment = strdup(comment);
/* Allocate the head of the parsed linked list to show this is an
* editable GRL. */
grl->parsed = calloc(1, sizeof(struct parsed_revocations));
}
int
hibagrl_version(const struct hibagrl *grl) {
if (grl == NULL) {
return HIBA_BAD_PARAMS;
}
return grl->version;
}
const char*
hibagrl_comment(const struct hibagrl *grl) {
if (grl == NULL) {
return hiba_err(-HIBA_BAD_PARAMS);
}
return grl->comment;
}
u_int64_t
hibagrl_timestamp(const struct hibagrl *grl) {
if (grl == NULL) {
return 0;
}
return grl->timestamp;
}
u_int64_t
hibagrl_serials_count(const struct hibagrl *grl) {
if (grl == NULL) {
return 0;
}
return grl->n_serials;
}
int
hibagrl_decode(struct hibagrl *grl, struct sshbuf *blob) {
int ret;
u_int32_t magic;
u_int64_t serials_section_size;
if (grl == NULL || blob == NULL)
return HIBA_BAD_PARAMS;
memset(grl, 0, sizeof(struct hibagrl));
if ((ret = sshbuf_get_u32(blob, &magic)) < 0) {
debug3("hibagrl_decode: sshbuf_get_u32 returned %d: %s", ret, ssh_err(ret));
return HIBA_INTERNAL_ERROR;
}
if (magic != HIBA_GRL_MAGIC) {
return HIBA_INVALID_GRL;
}
if ((ret = sshbuf_get_u32(blob, &grl->version)) != 0) {
debug3("hibagrl_decode: sshbuf_get_u32 returned %d: %s", ret, ssh_err(ret));
return HIBA_INTERNAL_ERROR;
}
if ((ret = sshbuf_get_u32(blob, &grl->min_version)) != 0) {
debug3("hibagrl_decode: sshbuf_get_u32 returned %d: %s", ret, ssh_err(ret));
return HIBA_INTERNAL_ERROR;
}
if ((ret = sshbuf_get_u64(blob, &grl->timestamp)) != 0) {
debug3("hibagrl_decode: sshbuf_get_u64 returned %d: %s", ret, ssh_err(ret));
return HIBA_INTERNAL_ERROR;
}
if ((ret = sshbuf_get_cstring(blob, &grl->comment, NULL)) != 0) {
debug3("hibagrl_decode: sshbuf_get_cstring returned %d: %s", ret, ssh_err(ret));
return HIBA_INTERNAL_ERROR;
}
/* Get the serials section. */
if ((ret = sshbuf_get_u64(blob, &serials_section_size)) != 0) {
debug3("hibagrl_decode: sshbuf_get_u64 returned %d: %s", ret, ssh_err(ret));
return HIBA_INTERNAL_ERROR;
}
grl->n_serials = serials_section_size / sizeof(struct serials);
debug2("hibagrl_decode: serials_section_size = %" PRIu64, serials_section_size);
grl->serials = (const struct serials *)sshbuf_ptr(blob);
if (log_level_get() >= SYSLOG_LEVEL_DEBUG3) {
debug3("hibagrl_decode: serial list section content");
sshbuf_dump_data(sshbuf_ptr(blob), serials_section_size, stderr);
}
if ((ret = sshbuf_consume(blob, serials_section_size)) != 0) {
debug3("hibagrl_decode: sshbuf_consume returned %d: %s", ret, ssh_err(ret));
return HIBA_INTERNAL_ERROR;
}
/* Get the bitmaps section. */
if ((ret = sshbuf_get_u64(blob, &grl->bitmaps_size)) != 0) {
debug3("hibagrl_decode: sshbuf_get_u64 returned %d: %s", ret, ssh_err(ret));
return HIBA_INTERNAL_ERROR;
}
debug2("hibagrl_decode: bitmaps_section_size = %" PRIu64, grl->bitmaps_size);
grl->bitmaps = sshbuf_ptr(blob);
if (log_level_get() >= SYSLOG_LEVEL_DEBUG3) {
debug3("hibagrl_decode: bitmap section content");
sshbuf_dump_data(sshbuf_ptr(blob), grl->bitmaps_size, stderr);
}
if ((ret = sshbuf_consume(blob, grl->bitmaps_size)) != 0) {
debug3("hibagrl_decode: sshbuf_consume returned %d: %s", ret, ssh_err(ret));
return HIBA_INTERNAL_ERROR;
}
if ((ret = sshbuf_get_u32(blob, &magic)) < 0) {
debug3("hibagrl_decode: sshbuf_get_u32 returned %d: %s", ret, ssh_err(ret));
return HIBA_INTERNAL_ERROR;
}
if (magic != HIBA_GRL_MAGIC) {
return HIBA_INVALID_GRL;
}
/* The parsed_revocations field is left NULL on purpose and will only be
* actually parsed from serials/bitmaps sections when needed using
* hibagrl_map(). */
debug2("hibagrl_decode: grl->n_serials = %" PRIu32, grl->n_serials);
return HIBA_OK;
}
int
hibagrl_map(struct hibagrl *grl) {
u_int64_t i = 0;
struct parsed_revocations *r;
if (grl == NULL)
return HIBA_BAD_PARAMS;
if (grl->parsed != NULL)
return HIBA_BAD_PARAMS;
/* The first parsed_revocation doesn't count, it is only used as the
* head of the linked list. */
grl->parsed = calloc(1, sizeof(struct parsed_revocations));
r = grl->parsed;
debug("hibagrl_map: mapping %" PRIu32 " serials", grl->n_serials);
for (i = 0; i < grl->n_serials; ++i) {
u_int64_t offset;
u_int64_t next_offset = grl->bitmaps_size;
r->next = calloc(1, sizeof(struct parsed_revocations));
r = r->next;
r->serial = PEEK_U64(&grl->serials[i].serial);
offset = PEEK_U64(&grl->serials[i].offset);
if (i+1 < grl->n_serials) {
next_offset = PEEK_U64(&grl->serials[i+1].offset);
}
r->size = next_offset - offset;
r->map = calloc(1, r->size);
memcpy(r->map, grl->bitmaps + offset, r->size);
}
/* Nullify mmapped sections since we now have an editable GRL. */
grl->serials = NULL;
grl->bitmaps = NULL;
grl->bitmaps_size = 0;
return 0;
}
int
hibagrl_encode(const struct hibagrl *grl, struct sshbuf *blob) {
int ret;
struct parsed_revocations *r;
u_int64_t serials_section_size;
u_int64_t i = 0;
u_int64_t offset = 0;
if (grl == NULL || blob == NULL || grl->parsed == NULL)
return HIBA_BAD_PARAMS;
/* Write the header. */
if ((ret = sshbuf_put_u32(blob, HIBA_GRL_MAGIC)) < 0) {
debug3("hibagrl_encode: sshbuf_put_u32 returned %d: %s", ret, ssh_err(ret));
return HIBA_INTERNAL_ERROR;
}
if ((ret = sshbuf_put_u32(blob, grl->version)) < 0) {
debug3("hibagrl_encode: sshbuf_put_u32 returned %d: %s", ret, ssh_err(ret));
return HIBA_INTERNAL_ERROR;
}
if ((ret = sshbuf_put_u32(blob, grl->min_version)) < 0) {
debug3("hibagrl_encode: sshbuf_put_u32 returned %d: %s", ret, ssh_err(ret));
return HIBA_INTERNAL_ERROR;
}
if ((ret = sshbuf_put_u64(blob, time(NULL))) < 0) {
debug3("hibagrl_encode: sshbuf_put_u64 returned %d: %s", ret, ssh_err(ret));
return HIBA_INTERNAL_ERROR;
}
if ((ret = sshbuf_put_cstring(blob, grl->comment)) < 0) {
debug3("hibagrl_encode: sshbuf_put_cstring returned %d: %s", ret, ssh_err(ret));
return HIBA_INTERNAL_ERROR;
}
serials_section_size = grl->n_serials * sizeof(struct serials);
debug2("hibagrl_encode: grl->n_serials = %" PRIu32, grl->n_serials);
debug2("hibagrl_decode: serials_section_size = %" PRIu64, serials_section_size);
if ((ret = sshbuf_put_u64(blob, serials_section_size)) < 0) {
debug3("hibagrl_encode: sshbuf_put_u64 returned %d: %s", ret, ssh_err(ret));
return HIBA_INTERNAL_ERROR;
}
/* Now we build the serial list. */
debug3("hibagrl_encode: serial list section @%zu", sshbuf_len(blob));
if ((ret = sshbuf_allocate(blob, serials_section_size)) < 0) {
debug3("hibagrl_encode: sshbuf_allocate(%" PRIu64 ") returned %d: %s",
serials_section_size, ret, ssh_err(ret));
return HIBA_INTERNAL_ERROR;
}
/* Calculate and reserve room for the bitmap size. */
r = grl->parsed;
while (r->next != NULL) {
r = r->next;
/* Add the serial to the 1st section. */
if ((ret = sshbuf_put_u64(blob, r->serial)) < 0) {
debug3("hibagrl_encode: sshbuf_put_u64 returned %d: %s", ret, ssh_err(ret));
return HIBA_INTERNAL_ERROR;
}
if ((ret = sshbuf_put_u64(blob, offset)) < 0) {
debug3("hibagrl_encode: sshbuf_put_u64 returned %d: %s", ret, ssh_err(ret));
return HIBA_INTERNAL_ERROR;
}
offset += r->size;
}
if ((ret = sshbuf_put_u64(blob, offset)) < 0) {
debug3("hibagrl_encode: sshbuf_put_u64 returned %d: %s", ret, ssh_err(ret));
return HIBA_INTERNAL_ERROR;
}
if ((ret = sshbuf_allocate(blob, offset)) < 0) {
debug3("hibagrl_encode: sshbuf_allocate(%" PRIu64 ") returned %d: %s",
offset, ret, ssh_err(ret));
return HIBA_INTERNAL_ERROR;
}
debug3("hibagrl_encode: bitmap section @%zu", sshbuf_len(blob));
r = grl->parsed;
while (r->next != NULL) {
int m;
r = r->next;
for (m = 0; m < r->size; ++m) {
if ((ret = sshbuf_put_u8(blob, r->map[m])) < 0) {
debug3("hibagrl_encode: sshbuf_put_u8 returned %d: %s", ret, ssh_err(ret));
return HIBA_INTERNAL_ERROR;
}
}
offset += r->size + sizeof(r->size);
++i;
}
debug2("hibagrl_encode: patch bitmap section size = %" PRIu64, offset);
/* Write trailer magic. */
if ((ret = sshbuf_put_u32(blob, HIBA_GRL_MAGIC)) < 0) {
debug3("hibagrl_encode: sshbuf_put_u32 returned %d: %s", ret, ssh_err(ret));
return HIBA_INTERNAL_ERROR;
}
return HIBA_OK;
}
/* The block of a grant index represents the char that contains the bit
* representing the grant.
* This corresponds to the floor of the position of the bit divided by 8 (size
* of a char). */
static __inline__ u_int32_t
block_for_idx(u_int32_t idx) {
return idx >> 3;
}
/* The offset of a grant index represents the position of the bit representing
* the grant inside the block.
* This corresponds to remainder of the position of the bit divided by 8 (size
* of a char). */
static __inline__ u_int32_t
offset_for_idx(u_int32_t idx) {
return idx & 0x7;
}
/* We want to update the offset in the right block. */
static __inline__ void
revoke_idx(struct parsed_revocations *r, u_int32_t idx) {
u_int32_t block = block_for_idx(idx);
u_int32_t offset = offset_for_idx(idx);
debug("REVOKE_ID(%d): map[%d] |= %02x", idx, block, offset);
r->map[block] |= 1 << offset;
}
/* A grant ID is marked as revoked if both conditions hold true:
* - the map size is larger or equal to the grant idx.
* - the grant ID corresponding bit is set. */
static __inline__ int
is_revoked_idx(const unsigned char *bitmap, u_int32_t size, u_int32_t idx) {
u_int32_t block = block_for_idx(idx);
u_int32_t offset = offset_for_idx(idx);
return (block <= size) && ((1 << offset) & bitmap[block]);
}
int
hibagrl_revoke_grant(struct hibagrl *grl, u_int64_t serial, u_int32_t lo, u_int32_t hi) {
u_int32_t i;
int required_size = 0;
struct parsed_revocations *r;
if (grl == NULL)
return HIBA_BAD_PARAMS;
if (grl->parsed == NULL)
return HIBA_BAD_PARAMS;
/* Look for where to add the revocation. */
r = grl->parsed;
while (r->next) {
if (r->next->serial >= serial)
break;
r = r->next;
}
/* Maybe add a new serial to the ordered list. */
if ((r->next == NULL) || (r->next->serial > serial)) {
struct parsed_revocations *next = r->next;
debug2("hibagrl_revoke_grant: adding a new entry for serial 0x%" PRIx64 " after 0x%" PRIx64 , serial, r->serial);
r->next = calloc(1, sizeof(struct parsed_revocations));
r = r->next;
r->next = next;
r->serial = serial;
++grl->n_serials;
} else {
r = r->next;
debug2("hibagrl_revoke_grant: reusing existing entry for serial 0x%" PRIx64, serial);
}
required_size = block_for_idx(hi) + 1;
/* Allocate or update the bitmap. */
if (r->size < required_size) {
char *prev_map = r->map;
debug2("hibagrl_revoke_grant: resizing map to %d", required_size);
r->map = calloc(required_size, 1);
if (prev_map) {
/* if we already add revocations for this serial, we
* must copy them. */
memcpy(r->map, prev_map, r->size);
free(prev_map);
}
r->size = required_size;
}
/* Add our grants. */
for (i = lo; i <= hi; ++i) {
revoke_idx(r, i);
}
return 0;
}
int
hibagrl_check(const struct hibagrl *grl, u_int64_t serial, u_int32_t grant_idx) {
u_int64_t offset;
u_int64_t size;
int64_t min = 0;
int64_t max;
int64_t cur;
if (grl == NULL)
return HIBA_BAD_PARAMS;
if (grl->serials == NULL || grl->bitmaps == NULL)
return HIBA_BAD_PARAMS;
/* Early return if the GRL contains no serials. */
if (grl->n_serials == 0)
return HIBA_OK;
max = grl->n_serials - 1;
/* First we search through serials. */
while (1) {
u_int64_t min_serial = PEEK_U64(&grl->serials[min].serial);
u_int64_t max_serial = PEEK_U64(&grl->serials[max].serial);
u_int64_t cur_serial;
cur = min + ((max - min) / 2);
cur_serial = PEEK_U64(&grl->serials[cur].serial);
debug3("hibagrl_check: window [0x%" PRIx64 " (@%" PRIx64 ") - 0x%" PRIx64 " (@%" PRIx64 ")]", min_serial, min, max_serial, max);
debug3("hibagrl_check: target 0x%" PRIx64 " against current serial 0x%" PRIx64 "(@%" PRIx64 ")", serial, cur_serial, cur);
if (cur_serial == serial) {
/* We only break the loop when we found a matching
* serial which index we store in cur. */
break;
} else if (cur_serial > serial) {
max = cur - 1;
} else if (cur_serial < serial) {
min = cur + 1;
}
if (max < min) {
debug2("hibagrl_check: serial not found in GRL");
return HIBA_OK;
}
}
debug2("hibagrl_check: found in GRL: 0x%" PRIx64, serial);
/* If the offset points outside of the bitmap, our GRL is invalid.
* Fail closed. */
offset = PEEK_U64(&grl->serials[cur].offset);
if (offset > grl->bitmaps_size)
return HIBA_BAD_PARAMS;
if (cur >= grl->n_serials - 1)
size = grl->bitmaps_size - offset;
else
size = PEEK_U64(&grl->serials[cur+1].offset) - offset;
if (is_revoked_idx(grl->bitmaps + offset, size, grant_idx))
return HIBA_CHECK_REVOKED;
return HIBA_OK;
}
static void
dump_map(const struct parsed_revocations *r, FILE *f) {
int i;
for (i = 0; i < r->size; ++i) {
fprintf(f, "%02X", (r->map[i] & 0xF) << 4 | r->map[i] >> 4);
}
fprintf(f, "\n");
}
int
hibagrl_dump_content(const struct hibagrl *grl, u_int64_t *serial, FILE *f) {
struct parsed_revocations *r;
if (grl == NULL)
return HIBA_BAD_PARAMS;
if (grl->parsed == NULL)
return HIBA_BAD_PARAMS;
r = grl->parsed;
while (r->next != NULL) {
r = r->next;
if (serial == NULL || r->serial == *serial) {
fprintf(f, " [0x%.16" PRIx64 "]: ", r->serial);
dump_map(r, f);
/* When filtering on serial, we can early exit as each
* serial appears at most once. */
if (serial)
break;
}
}
fprintf(f, "\n");
return HIBA_OK;
}