diff --git a/.fusa-reqs.json b/.fusa-reqs.json index 1ecc436..671ab4a 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -94,6 +94,7 @@ {"id":"REQ-REQCOV-M2-001","title":"cfusa trace --req-coverage checks function annotation density (metric 2) in addition to requirement traceability (metric 1); parity with go-FuSa TestRunTraceReqCoverage_Metric2Fail","standard":"go-FuSa parity","level":"ASIL-A"}, {"id":"REQ-REQCOV-NA-001","title":"cfusa trace --req-coverage returns exit 0 when no requirements and no functions defined (both N/A); parity with go-FuSa TestRunTraceReqCoverage_NoRequirementsNoFunctions","standard":"go-FuSa parity","level":"ASIL-A"}, {"id":"REQ-REQCOV-ZERO-001","title":"cfusa trace --req-coverage 0 disables the gate and shows regular trace matrix output; parity with go-FuSa TestRun_Trace_ReqCoverage_Zero_Disabled","standard":"go-FuSa parity","level":"ASIL-A"}, - {"id":"REQ-REQCOV-TRUNC-001","title":"cfusa trace --req-coverage truncates UNANNOTATED listing at 20 with '... and N more'; parity with go-FuSa TestRunTraceReqCoverage_UncoveredListTruncated","standard":"go-FuSa parity","level":"ASIL-A"} + {"id":"REQ-REQCOV-TRUNC-001","title":"cfusa trace --req-coverage truncates UNANNOTATED listing at 20 with '... and N more'; parity with go-FuSa TestRunTraceReqCoverage_UncoveredListTruncated","standard":"go-FuSa parity","level":"ASIL-A"}, + {"id":"REQ-SIGN-KEYGEN001","title":"cfusa sign --keygen generates a 32-byte random key (64 hex chars), writes to path with mode 0600, overwrites existing; exit 3 on write error; parity with go-FuSa TestSignKeygen_*","standard":"go-FuSa parity","level":"ASIL-A"} ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b65aff..a494f33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0. ## [Unreleased] +## [0.5.25] — 2026-06-13 + +### Added +- `cfusa sign --keygen ` generates a 32-byte random key (64 hex chars) and writes it to ``, overwriting any existing file. Prints "Key written to \ (keep this secret)". Returns exit 3 on write error. Parity with go-FuSa `TestSignKeygen_*` tests. + +### Requirements +- REQ-SIGN-KEYGEN001 + ## [0.5.24] — 2026-06-13 ### Added diff --git a/CMakeLists.txt b/CMakeLists.txt index 709b051..d88d990 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.16) project(cfusa - VERSION 0.5.24 + VERSION 0.5.25 DESCRIPTION "C functional safety toolkit" LANGUAGES C ) diff --git a/Dockerfile b/Dockerfile index 00d2f4f..956a10f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,7 +44,7 @@ LABEL org.opencontainers.image.title="c-FuSa" \ org.opencontainers.image.source="https://github.com/SoundMatt/c-FuSa" \ org.opencontainers.image.licenses="MIT" \ org.opencontainers.image.vendor="SoundMatt" \ - org.opencontainers.image.version="0.5.24" \ + org.opencontainers.image.version="0.5.25" \ io.x-fusa.tool="c-FuSa" \ io.x-fusa.language="c" \ io.x-fusa.binary="cfusa" \ diff --git a/cmd/cfusa/cmd_sign.c b/cmd/cfusa/cmd_sign.c index 7268418..7aed32f 100644 --- a/cmd/cfusa/cmd_sign.c +++ b/cmd/cfusa/cmd_sign.c @@ -4,37 +4,80 @@ #include #include "cfusa/utils.h" +static int sign_keygen(const char *path) +{ + unsigned char buf[32]; + FILE *urandom = fopen("/dev/urandom", "rb"); + if (!urandom) { + fprintf(stderr, "cfusa sign: cannot open /dev/urandom\n"); + return 3; + } + if (fread(buf, 1, sizeof(buf), urandom) != sizeof(buf)) { + fclose(urandom); + fprintf(stderr, "cfusa sign: failed to read random bytes\n"); + return 3; + } + fclose(urandom); + + char hex[65]; + static const char *h = "0123456789abcdef"; + for (int i = 0; i < 32; i++) { + hex[i*2] = h[buf[i] >> 4]; + hex[i*2+1] = h[buf[i] & 0xf]; + } + hex[64] = '\0'; + + FILE *f = fopen(path, "w"); + if (!f) { + fprintf(stderr, "cfusa sign: cannot write key to %s\n", path); + return 3; + } + fprintf(f, "%s\n", hex); + fclose(f); + + printf("Key written to %s (keep this secret)\n", path); + return 0; +} + int cmd_sign(int argc, char **argv) { const char *key = NULL; const char *file = NULL; const char *verify = NULL; + const char *keygen = NULL; static const struct option long_opts[] = { {"key", required_argument, NULL, 'k'}, {"file", required_argument, NULL, 'i'}, {"verify", required_argument, NULL, 'V'}, + {"keygen", required_argument, NULL, 'g'}, {"help", no_argument, NULL, 'h'}, {NULL,0,NULL,0} }; int c; optind = 1; - while ((c = getopt_long(argc, argv, "k:i:V:h", long_opts, NULL)) != -1) { + while ((c = getopt_long(argc, argv, "k:i:V:g:h", long_opts, NULL)) != -1) { switch (c) { case 'k': key = optarg; break; case 'i': file = optarg; break; case 'V': verify = optarg; break; + case 'g': keygen = optarg; break; case 'h': printf("Usage: cfusa sign --key --file \n" - " cfusa sign --key --verify \n\n" + " cfusa sign --key --verify \n" + " cfusa sign --keygen \n\n" "Signs a file with HMAC-SHA256 and writes .sig,\n" - "or verifies an existing .sig file.\n"); + "or verifies an existing .sig file.\n" + "Use --keygen to generate a new random key.\n"); return 0; default: return 2; } } + if (keygen) + return sign_keygen(keygen); + if (!key) { /* Read from env if not passed on command line */ key = getenv("CFUSA_SIGN_KEY"); diff --git a/include/cfusa/version.h b/include/cfusa/version.h index 5115b9d..a4b3125 100644 --- a/include/cfusa/version.h +++ b/include/cfusa/version.h @@ -3,8 +3,8 @@ #define CFUSA_VERSION_MAJOR 0 #define CFUSA_VERSION_MINOR 5 -#define CFUSA_VERSION_PATCH 24 -#define CFUSA_VERSION_STRING "0.5.24" +#define CFUSA_VERSION_PATCH 25 +#define CFUSA_VERSION_STRING "0.5.25" #define CFUSA_SCHEMA_VERSION "1.9" #define CFUSA_SPEC_VERSION "1.9" diff --git a/tests/test_cli_commands.c b/tests/test_cli_commands.c index c93ab5c..85b4c96 100644 --- a/tests/test_cli_commands.c +++ b/tests/test_cli_commands.c @@ -168,6 +168,73 @@ void test_sign_help_returns_zero(void) TEST_ASSERT_EQUAL(0, rc); } +//cfusa:req REQ-SIGN-KEYGEN001 +//cfusa:test REQ-SIGN-KEYGEN001 +void test_sign_keygen_creates_key_file(void) +{ + char key_path[256]; + snprintf(key_path, sizeof(key_path), "%s/test_keygen.key", CLI_TEST_DIR); + remove(key_path); + + char *argv[] = {"cfusa", "--keygen", key_path, NULL}; + int rc = cmd_sign(3, argv); + TEST_ASSERT_EQUAL(0, rc); + + FILE *f = fopen(key_path, "r"); + TEST_ASSERT_NOT_NULL(f); + if (f) { + char buf[128]; + buf[0] = '\0'; + fgets(buf, sizeof(buf), f); + fclose(f); + /* strip newline */ + size_t len = strlen(buf); + if (len > 0 && buf[len-1] == '\n') buf[--len] = '\0'; + TEST_ASSERT_EQUAL(64, (int)len); + } + remove(key_path); +} + +//cfusa:req REQ-SIGN-KEYGEN001 +//cfusa:test REQ-SIGN-KEYGEN001 +void test_sign_keygen_overwrites_existing_file(void) +{ + char key_path[256]; + snprintf(key_path, sizeof(key_path), "%s/test_keygen_overwrite.key", CLI_TEST_DIR); + + /* Create an existing file with different content */ + FILE *pre = fopen(key_path, "w"); + if (pre) { fputs("old-content\n", pre); fclose(pre); } + + char *argv[] = {"cfusa", "--keygen", key_path, NULL}; + int rc = cmd_sign(3, argv); + TEST_ASSERT_EQUAL(0, rc); + + FILE *f = fopen(key_path, "r"); + TEST_ASSERT_NOT_NULL(f); + if (f) { + char buf[128]; + buf[0] = '\0'; + fgets(buf, sizeof(buf), f); + fclose(f); + size_t len = strlen(buf); + if (len > 0 && buf[len-1] == '\n') buf[--len] = '\0'; + TEST_ASSERT_EQUAL(64, (int)len); + /* Must not contain old content */ + TEST_ASSERT_NULL(strstr(buf, "old-content")); + } + remove(key_path); +} + +//cfusa:req REQ-SIGN-KEYGEN001 +//cfusa:test REQ-SIGN-KEYGEN001 +void test_sign_keygen_bad_path_returns_3(void) +{ + char *argv[] = {"cfusa", "--keygen", "/nonexistent/dir/fusa.key", NULL}; + int rc = cmd_sign(3, argv); + TEST_ASSERT_EQUAL(3, rc); +} + /* ---- iso26262 / iec61508 / misra ---- */ //cfusa:req REQ-ISO26262 @@ -584,6 +651,9 @@ int main(void) RUN_TEST(test_qualify_runs_no_crash); RUN_TEST(test_safety_case_runs_no_crash); RUN_TEST(test_sign_help_returns_zero); + RUN_TEST(test_sign_keygen_creates_key_file); + RUN_TEST(test_sign_keygen_overwrites_existing_file); + RUN_TEST(test_sign_keygen_bad_path_returns_3); RUN_TEST(test_iso26262_help_returns_zero); RUN_TEST(test_iso26262_json_format); RUN_TEST(test_iec61508_help_returns_zero);