From 507007e865f7db7cfd4132512cf6f08b46086ca5 Mon Sep 17 00:00:00 2001 From: Carl Niklas Rydberg Date: Fri, 23 Jan 2026 23:08:41 +0100 Subject: [PATCH] Add read-only capability tokens --- registry/amduatd-api-contract.v1.json | 18 + src/amduatd.c | 1729 ++++++++++++++++++++++++- 2 files changed, 1742 insertions(+), 5 deletions(-) diff --git a/registry/amduatd-api-contract.v1.json b/registry/amduatd-api-contract.v1.json index 13ec487..00a16bf 100644 --- a/registry/amduatd-api-contract.v1.json +++ b/registry/amduatd-api-contract.v1.json @@ -6,6 +6,8 @@ {"method": "GET", "path": "/v1/meta"}, {"method": "HEAD", "path": "/v1/meta"}, {"method": "GET", "path": "/v1/contract"}, + {"method": "POST", "path": "/v1/capabilities"}, + {"method": "GET", "path": "/v1/cap/resolve"}, {"method": "GET", "path": "/v1/fed/records"}, {"method": "GET", "path": "/v1/fed/artifacts/{ref}"}, {"method": "GET", "path": "/v1/fed/status"}, @@ -24,6 +26,22 @@ {"method": "POST", "path": "/v1/context_frames"} ], "schemas": { + "capability_mint_request": { + "type": "object", + "required": ["kind", "target", "expiry_seconds"], + "properties": { + "kind": {"type": "string"}, + "target": {"type": "object"}, + "expiry_seconds": {"type": "integer"} + } + }, + "capability_mint_response": { + "type": "object", + "required": ["token"], + "properties": { + "token": {"type": "string"} + } + }, "pel_run_request": { "type": "object", "required": ["program_ref", "input_refs"], diff --git a/src/amduatd.c b/src/amduatd.c index 94ee145..af9e90f 100644 --- a/src/amduatd.c +++ b/src/amduatd.c @@ -32,6 +32,7 @@ #include "amduat/pel/run.h" #include "amduat/util/hex.h" #include "amduat/util/log.h" +#include "amduat/hash/asl1.h" #include #include @@ -90,6 +91,7 @@ static const uint64_t AMDUATD_FED_TICK_MS = 1000u; static const uint32_t AMDUATD_EDGE_VIEW_BATCH = 1024u; static const uint16_t AMDUATD_EDGE_COLLECTION_KIND = 1u; +static const uint64_t AMDUATD_CAP_MAX_EXPIRY_SECONDS = 7u * 24u * 60u * 60u; typedef struct { const char *space_id; @@ -97,6 +99,13 @@ typedef struct { bool migrate_unscoped_edges; } amduatd_cfg_t; +typedef struct { + bool enabled; + bool reads_enabled; + uint8_t hmac_key[32]; + size_t hmac_key_len; +} amduatd_cap_state_t; + typedef struct { amduat_reference_t record_ref; amduat_reference_t src_ref; @@ -227,6 +236,21 @@ static const uint8_t k_amduatd_edge_magic[AMDUATD_EDGE_MAGIC_LEN] = { 'A', 'S', 'L', 'E', 'D', 'G', '1', '\0' }; +enum { + AMDUATD_CAP_MAGIC_LEN = 8, + AMDUATD_CAP_VERSION = 1 +}; + +enum { + AMDUATD_CAP_KIND_ARTIFACT_REF = 1, + AMDUATD_CAP_KIND_POINTER_NAME = 2, + AMDUATD_CAP_KIND_PEL_VIEW = 3 +}; + +static const uint8_t k_amduatd_cap_magic[AMDUATD_CAP_MAGIC_LEN] = { + 'A', 'S', 'L', 'C', 'A', 'P', '1', '\0' +}; + static const char *const AMDUATD_EDGE_SCHEMA = "tgk/edge"; static const char *const AMDUATD_REL_ALIAS = "alias"; @@ -259,6 +283,36 @@ static bool amduatd_read_u32_le(const uint8_t *data, return true; } +static void amduatd_store_u64_le(uint8_t *out, uint64_t value) { + out[0] = (uint8_t)(value & 0xffu); + out[1] = (uint8_t)((value >> 8) & 0xffu); + out[2] = (uint8_t)((value >> 16) & 0xffu); + out[3] = (uint8_t)((value >> 24) & 0xffu); + out[4] = (uint8_t)((value >> 32) & 0xffu); + out[5] = (uint8_t)((value >> 40) & 0xffu); + out[6] = (uint8_t)((value >> 48) & 0xffu); + out[7] = (uint8_t)((value >> 56) & 0xffu); +} + +static bool amduatd_read_u64_le(const uint8_t *data, + size_t len, + size_t *offset, + uint64_t *out) { + if (len - *offset < 8u) { + return false; + } + *out = (uint64_t)data[*offset] | + ((uint64_t)data[*offset + 1u] << 8) | + ((uint64_t)data[*offset + 2u] << 16) | + ((uint64_t)data[*offset + 3u] << 24) | + ((uint64_t)data[*offset + 4u] << 32) | + ((uint64_t)data[*offset + 5u] << 40) | + ((uint64_t)data[*offset + 6u] << 48) | + ((uint64_t)data[*offset + 7u] << 56); + *offset += 8u; + return true; +} + static bool amduatd_add_size(size_t *acc, size_t add) { if (*acc > SIZE_MAX - add) { return false; @@ -877,6 +931,8 @@ static const char k_amduatd_contract_v1_json[] = "{\"method\":\"GET\",\"path\":\"/v1/meta\"}," "{\"method\":\"HEAD\",\"path\":\"/v1/meta\"}," "{\"method\":\"GET\",\"path\":\"/v1/contract\"}," + "{\"method\":\"POST\",\"path\":\"/v1/capabilities\"}," + "{\"method\":\"GET\",\"path\":\"/v1/cap/resolve\"}," "{\"method\":\"GET\",\"path\":\"/v1/fed/records\"}," "{\"method\":\"GET\",\"path\":\"/v1/fed/artifacts/{ref}\"}," "{\"method\":\"GET\",\"path\":\"/v1/fed/status\"}," @@ -895,6 +951,22 @@ static const char k_amduatd_contract_v1_json[] = "{\"method\":\"POST\",\"path\":\"/v1/context_frames\"}" "]," "\"schemas\":{" + "\"capability_mint_request\":{" + "\"type\":\"object\"," + "\"required\":[\"kind\",\"target\",\"expiry_seconds\"]," + "\"properties\":{" + "\"kind\":{\"type\":\"string\"}," + "\"target\":{\"type\":\"object\"}," + "\"expiry_seconds\":{\"type\":\"integer\"}" + "}" + "}," + "\"capability_mint_response\":{" + "\"type\":\"object\"," + "\"required\":[\"token\"]," + "\"properties\":{" + "\"token\":{\"type\":\"string\"}" + "}" + "}," "\"pel_run_request\":{" "\"type\":\"object\"," "\"required\":[\"program_ref\",\"input_refs\"]," @@ -1422,6 +1494,246 @@ static bool amduatd_read_urandom(uint8_t *out, size_t len) { return true; } +static bool amduatd_ct_memcmp(const uint8_t *a, + const uint8_t *b, + size_t len) { + uint8_t diff = 0u; + if (a == NULL || b == NULL) { + return false; + } + for (size_t i = 0u; i < len; ++i) { + diff |= (uint8_t)(a[i] ^ b[i]); + } + return diff == 0u; +} + +static bool amduatd_sha256_digest(amduat_octets_t input, uint8_t out[32]) { + return amduat_hash_asl1_digest(AMDUAT_HASH_ASL1_ID_SHA256, + input, + out, + 32u); +} + +static bool amduatd_hmac_sha256(const uint8_t *key, + size_t key_len, + const uint8_t *data, + size_t data_len, + uint8_t out[32]) { + uint8_t key_block[64]; + uint8_t ipad[64]; + uint8_t opad[64]; + uint8_t inner[32]; + size_t i; + amduat_hash_asl1_stream_t stream; + + if (key == NULL || data == NULL || out == NULL) { + return false; + } + memset(key_block, 0, sizeof(key_block)); + if (key_len > sizeof(key_block)) { + if (!amduatd_sha256_digest(amduat_octets(key, key_len), key_block)) { + return false; + } + } else if (key_len != 0u) { + memcpy(key_block, key, key_len); + } + + for (i = 0u; i < sizeof(key_block); ++i) { + ipad[i] = (uint8_t)(key_block[i] ^ 0x36u); + opad[i] = (uint8_t)(key_block[i] ^ 0x5cu); + } + + if (!amduat_hash_asl1_stream_init(AMDUAT_HASH_ASL1_ID_SHA256, &stream)) { + return false; + } + if (!amduat_hash_asl1_stream_update(&stream, + amduat_octets(ipad, sizeof(ipad))) || + !amduat_hash_asl1_stream_update(&stream, + amduat_octets(data, data_len)) || + !amduat_hash_asl1_stream_final(&stream, inner, sizeof(inner))) { + amduat_hash_asl1_stream_destroy(&stream); + return false; + } + amduat_hash_asl1_stream_destroy(&stream); + + if (!amduat_hash_asl1_stream_init(AMDUAT_HASH_ASL1_ID_SHA256, &stream)) { + return false; + } + if (!amduat_hash_asl1_stream_update(&stream, + amduat_octets(opad, sizeof(opad))) || + !amduat_hash_asl1_stream_update(&stream, + amduat_octets(inner, sizeof(inner))) || + !amduat_hash_asl1_stream_final(&stream, out, 32u)) { + amduat_hash_asl1_stream_destroy(&stream); + return false; + } + amduat_hash_asl1_stream_destroy(&stream); + return true; +} + +static int amduatd_b64url_value(char c) { + if (c >= 'A' && c <= 'Z') { + return c - 'A'; + } + if (c >= 'a' && c <= 'z') { + return c - 'a' + 26; + } + if (c >= '0' && c <= '9') { + return c - '0' + 52; + } + if (c == '-') { + return 62; + } + if (c == '_') { + return 63; + } + return -1; +} + +static bool amduatd_b64url_encode(const uint8_t *data, + size_t len, + char **out) { + static const char k_table[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + size_t out_len; + size_t i; + size_t o = 0; + char *buf; + + if (out != NULL) { + *out = NULL; + } + if (data == NULL || out == NULL) { + return false; + } + out_len = (len / 3u) * 4u; + if (len % 3u == 1u) { + out_len += 2u; + } else if (len % 3u == 2u) { + out_len += 3u; + } + if (out_len > SIZE_MAX - 1u) { + return false; + } + buf = (char *)malloc(out_len + 1u); + if (buf == NULL) { + return false; + } + + for (i = 0u; i + 2u < len; i += 3u) { + uint32_t v = ((uint32_t)data[i] << 16) | + ((uint32_t)data[i + 1u] << 8) | + (uint32_t)data[i + 2u]; + buf[o++] = k_table[(v >> 18) & 0x3fu]; + buf[o++] = k_table[(v >> 12) & 0x3fu]; + buf[o++] = k_table[(v >> 6) & 0x3fu]; + buf[o++] = k_table[v & 0x3fu]; + } + if (len % 3u == 1u) { + uint32_t v = (uint32_t)data[i] << 16; + buf[o++] = k_table[(v >> 18) & 0x3fu]; + buf[o++] = k_table[(v >> 12) & 0x3fu]; + } else if (len % 3u == 2u) { + uint32_t v = ((uint32_t)data[i] << 16) | + ((uint32_t)data[i + 1u] << 8); + buf[o++] = k_table[(v >> 18) & 0x3fu]; + buf[o++] = k_table[(v >> 12) & 0x3fu]; + buf[o++] = k_table[(v >> 6) & 0x3fu]; + } + buf[o] = '\0'; + *out = buf; + return true; +} + +static bool amduatd_b64url_decode(const char *text, + uint8_t **out, + size_t *out_len) { + size_t len; + size_t clean_len; + size_t i = 0u; + size_t o = 0u; + uint8_t *buf; + + if (out != NULL) { + *out = NULL; + } + if (out_len != NULL) { + *out_len = 0; + } + if (text == NULL || out == NULL || out_len == NULL) { + return false; + } + len = strlen(text); + clean_len = len; + while (clean_len > 0u && text[clean_len - 1u] == '=') { + clean_len--; + } + if (clean_len % 4u == 1u) { + return false; + } + *out_len = (clean_len / 4u) * 3u; + if (clean_len % 4u == 2u) { + *out_len += 1u; + } else if (clean_len % 4u == 3u) { + *out_len += 2u; + } + if (*out_len > SIZE_MAX) { + return false; + } + buf = (uint8_t *)malloc(*out_len); + if (buf == NULL) { + return false; + } + + while (i + 4u <= clean_len) { + int v0 = amduatd_b64url_value(text[i]); + int v1 = amduatd_b64url_value(text[i + 1u]); + int v2 = amduatd_b64url_value(text[i + 2u]); + int v3 = amduatd_b64url_value(text[i + 3u]); + uint32_t v; + if (v0 < 0 || v1 < 0 || v2 < 0 || v3 < 0) { + free(buf); + return false; + } + v = ((uint32_t)v0 << 18) | ((uint32_t)v1 << 12) | + ((uint32_t)v2 << 6) | (uint32_t)v3; + buf[o++] = (uint8_t)((v >> 16) & 0xffu); + buf[o++] = (uint8_t)((v >> 8) & 0xffu); + buf[o++] = (uint8_t)(v & 0xffu); + i += 4u; + } + if (clean_len % 4u == 2u) { + int v0 = amduatd_b64url_value(text[i]); + int v1 = amduatd_b64url_value(text[i + 1u]); + uint32_t v; + if (v0 < 0 || v1 < 0) { + free(buf); + return false; + } + v = ((uint32_t)v0 << 18) | ((uint32_t)v1 << 12); + buf[o++] = (uint8_t)((v >> 16) & 0xffu); + } else if (clean_len % 4u == 3u) { + int v0 = amduatd_b64url_value(text[i]); + int v1 = amduatd_b64url_value(text[i + 1u]); + int v2 = amduatd_b64url_value(text[i + 2u]); + uint32_t v; + if (v0 < 0 || v1 < 0 || v2 < 0) { + free(buf); + return false; + } + v = ((uint32_t)v0 << 18) | ((uint32_t)v1 << 12) | ((uint32_t)v2 << 6); + buf[o++] = (uint8_t)((v >> 16) & 0xffu); + buf[o++] = (uint8_t)((v >> 8) & 0xffu); + } + + if (o != *out_len) { + free(buf); + return false; + } + *out = buf; + return true; +} + static bool amduatd_name_valid(const char *name) { size_t i; size_t len; @@ -1630,6 +1942,685 @@ static bool amduatd_unscoped_name(const amduatd_cfg_t *cfg, return true; } +typedef struct { + uint8_t kind; + char *space; + size_t space_len; + uint64_t expiry_unix; + amduat_reference_t artifact_ref; + char *pointer_name; + amduat_reference_t program_ref; + amduat_reference_t *input_refs; + size_t input_refs_len; + bool has_params_ref; + amduat_reference_t params_ref; +} amduatd_cap_token_t; + +static void amduatd_cap_token_free(amduatd_cap_token_t *token) { + if (token == NULL) { + return; + } + free(token->space); + token->space = NULL; + token->space_len = 0u; + amduat_reference_free(&token->artifact_ref); + free(token->pointer_name); + token->pointer_name = NULL; + amduat_reference_free(&token->program_ref); + if (token->input_refs != NULL) { + for (size_t i = 0u; i < token->input_refs_len; ++i) { + amduat_reference_free(&token->input_refs[i]); + } + free(token->input_refs); + } + token->input_refs = NULL; + token->input_refs_len = 0u; + if (token->has_params_ref) { + amduat_reference_free(&token->params_ref); + } + token->has_params_ref = false; + memset(token, 0, sizeof(*token)); +} + +static bool amduatd_pointer_name_scoped_for_space(const amduatd_cfg_t *dcfg, + const char *name) { + size_t space_len; + if (dcfg == NULL || name == NULL) { + return false; + } + if (!amduat_asl_pointer_name_is_valid(name)) { + return false; + } + if (!dcfg->space_enabled || dcfg->space_id == NULL) { + return true; + } + space_len = strlen(dcfg->space_id); + if (strncmp(name, "space/", 6u) != 0) { + return false; + } + if (strncmp(name + 6u, dcfg->space_id, space_len) != 0) { + return false; + } + if (name[6u + space_len] != '/') { + return false; + } + return true; +} + +static const char *amduatd_cap_kind_name(uint8_t kind) { + switch (kind) { + case AMDUATD_CAP_KIND_ARTIFACT_REF: + return "artifact_ref"; + case AMDUATD_CAP_KIND_POINTER_NAME: + return "pointer_name"; + case AMDUATD_CAP_KIND_PEL_VIEW: + return "pel_view"; + default: + return "unknown"; + } +} + +static void amduatd_cap_token_hash_prefix(const uint8_t *bytes, + size_t len, + char out[9]) { + uint8_t digest[32]; + if (out == NULL) { + return; + } + if (bytes == NULL || len == 0u || + !amduatd_sha256_digest(amduat_octets(bytes, len), digest)) { + strncpy(out, "unknown", 8u); + out[8] = '\0'; + return; + } + if (!amduat_hex_encode_lower(digest, 4u, out, 9u)) { + strncpy(out, "unknown", 8u); + out[8] = '\0'; + return; + } + out[8] = '\0'; +} + +static bool amduatd_cap_init(const char *root_path, amduatd_cap_state_t *cap) { + char secrets_path[1024]; + char key_path[1060]; + struct stat st; + int fd = -1; + uint8_t key_bytes[32]; + + if (cap == NULL) { + return false; + } + memset(cap, 0, sizeof(*cap)); + if (root_path == NULL) { + return false; + } + { + int n = snprintf(secrets_path, sizeof(secrets_path), + "%s/secrets", root_path); + if (n <= 0 || (size_t)n >= sizeof(secrets_path)) { + return false; + } + } + if (stat(secrets_path, &st) != 0) { + if (errno != ENOENT) { + return false; + } + if (mkdir(secrets_path, 0700) != 0) { + return false; + } + } else if (!S_ISDIR(st.st_mode)) { + return false; + } + if (stat(secrets_path, &st) != 0) { + return false; + } + if ((st.st_mode & 0077u) != 0u) { + amduat_log(AMDUAT_LOG_WARN, + "capabilities disabled: secrets dir permissions too open"); + return false; + } + + { + int n = snprintf(key_path, sizeof(key_path), + "%s/secrets/cap_hmac_key", root_path); + if (n <= 0 || (size_t)n >= sizeof(key_path)) { + return false; + } + } + if (stat(key_path, &st) != 0) { + if (errno != ENOENT) { + return false; + } + fd = open(key_path, O_WRONLY | O_CREAT | O_EXCL, 0600); + if (fd < 0) { + return false; + } + if (!amduatd_read_urandom(key_bytes, sizeof(key_bytes))) { + close(fd); + return false; + } + if (!amduatd_write_all(fd, key_bytes, sizeof(key_bytes))) { + close(fd); + return false; + } + close(fd); + } + + if (stat(key_path, &st) != 0) { + return false; + } + if (!S_ISREG(st.st_mode) || (st.st_mode & 0077u) != 0u || + st.st_size != (off_t)sizeof(key_bytes)) { + amduat_log(AMDUAT_LOG_WARN, + "capabilities disabled: cap_hmac_key permissions invalid"); + return false; + } + + fd = open(key_path, O_RDONLY); + if (fd < 0) { + return false; + } + if (!amduatd_read_exact(fd, key_bytes, sizeof(key_bytes))) { + close(fd); + return false; + } + close(fd); + + memcpy(cap->hmac_key, key_bytes, sizeof(key_bytes)); + cap->hmac_key_len = sizeof(key_bytes); + cap->enabled = true; + return true; +} + +static bool amduatd_cap_build_token(const amduatd_cap_token_t *token, + const uint8_t *key, + size_t key_len, + uint8_t **out, + size_t *out_len) { + size_t total = 0u; + size_t payload_len = 0u; + size_t offset = 0u; + size_t hmac_offset = 0u; + uint8_t *buf = NULL; + uint8_t hmac[32]; + amduat_octets_t ref_bytes = amduat_octets(NULL, 0u); + amduat_octets_t program_bytes = amduat_octets(NULL, 0u); + amduat_octets_t *input_bytes = NULL; + amduat_octets_t params_bytes = amduat_octets(NULL, 0u); + + if (out != NULL) { + *out = NULL; + } + if (out_len != NULL) { + *out_len = 0u; + } + if (token == NULL || out == NULL || out_len == NULL || + key == NULL || key_len == 0u) { + return false; + } + + if (token->kind == AMDUATD_CAP_KIND_ARTIFACT_REF) { + if (!amduat_enc_asl1_core_encode_reference_v1(token->artifact_ref, + &ref_bytes)) { + return false; + } + payload_len = 4u + ref_bytes.len; + } else if (token->kind == AMDUATD_CAP_KIND_POINTER_NAME) { + if (token->pointer_name == NULL) { + return false; + } + payload_len = 4u + strlen(token->pointer_name); + } else if (token->kind == AMDUATD_CAP_KIND_PEL_VIEW) { + size_t inputs_total = 0u; + if (!amduat_enc_asl1_core_encode_reference_v1(token->program_ref, + &program_bytes)) { + return false; + } + if (token->input_refs_len != 0u) { + input_bytes = (amduat_octets_t *)calloc(token->input_refs_len, + sizeof(*input_bytes)); + if (input_bytes == NULL) { + free((void *)program_bytes.data); + return false; + } + for (size_t i = 0u; i < token->input_refs_len; ++i) { + if (!amduat_enc_asl1_core_encode_reference_v1(token->input_refs[i], + &input_bytes[i])) { + free((void *)program_bytes.data); + for (size_t j = 0u; j < i; ++j) { + free((void *)input_bytes[j].data); + } + free(input_bytes); + return false; + } + if (!amduatd_add_size(&inputs_total, 4u + input_bytes[i].len)) { + free((void *)program_bytes.data); + for (size_t j = 0u; j <= i; ++j) { + free((void *)input_bytes[j].data); + } + free(input_bytes); + return false; + } + } + } + if (token->has_params_ref) { + if (!amduat_enc_asl1_core_encode_reference_v1(token->params_ref, + ¶ms_bytes)) { + free((void *)program_bytes.data); + if (input_bytes != NULL) { + for (size_t i = 0u; i < token->input_refs_len; ++i) { + free((void *)input_bytes[i].data); + } + free(input_bytes); + } + return false; + } + } + payload_len = 4u + program_bytes.len + 4u + inputs_total + 1u; + if (token->has_params_ref) { + payload_len += 4u + params_bytes.len; + } + } else { + return false; + } + + if (!amduatd_add_size(&total, AMDUATD_CAP_MAGIC_LEN) || + !amduatd_add_size(&total, 4u) || + !amduatd_add_size(&total, 4u + token->space_len) || + !amduatd_add_size(&total, 8u) || + !amduatd_add_size(&total, 1u) || + !amduatd_add_size(&total, payload_len) || + !amduatd_add_size(&total, 4u) || + !amduatd_add_size(&total, sizeof(hmac))) { + free((void *)ref_bytes.data); + free((void *)program_bytes.data); + if (input_bytes != NULL) { + for (size_t i = 0u; i < token->input_refs_len; ++i) { + free((void *)input_bytes[i].data); + } + free(input_bytes); + } + free((void *)params_bytes.data); + return false; + } + + buf = (uint8_t *)malloc(total); + if (buf == NULL) { + free((void *)ref_bytes.data); + free((void *)program_bytes.data); + if (input_bytes != NULL) { + for (size_t i = 0u; i < token->input_refs_len; ++i) { + free((void *)input_bytes[i].data); + } + free(input_bytes); + } + free((void *)params_bytes.data); + return false; + } + + memcpy(buf + offset, k_amduatd_cap_magic, AMDUATD_CAP_MAGIC_LEN); + offset += AMDUATD_CAP_MAGIC_LEN; + amduatd_store_u32_le(buf + offset, AMDUATD_CAP_VERSION); + offset += 4u; + amduatd_store_u32_le(buf + offset, (uint32_t)token->space_len); + offset += 4u; + if (token->space_len != 0u && token->space != NULL) { + memcpy(buf + offset, token->space, token->space_len); + offset += token->space_len; + } + amduatd_store_u64_le(buf + offset, token->expiry_unix); + offset += 8u; + buf[offset++] = token->kind; + + if (token->kind == AMDUATD_CAP_KIND_ARTIFACT_REF) { + amduatd_store_u32_le(buf + offset, (uint32_t)ref_bytes.len); + offset += 4u; + if (ref_bytes.len != 0u) { + memcpy(buf + offset, ref_bytes.data, ref_bytes.len); + offset += ref_bytes.len; + } + } else if (token->kind == AMDUATD_CAP_KIND_POINTER_NAME) { + uint32_t name_len = (uint32_t)strlen(token->pointer_name); + amduatd_store_u32_le(buf + offset, name_len); + offset += 4u; + if (name_len != 0u) { + memcpy(buf + offset, token->pointer_name, name_len); + offset += name_len; + } + } else if (token->kind == AMDUATD_CAP_KIND_PEL_VIEW) { + amduatd_store_u32_le(buf + offset, (uint32_t)program_bytes.len); + offset += 4u; + if (program_bytes.len != 0u) { + memcpy(buf + offset, program_bytes.data, program_bytes.len); + offset += program_bytes.len; + } + amduatd_store_u32_le(buf + offset, (uint32_t)token->input_refs_len); + offset += 4u; + if (input_bytes != NULL) { + for (size_t i = 0u; i < token->input_refs_len; ++i) { + amduatd_store_u32_le(buf + offset, (uint32_t)input_bytes[i].len); + offset += 4u; + if (input_bytes[i].len != 0u) { + memcpy(buf + offset, input_bytes[i].data, input_bytes[i].len); + offset += input_bytes[i].len; + } + } + } + buf[offset++] = token->has_params_ref ? 1u : 0u; + if (token->has_params_ref) { + amduatd_store_u32_le(buf + offset, (uint32_t)params_bytes.len); + offset += 4u; + if (params_bytes.len != 0u) { + memcpy(buf + offset, params_bytes.data, params_bytes.len); + offset += params_bytes.len; + } + } + } + + hmac_offset = offset; + amduatd_store_u32_le(buf + offset, (uint32_t)sizeof(hmac)); + offset += 4u; + + if (!amduatd_hmac_sha256(key, key_len, buf, hmac_offset, hmac)) { + free(buf); + free((void *)ref_bytes.data); + free((void *)program_bytes.data); + if (input_bytes != NULL) { + for (size_t i = 0u; i < token->input_refs_len; ++i) { + free((void *)input_bytes[i].data); + } + free(input_bytes); + } + free((void *)params_bytes.data); + return false; + } + memcpy(buf + offset, hmac, sizeof(hmac)); + offset += sizeof(hmac); + + free((void *)ref_bytes.data); + free((void *)program_bytes.data); + if (input_bytes != NULL) { + for (size_t i = 0u; i < token->input_refs_len; ++i) { + free((void *)input_bytes[i].data); + } + free(input_bytes); + } + free((void *)params_bytes.data); + + if (offset != total) { + free(buf); + return false; + } + *out = buf; + *out_len = total; + return true; +} + +static bool amduatd_cap_decode_token(const uint8_t *bytes, + size_t len, + amduatd_cap_token_t *out_token, + size_t *out_hmac_offset, + uint32_t *out_hmac_len, + const uint8_t **out_hmac) { + size_t offset = 0u; + uint32_t version = 0u; + uint32_t space_len = 0u; + uint32_t ref_len = 0u; + uint32_t input_count = 0u; + uint32_t hmac_len = 0u; + uint64_t expiry = 0u; + uint8_t kind = 0u; + amduatd_cap_token_t token; + const uint8_t *hmac_ptr = NULL; + + if (out_token == NULL || out_hmac_offset == NULL || + out_hmac_len == NULL || out_hmac == NULL) { + return false; + } + memset(&token, 0, sizeof(token)); + + if (len < AMDUATD_CAP_MAGIC_LEN + 4u + 4u + 8u + 1u + 4u + 32u) { + return false; + } + if (memcmp(bytes, k_amduatd_cap_magic, AMDUATD_CAP_MAGIC_LEN) != 0) { + return false; + } + offset += AMDUATD_CAP_MAGIC_LEN; + if (!amduatd_read_u32_le(bytes, len, &offset, &version) || + version != AMDUATD_CAP_VERSION) { + return false; + } + if (!amduatd_read_u32_le(bytes, len, &offset, &space_len)) { + return false; + } + if (len - offset < space_len) { + return false; + } + if (space_len != 0u) { + token.space = (char *)malloc(space_len + 1u); + if (token.space == NULL) { + return false; + } + memcpy(token.space, bytes + offset, space_len); + token.space[space_len] = '\0'; + token.space_len = space_len; + } + offset += space_len; + if (!amduatd_read_u64_le(bytes, len, &offset, &expiry)) { + amduatd_cap_token_free(&token); + return false; + } + token.expiry_unix = expiry; + if (offset >= len) { + amduatd_cap_token_free(&token); + return false; + } + kind = bytes[offset++]; + token.kind = kind; + + if (kind == AMDUATD_CAP_KIND_ARTIFACT_REF) { + if (!amduatd_read_u32_le(bytes, len, &offset, &ref_len)) { + amduatd_cap_token_free(&token); + return false; + } + if (len - offset < ref_len) { + amduatd_cap_token_free(&token); + return false; + } + if (!amduat_enc_asl1_core_decode_reference_v1( + amduat_octets(bytes + offset, ref_len), &token.artifact_ref)) { + amduatd_cap_token_free(&token); + return false; + } + offset += ref_len; + } else if (kind == AMDUATD_CAP_KIND_POINTER_NAME) { + uint32_t name_len = 0u; + if (!amduatd_read_u32_le(bytes, len, &offset, &name_len)) { + amduatd_cap_token_free(&token); + return false; + } + if (len - offset < name_len) { + amduatd_cap_token_free(&token); + return false; + } + token.pointer_name = (char *)malloc(name_len + 1u); + if (token.pointer_name == NULL) { + amduatd_cap_token_free(&token); + return false; + } + memcpy(token.pointer_name, bytes + offset, name_len); + token.pointer_name[name_len] = '\0'; + offset += name_len; + if (!amduat_asl_pointer_name_is_valid(token.pointer_name)) { + amduatd_cap_token_free(&token); + return false; + } + } else if (kind == AMDUATD_CAP_KIND_PEL_VIEW) { + if (!amduatd_read_u32_le(bytes, len, &offset, &ref_len)) { + amduatd_cap_token_free(&token); + return false; + } + if (len - offset < ref_len) { + amduatd_cap_token_free(&token); + return false; + } + if (!amduat_enc_asl1_core_decode_reference_v1( + amduat_octets(bytes + offset, ref_len), &token.program_ref)) { + amduatd_cap_token_free(&token); + return false; + } + offset += ref_len; + if (!amduatd_read_u32_le(bytes, len, &offset, &input_count)) { + amduatd_cap_token_free(&token); + return false; + } + if (input_count != 0u) { + token.input_refs = (amduat_reference_t *)calloc( + input_count, sizeof(*token.input_refs)); + if (token.input_refs == NULL) { + amduatd_cap_token_free(&token); + return false; + } + token.input_refs_len = input_count; + for (uint32_t i = 0u; i < input_count; ++i) { + if (!amduatd_read_u32_le(bytes, len, &offset, &ref_len) || + len - offset < ref_len || + !amduat_enc_asl1_core_decode_reference_v1( + amduat_octets(bytes + offset, ref_len), + &token.input_refs[i])) { + amduatd_cap_token_free(&token); + return false; + } + offset += ref_len; + } + } + if (offset >= len) { + amduatd_cap_token_free(&token); + return false; + } + token.has_params_ref = bytes[offset++] != 0u; + if (token.has_params_ref) { + if (!amduatd_read_u32_le(bytes, len, &offset, &ref_len) || + len - offset < ref_len || + !amduat_enc_asl1_core_decode_reference_v1( + amduat_octets(bytes + offset, ref_len), + &token.params_ref)) { + amduatd_cap_token_free(&token); + return false; + } + offset += ref_len; + } + } else { + amduatd_cap_token_free(&token); + return false; + } + + *out_hmac_offset = offset; + if (!amduatd_read_u32_le(bytes, len, &offset, &hmac_len)) { + amduatd_cap_token_free(&token); + return false; + } + if (len - offset < hmac_len) { + amduatd_cap_token_free(&token); + return false; + } + hmac_ptr = bytes + offset; + offset += hmac_len; + if (offset != len) { + amduatd_cap_token_free(&token); + return false; + } + + *out_hmac_len = hmac_len; + *out_hmac = hmac_ptr; + *out_token = token; + return true; +} + +static bool amduatd_cap_verify_token(const amduatd_cap_state_t *cap, + const amduatd_cfg_t *dcfg, + const uint8_t *bytes, + size_t len, + amduatd_cap_token_t *out_token, + const char **out_reason) { + amduatd_cap_token_t token; + size_t hmac_offset = 0u; + uint32_t hmac_len = 0u; + const uint8_t *hmac = NULL; + uint8_t expected[32]; + time_t now; + + if (out_reason != NULL) { + *out_reason = "invalid"; + } + if (cap == NULL || dcfg == NULL || bytes == NULL || out_token == NULL) { + return false; + } + if (!amduatd_cap_decode_token(bytes, len, &token, + &hmac_offset, &hmac_len, &hmac)) { + return false; + } + if (hmac_len != sizeof(expected)) { + *out_token = token; + return false; + } + if (!amduatd_hmac_sha256(cap->hmac_key, + cap->hmac_key_len, + bytes, + hmac_offset, + expected) || + !amduatd_ct_memcmp(expected, hmac, sizeof(expected))) { + if (out_reason != NULL) { + *out_reason = "bad-mac"; + } + *out_token = token; + return false; + } + + now = time(NULL); + if (now < 0 || token.expiry_unix < (uint64_t)now) { + if (out_reason != NULL) { + *out_reason = "expired"; + } + *out_token = token; + return false; + } + + if (dcfg->space_enabled) { + if (token.space_len == 0u || token.space == NULL || + strcmp(token.space, dcfg->space_id) != 0) { + if (out_reason != NULL) { + *out_reason = "wrong-space"; + } + *out_token = token; + return false; + } + } else { + if (token.space_len != 0u) { + if (out_reason != NULL) { + *out_reason = "wrong-space"; + } + *out_token = token; + return false; + } + } + if (token.kind == AMDUATD_CAP_KIND_POINTER_NAME && + !amduatd_pointer_name_scoped_for_space(dcfg, token.pointer_name)) { + if (out_reason != NULL) { + *out_reason = "wrong-space"; + } + *out_token = token; + return false; + } + + *out_token = token; + if (out_reason != NULL) { + *out_reason = "ok"; + } + return true; +} + static bool amduatd_build_prefixed_bytes(const char *prefix, const char *text, uint8_t **out, @@ -2836,6 +3827,7 @@ typedef struct { char content_type[128]; char accept[128]; char x_type_tag[64]; + char x_capability[2048]; size_t content_length; bool has_actor; amduat_octets_t actor; @@ -3035,6 +4027,12 @@ static bool amduatd_http_parse_request(int fd, amduatd_http_req_t *out_req) { v++; } strncpy(out_req->x_type_tag, v, sizeof(out_req->x_type_tag) - 1); + } else if (strncasecmp(line, "X-Amduat-Capability:", 20) == 0) { + const char *v = line + 20; + while (*v == ' ' || *v == '\t') { + v++; + } + strncpy(out_req->x_capability, v, sizeof(out_req->x_capability) - 1); } } @@ -8186,6 +9184,688 @@ static bool amduatd_handle_get_resolve(int fd, return amduatd_http_send_json(fd, 200, "OK", json, false); } +static bool amduatd_handle_post_capabilities( + int fd, + amduat_asl_store_t *store, + const amduat_asl_store_fs_config_t *cfg, + const amduatd_concepts_t *concepts, + const amduatd_cfg_t *dcfg, + const amduatd_cap_state_t *cap, + const amduatd_http_req_t *req) { + uint8_t *body = NULL; + const char *p = NULL; + const char *end = NULL; + bool have_kind = false; + bool have_target = false; + bool have_expiry = false; + char *kind = NULL; + uint64_t expiry_seconds = 0u; + amduatd_cap_token_t token; + bool target_have_ref = false; + bool target_have_name = false; + bool target_have_program_ref = false; + bool target_have_inputs = false; + bool target_have_params_ref = false; + size_t input_refs_cap = 0u; + uint8_t *token_bytes = NULL; + size_t token_len = 0u; + char *token_b64 = NULL; + char *token_text = NULL; + char token_hash[9]; + char json[4096]; + bool ok = false; + const char *space = NULL; + size_t space_len = 0u; + time_t now; + + memset(&token, 0, sizeof(token)); + memset(token_hash, 0, sizeof(token_hash)); + + if (store == NULL || cfg == NULL || concepts == NULL || dcfg == NULL || + cap == NULL || req == NULL) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "internal error"); + } + if (!cap->enabled) { + return amduatd_send_json_error(fd, 403, "Forbidden", + "capabilities disabled"); + } + if (req->content_length == 0) { + return amduatd_send_json_error(fd, 400, "Bad Request", "missing body"); + } + if (req->content_length > (256u * 1024u)) { + return amduatd_send_json_error(fd, 413, "Payload Too Large", + "payload too large"); + } + + body = (uint8_t *)malloc(req->content_length); + if (body == NULL) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", "oom"); + } + if (!amduatd_read_exact(fd, body, req->content_length)) { + free(body); + return false; + } + p = (const char *)body; + end = (const char *)body + req->content_length; + if (!amduatd_json_expect(&p, end, '{')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid json"); + goto cap_mint_cleanup; + } + + for (;;) { + const char *key = NULL; + size_t key_len = 0; + const char *cur = NULL; + + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + if (!amduatd_json_parse_string_noesc(&p, end, &key, &key_len) || + !amduatd_json_expect(&p, end, ':')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid json"); + goto cap_mint_cleanup; + } + + if (key_len == strlen("kind") && memcmp(key, "kind", key_len) == 0) { + const char *sv = NULL; + size_t sv_len = 0; + if (have_kind || + !amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len) || + !amduatd_copy_json_str(sv, sv_len, &kind)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid kind"); + goto cap_mint_cleanup; + } + have_kind = true; + } else if (key_len == strlen("expiry_seconds") && + memcmp(key, "expiry_seconds", key_len) == 0) { + if (have_expiry || !amduatd_json_parse_u64(&p, end, &expiry_seconds)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid expiry_seconds"); + goto cap_mint_cleanup; + } + have_expiry = true; + } else if (key_len == strlen("target") && + memcmp(key, "target", key_len) == 0) { + if (have_target || !amduatd_json_expect(&p, end, '{')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid target"); + goto cap_mint_cleanup; + } + for (;;) { + const char *tkey = NULL; + size_t tkey_len = 0; + const char *sv = NULL; + size_t sv_len = 0; + + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + if (!amduatd_json_parse_string_noesc(&p, end, &tkey, &tkey_len) || + !amduatd_json_expect(&p, end, ':')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid target"); + goto cap_mint_cleanup; + } + + if (tkey_len == strlen("ref") && memcmp(tkey, "ref", tkey_len) == 0) { + amduatd_ref_status_t st; + if (target_have_ref || + !amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid ref"); + goto cap_mint_cleanup; + } + st = amduatd_decode_ref_or_name_latest(store, + cfg, + concepts, + dcfg, + sv, + sv_len, + &token.artifact_ref); + if (st != AMDUATD_REF_OK) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid ref"); + goto cap_mint_cleanup; + } + target_have_ref = true; + } else if (tkey_len == strlen("name") && + memcmp(tkey, "name", tkey_len) == 0) { + if (target_have_name || + !amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len) || + !amduatd_copy_json_str(sv, sv_len, &token.pointer_name)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid name"); + goto cap_mint_cleanup; + } + target_have_name = true; + } else if (tkey_len == strlen("program_ref") && + memcmp(tkey, "program_ref", tkey_len) == 0) { + amduatd_ref_status_t st; + if (target_have_program_ref || + !amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid program_ref"); + goto cap_mint_cleanup; + } + st = amduatd_decode_ref_or_name_latest(store, + cfg, + concepts, + dcfg, + sv, + sv_len, + &token.program_ref); + if (st != AMDUATD_REF_OK) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid program_ref"); + goto cap_mint_cleanup; + } + target_have_program_ref = true; + } else if (tkey_len == strlen("input_refs") && + memcmp(tkey, "input_refs", tkey_len) == 0) { + if (target_have_inputs || !amduatd_json_expect(&p, end, '[')) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid input_refs"); + goto cap_mint_cleanup; + } + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == ']') { + p = cur + 1; + target_have_inputs = true; + } else { + for (;;) { + amduat_reference_t ref; + amduatd_ref_status_t st; + if (!amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid input_refs"); + goto cap_mint_cleanup; + } + memset(&ref, 0, sizeof(ref)); + st = amduatd_decode_ref_or_name_latest(store, + cfg, + concepts, + dcfg, + sv, + sv_len, + &ref); + if (st != AMDUATD_REF_OK) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid input_ref"); + goto cap_mint_cleanup; + } + if (token.input_refs_len == 0u) { + input_refs_cap = 4u; + token.input_refs = (amduat_reference_t *)calloc( + input_refs_cap, sizeof(*token.input_refs)); + if (token.input_refs == NULL) { + amduat_reference_free(&ref); + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "oom"); + goto cap_mint_cleanup; + } + token.input_refs_len = 0u; + } else if (token.input_refs_len == input_refs_cap) { + size_t next_cap = input_refs_cap * 2u; + amduat_reference_t *next = (amduat_reference_t *)realloc( + token.input_refs, next_cap * sizeof(*token.input_refs)); + if (next == NULL) { + amduat_reference_free(&ref); + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "oom"); + goto cap_mint_cleanup; + } + token.input_refs = next; + input_refs_cap = next_cap; + } + token.input_refs[token.input_refs_len++] = ref; + target_have_inputs = true; + + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == ',') { + p = cur + 1; + continue; + } + if (cur < end && *cur == ']') { + p = cur + 1; + target_have_inputs = true; + break; + } + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid input_refs"); + goto cap_mint_cleanup; + } + } + } else if (tkey_len == strlen("params_ref") && + memcmp(tkey, "params_ref", tkey_len) == 0) { + amduatd_ref_status_t st; + if (target_have_params_ref || + !amduatd_json_parse_string_noesc(&p, end, &sv, &sv_len)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid params_ref"); + goto cap_mint_cleanup; + } + st = amduatd_decode_ref_or_name_latest(store, + cfg, + concepts, + dcfg, + sv, + sv_len, + &token.params_ref); + if (st != AMDUATD_REF_OK) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid params_ref"); + goto cap_mint_cleanup; + } + token.has_params_ref = true; + target_have_params_ref = true; + } else { + if (!amduatd_json_skip_value(&p, end, 0)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid target"); + goto cap_mint_cleanup; + } + } + + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == ',') { + p = cur + 1; + continue; + } + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid target"); + goto cap_mint_cleanup; + } + have_target = true; + } else { + if (!amduatd_json_skip_value(&p, end, 0)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid json"); + goto cap_mint_cleanup; + } + } + + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == ',') { + p = cur + 1; + continue; + } + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid json"); + goto cap_mint_cleanup; + } + + if (!have_kind || !have_target || !have_expiry) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "missing fields"); + goto cap_mint_cleanup; + } + if (expiry_seconds == 0u || expiry_seconds > AMDUATD_CAP_MAX_EXPIRY_SECONDS) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "expiry_seconds out of range"); + goto cap_mint_cleanup; + } + now = time(NULL); + if (now < 0 || + expiry_seconds > (uint64_t)(UINT64_MAX - (uint64_t)now)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "expiry_seconds out of range"); + goto cap_mint_cleanup; + } + token.expiry_unix = (uint64_t)now + expiry_seconds; + + if (strcmp(kind, "artifact_ref") == 0) { + token.kind = AMDUATD_CAP_KIND_ARTIFACT_REF; + if (token.artifact_ref.hash_id == 0 || + token.artifact_ref.digest.data == NULL) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "missing ref"); + goto cap_mint_cleanup; + } + } else if (strcmp(kind, "pointer_name") == 0) { + token.kind = AMDUATD_CAP_KIND_POINTER_NAME; + if (token.pointer_name == NULL || + !amduatd_pointer_name_scoped_for_space(dcfg, token.pointer_name)) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "invalid name"); + goto cap_mint_cleanup; + } + } else if (strcmp(kind, "pel_view") == 0) { + token.kind = AMDUATD_CAP_KIND_PEL_VIEW; + if (token.program_ref.hash_id == 0 || + token.program_ref.digest.data == NULL) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "missing program_ref"); + goto cap_mint_cleanup; + } + if (!target_have_inputs) { + ok = amduatd_send_json_error(fd, 400, "Bad Request", + "missing input_refs"); + goto cap_mint_cleanup; + } + } else { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid kind"); + goto cap_mint_cleanup; + } + + if (dcfg->space_enabled && dcfg->space_id != NULL) { + space = dcfg->space_id; + space_len = strlen(space); + token.space = (char *)malloc(space_len + 1u); + if (token.space == NULL) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", "oom"); + goto cap_mint_cleanup; + } + memcpy(token.space, space, space_len); + token.space[space_len] = '\0'; + token.space_len = space_len; + } + + if (!amduatd_cap_build_token(&token, + cap->hmac_key, + cap->hmac_key_len, + &token_bytes, + &token_len)) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "token encode failed"); + goto cap_mint_cleanup; + } + if (!amduatd_b64url_encode(token_bytes, token_len, &token_b64)) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "token encode failed"); + goto cap_mint_cleanup; + } + token_text = (char *)malloc(strlen(token_b64) + 5u); + if (token_text == NULL) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", "oom"); + goto cap_mint_cleanup; + } + snprintf(token_text, strlen(token_b64) + 5u, "cap_%s", token_b64); + + amduatd_cap_token_hash_prefix(token_bytes, token_len, token_hash); + { + char uid_buf[32]; + if (req->has_uid) { + snprintf(uid_buf, sizeof(uid_buf), "%u", (unsigned int)req->uid); + } else { + strncpy(uid_buf, "unknown", sizeof(uid_buf) - 1u); + uid_buf[sizeof(uid_buf) - 1u] = '\0'; + } + amduat_log(AMDUAT_LOG_INFO, + "cap mint uid=%s kind=%s expiry=%llu space=%s token=%s", + uid_buf, + amduatd_cap_kind_name(token.kind), + (unsigned long long)token.expiry_unix, + token.space_len != 0u ? token.space : "none", + token_hash); + } + + { + int n = snprintf(json, sizeof(json), "{\"token\":\"%s\"}\n", token_text); + if (n <= 0 || (size_t)n >= sizeof(json)) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", "error"); + goto cap_mint_cleanup; + } + ok = amduatd_http_send_json(fd, 200, "OK", json, false); + } + +cap_mint_cleanup: + free(body); + free(kind); + free(token_bytes); + free(token_b64); + free(token_text); + amduatd_cap_token_free(&token); + return ok; +} + +static bool amduatd_handle_get_cap_resolve( + int fd, + amduat_asl_store_t *store, + const amduat_asl_store_fs_config_t *cfg, + const amduatd_concepts_t *concepts, + const amduatd_cfg_t *dcfg, + const amduatd_cap_state_t *cap, + const char *root_path, + const amduatd_http_req_t *req) { + const char *cap_header = NULL; + uint8_t *token_bytes = NULL; + size_t token_len = 0u; + amduatd_cap_token_t token; + const char *reason = "invalid"; + char token_hash[9]; + bool ok = false; + + memset(&token, 0, sizeof(token)); + memset(token_hash, 0, sizeof(token_hash)); + + if (store == NULL || cfg == NULL || concepts == NULL || dcfg == NULL || + cap == NULL || req == NULL || root_path == NULL) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "internal error"); + } + (void)cfg; + (void)concepts; + if (!cap->enabled || !cap->reads_enabled) { + return amduatd_send_json_error(fd, 403, "Forbidden", + "cap reads disabled"); + } + + cap_header = req->x_capability; + if (cap_header == NULL || cap_header[0] == '\0') { + amduat_log(AMDUAT_LOG_INFO, "cap use fail reason=missing token=none"); + return amduatd_send_json_error(fd, 401, "Unauthorized", + "missing capability"); + } + if (strncmp(cap_header, "cap_", 4u) != 0) { + amduat_log(AMDUAT_LOG_INFO, "cap use fail reason=bad-prefix token=none"); + return amduatd_send_json_error(fd, 401, "Unauthorized", + "invalid capability"); + } + if (!amduatd_b64url_decode(cap_header + 4u, &token_bytes, &token_len)) { + amduat_log(AMDUAT_LOG_INFO, "cap use fail reason=bad-base64 token=none"); + return amduatd_send_json_error(fd, 401, "Unauthorized", + "invalid capability"); + } + + amduatd_cap_token_hash_prefix(token_bytes, token_len, token_hash); + if (!amduatd_cap_verify_token(cap, dcfg, token_bytes, token_len, + &token, &reason)) { + amduat_log(AMDUAT_LOG_INFO, + "cap use fail reason=%s kind=%s space=%s token=%s", + reason, + token.kind != 0u ? amduatd_cap_kind_name(token.kind) : "unknown", + token.space_len != 0u && token.space != NULL ? token.space : "none", + token_hash); + free(token_bytes); + amduatd_cap_token_free(&token); + return amduatd_send_json_error(fd, 403, "Forbidden", + "invalid capability"); + } + + amduat_log(AMDUAT_LOG_INFO, + "cap use ok kind=%s space=%s token=%s", + amduatd_cap_kind_name(token.kind), + token.space_len != 0u ? token.space : "none", + token_hash); + + if (token.kind == AMDUATD_CAP_KIND_ARTIFACT_REF) { + char *ref_hex = NULL; + char path[1024]; + if (!amduat_asl_ref_encode_hex(token.artifact_ref, &ref_hex)) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "encode error"); + goto cap_resolve_cleanup; + } + if (snprintf(path, sizeof(path), "/v1/artifacts/%s", ref_hex) <= 0 || + strlen(path) >= sizeof(path)) { + free(ref_hex); + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", "error"); + goto cap_resolve_cleanup; + } + ok = amduatd_handle_get_artifact(fd, store, req, path, false); + free(ref_hex); + goto cap_resolve_cleanup; + } else if (token.kind == AMDUATD_CAP_KIND_POINTER_NAME) { + amduat_asl_pointer_store_t pointer_store; + amduat_reference_t ref; + bool exists = false; + char format[32]; + memset(&ref, 0, sizeof(ref)); + if (!amduat_asl_pointer_store_init(&pointer_store, root_path)) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "pointer store error"); + goto cap_resolve_cleanup; + } + if (amduat_asl_pointer_get(&pointer_store, token.pointer_name, + &exists, &ref) != AMDUAT_ASL_POINTER_OK) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "pointer read failed"); + goto cap_resolve_cleanup; + } + if (!exists) { + ok = amduatd_send_json_error(fd, 404, "Not Found", "not found"); + goto cap_resolve_cleanup; + } + memset(format, 0, sizeof(format)); + if (amduatd_query_param(req->path, "format", format, sizeof(format)) != NULL && + strcmp(format, "artifact") == 0) { + char *ref_hex = NULL; + char path[1024]; + if (!amduat_asl_ref_encode_hex(ref, &ref_hex)) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "encode error"); + amduat_reference_free(&ref); + goto cap_resolve_cleanup; + } + if (snprintf(path, sizeof(path), "/v1/artifacts/%s", ref_hex) <= 0 || + strlen(path) >= sizeof(path)) { + free(ref_hex); + amduat_reference_free(&ref); + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "error"); + goto cap_resolve_cleanup; + } + ok = amduatd_handle_get_artifact(fd, store, req, path, false); + free(ref_hex); + amduat_reference_free(&ref); + goto cap_resolve_cleanup; + } else { + char *ref_hex = NULL; + char json_out[2048]; + int n; + if (!amduat_asl_ref_encode_hex(ref, &ref_hex)) { + amduat_reference_free(&ref); + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "encode error"); + goto cap_resolve_cleanup; + } + n = snprintf(json_out, sizeof(json_out), + "{\"ref\":\"%s\"}\n", ref_hex); + free(ref_hex); + amduat_reference_free(&ref); + if (n <= 0 || (size_t)n >= sizeof(json_out)) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "error"); + goto cap_resolve_cleanup; + } + ok = amduatd_http_send_json(fd, 200, "OK", json_out, false); + goto cap_resolve_cleanup; + } + } else if (token.kind == AMDUATD_CAP_KIND_PEL_VIEW) { + amduat_pel_run_result_t run_result; + char *result_hex = NULL; + const char *status = "UNKNOWN"; + amduatd_strbuf_t resp; + size_t i; + memset(&run_result, 0, sizeof(run_result)); + memset(&resp, 0, sizeof(resp)); + if (!amduat_pel_surf_run_with_result( + store, + amduat_pel_program_dag_scheme_ref(), + token.program_ref, + token.input_refs, + token.input_refs_len, + token.has_params_ref, + token.params_ref, + &run_result)) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "pel run failed"); + goto cap_view_cleanup; + } + if (!run_result.has_result_value) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "pel run failed"); + goto cap_view_cleanup; + } + status = amduat_format_pel_status_name( + run_result.result_value.core_result.status); + if (!amduat_asl_ref_encode_hex(run_result.result_ref, &result_hex)) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "encode error"); + goto cap_view_cleanup; + } + if (!amduatd_strbuf_append_cstr(&resp, "{\"result_ref\":\"") || + !amduatd_strbuf_append_cstr(&resp, result_hex) || + !amduatd_strbuf_append_cstr(&resp, "\",\"output_refs\":[")) { + free(result_hex); + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", "oom"); + goto cap_view_cleanup; + } + free(result_hex); + for (i = 0; i < run_result.output_refs_len; ++i) { + char *out_hex = NULL; + if (!amduat_asl_ref_encode_hex(run_result.output_refs[i], &out_hex)) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", + "encode error"); + goto cap_view_cleanup; + } + if (i != 0) { + (void)amduatd_strbuf_append_char(&resp, ','); + } + (void)amduatd_strbuf_append_cstr(&resp, "\""); + (void)amduatd_strbuf_append_cstr(&resp, out_hex); + (void)amduatd_strbuf_append_cstr(&resp, "\""); + free(out_hex); + } + if (!amduatd_strbuf_append_cstr(&resp, "],\"status\":\"") || + !amduatd_strbuf_append_cstr(&resp, status) || + !amduatd_strbuf_append_cstr(&resp, "\"}\n")) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", "oom"); + goto cap_view_cleanup; + } + ok = amduatd_http_send_json(fd, 200, "OK", resp.data, false); + goto cap_view_cleanup; +cap_view_cleanup: + amduatd_strbuf_free(&resp); + if (run_result.has_result_value) { + amduat_enc_pel1_result_free(&run_result.result_value); + } + if (run_result.output_refs != NULL) { + amduat_pel_surf_free_refs(run_result.output_refs, + run_result.output_refs_len); + } + amduat_pel_surf_free_ref(&run_result.result_ref); + goto cap_resolve_cleanup; + } else { + ok = amduatd_send_json_error(fd, 400, "Bad Request", "invalid capability"); + } + +cap_resolve_cleanup: + free(token_bytes); + amduatd_cap_token_free(&token); + return ok; +} + static bool amduatd_handle_conn(int fd, amduat_asl_store_t *store, const amduat_asl_store_fs_config_t *cfg, @@ -8194,7 +9874,9 @@ static bool amduatd_handle_conn(int fd, amduatd_concepts_t *concepts, const amduatd_cfg_t *dcfg, const amduat_fed_coord_t *coord, - const amduatd_allowlist_t *allowlist) { + const amduatd_allowlist_t *allowlist, + const amduatd_cap_state_t *cap_state, + const char *root_path) { amduatd_http_req_t req; char no_query[1024]; bool ok = false; @@ -8217,7 +9899,11 @@ static bool amduatd_handle_conn(int fd, req.has_uid = has_uid; req.uid = uid; - if (!amduatd_actor_allowed(allowlist, has_uid, uid)) { + amduatd_path_without_query(req.path, no_query, sizeof(no_query)); + + if (!(strcmp(req.method, "GET") == 0 && + strcmp(no_query, "/v1/cap/resolve") == 0) && + !amduatd_actor_allowed(allowlist, has_uid, uid)) { amduat_log(AMDUAT_LOG_DEBUG, "reject uid=%u (not in allowlist)", (unsigned int)uid); @@ -8225,8 +9911,6 @@ static bool amduatd_handle_conn(int fd, goto conn_cleanup; } - amduatd_path_without_query(req.path, no_query, sizeof(no_query)); - if (strcmp(req.method, "GET") == 0 && strcmp(no_query, "/v1/ui") == 0) { ok = amduatd_handle_get_ui(fd, store, ui_ref); goto conn_cleanup; @@ -8259,6 +9943,17 @@ static bool amduatd_handle_conn(int fd, ok = amduatd_handle_post_artifacts(fd, store, &req); goto conn_cleanup; } + if (strcmp(req.method, "POST") == 0 && + strcmp(no_query, "/v1/capabilities") == 0) { + ok = amduatd_handle_post_capabilities(fd, + store, + cfg, + concepts, + dcfg, + cap_state, + &req); + goto conn_cleanup; + } if (strcmp(req.method, "POST") == 0 && strcmp(no_query, "/v1/pel/run") == 0) { ok = amduatd_handle_post_pel_run(fd, store, cfg, concepts, dcfg, &req); goto conn_cleanup; @@ -8329,6 +10024,18 @@ static bool amduatd_handle_conn(int fd, ok = amduatd_handle_get_resolve(fd, store, cfg, concepts, dcfg, name); goto conn_cleanup; } + if (strcmp(req.method, "GET") == 0 && + strcmp(no_query, "/v1/cap/resolve") == 0) { + ok = amduatd_handle_get_cap_resolve(fd, + store, + cfg, + concepts, + dcfg, + cap_state, + root_path, + &req); + goto conn_cleanup; + } if (strcmp(req.method, "GET") == 0 && strncmp(no_query, "/v1/artifacts/", 14) == 0) { ok = amduatd_handle_get_artifact(fd, store, &req, req.path, false); @@ -8353,6 +10060,7 @@ static void amduatd_print_usage(FILE *stream) { " amduatd [--root PATH] [--sock PATH]\n" " [--space SPACE_ID] [--migrate-unscoped-edges]\n" " [--allow-uid UID] [--allow-user NAME]\n" + " [--enable-cap-reads]\n" "\n" "defaults:\n" " --root %s\n" @@ -8366,7 +10074,9 @@ int main(int argc, char **argv) { const char *sock_path = AMDUATD_DEFAULT_SOCK; const char *space_id = NULL; bool migrate_unscoped_edges = false; + bool enable_cap_reads = false; amduatd_cfg_t dcfg; + amduatd_cap_state_t cap_state; amduat_asl_store_fs_config_t cfg; amduat_asl_store_fs_t fs; amduat_asl_store_t store; @@ -8386,6 +10096,7 @@ int main(int argc, char **argv) { memset(&concepts, 0, sizeof(concepts)); memset(&allowlist, 0, sizeof(allowlist)); memset(&dcfg, 0, sizeof(dcfg)); + memset(&cap_state, 0, sizeof(cap_state)); for (i = 1; i < argc; ++i) { if (strcmp(argv[i], "--root") == 0) { @@ -8443,6 +10154,8 @@ int main(int argc, char **argv) { fprintf(stderr, "error: failed to add allow-user\n"); return 2; } + } else if (strcmp(argv[i], "--enable-cap-reads") == 0) { + enable_cap_reads = true; } else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { amduatd_print_usage(stdout); return 0; @@ -8468,6 +10181,10 @@ int main(int argc, char **argv) { return 8; } amduat_asl_store_init(&store, cfg.config, amduat_asl_store_fs_ops(), &fs); + if (!amduatd_cap_init(root, &cap_state)) { + amduat_log(AMDUAT_LOG_WARN, "capabilities unavailable"); + } + cap_state.reads_enabled = enable_cap_reads; if (!amduatd_seed_api_contract(&store, &cfg, &api_contract_ref)) { fprintf(stderr, "error: failed to seed api contract\n"); @@ -8597,7 +10314,9 @@ int main(int argc, char **argv) { &concepts, &dcfg, fed_coord, - &allowlist); + &allowlist, + &cap_state, + root); (void)close(cfd); } }