Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .fusa-reqs.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path> 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"}
]
}
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path>` generates a 32-byte random key (64 hex chars) and writes it to `<path>`, overwriting any existing file. Prints "Key written to \<path\> (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
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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
)
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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" \
Expand Down
49 changes: 46 additions & 3 deletions cmd/cfusa/cmd_sign.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,80 @@
#include <getopt.h>
#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)) {

Check warning

Code scanning / cfusa

signed/unsigned comparison with sizeof — sizeof returns size_t (unsigned); compare with (size_t) or use explicit cast (CERT-C INT02-C) Warning

signed/unsigned comparison with sizeof — sizeof returns size_t (unsigned); compare with (size_t) or use explicit cast (CERT-C INT02-C)
fclose(urandom);

Check warning

Code scanning / cfusa

return value of 'fclose' may be unchecked — system call failures must be handled (CERT-C ERR33-C) Warning

return value of 'fclose' may be unchecked — system call failures must be handled (CERT-C ERR33-C)
fprintf(stderr, "cfusa sign: failed to read random bytes\n");
return 3;
}
fclose(urandom);

Check warning

Code scanning / cfusa

return value of 'fclose' may be unchecked — system call failures must be handled (CERT-C ERR33-C) Warning

return value of 'fclose' may be unchecked — system call failures must be handled (CERT-C ERR33-C)

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");

Check failure

Code scanning / CodeQL

File created without restricting permissions High

A file may be created here with mode 0666, which would make it world-writable.
if (!f) {
fprintf(stderr, "cfusa sign: cannot write key to %s\n", path);
return 3;
}
fprintf(f, "%s\n", hex);
fclose(f);

Check warning

Code scanning / cfusa

return value of 'fclose' may be unchecked — system call failures must be handled (CERT-C ERR33-C) Warning

return value of 'fclose' may be unchecked — system call failures must be handled (CERT-C ERR33-C)

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 <secret> --file <path>\n"
" cfusa sign --key <secret> --verify <path.sig>\n\n"
" cfusa sign --key <secret> --verify <path.sig>\n"
" cfusa sign --keygen <keyfile>\n\n"
"Signs a file with HMAC-SHA256 and writes <path>.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");
Expand Down
4 changes: 2 additions & 2 deletions include/cfusa/version.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
70 changes: 70 additions & 0 deletions tests/test_cli_commands.c
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,73 @@
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};

Check notice

Code scanning / cfusa

pointer arithmetic detected — verify bounds (MISRA-C:2012 R18.4) Note test

pointer arithmetic detected — verify bounds (MISRA-C:2012 R18.4)
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);

Check warning

Code scanning / cfusa

return value of 'fclose' may be unchecked — system call failures must be handled (CERT-C ERR33-C) Warning test

return value of 'fclose' may be unchecked — system call failures must be handled (CERT-C ERR33-C)
/* 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");

Check failure

Code scanning / CodeQL

File created without restricting permissions High test

A file may be created here with mode 0666, which would make it world-writable.
if (pre) { fputs("old-content\n", pre); fclose(pre); }

char *argv[] = {"cfusa", "--keygen", key_path, NULL};

Check notice

Code scanning / cfusa

pointer arithmetic detected — verify bounds (MISRA-C:2012 R18.4) Note test

pointer arithmetic detected — verify bounds (MISRA-C:2012 R18.4)
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);

Check warning

Code scanning / cfusa

return value of 'fclose' may be unchecked — system call failures must be handled (CERT-C ERR33-C) Warning test

return value of 'fclose' may be unchecked — system call failures must be handled (CERT-C ERR33-C)
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};

Check notice

Code scanning / cfusa

pointer arithmetic detected — verify bounds (MISRA-C:2012 R18.4) Note test

pointer arithmetic detected — verify bounds (MISRA-C:2012 R18.4)
int rc = cmd_sign(3, argv);
TEST_ASSERT_EQUAL(3, rc);
}

/* ---- iso26262 / iec61508 / misra ---- */

//cfusa:req REQ-ISO26262
Expand Down Expand Up @@ -584,6 +651,9 @@
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);
Expand Down
Loading