mirror of
https://github.com/luxfi/pulsar.git
synced 2026-09-24 00:25:52 +00:00
Single repo, one canonical impl + spec + proofs + KAT + cut tool.
Per "ONE and ONLY ONE way to do things" the prior dual-repo split
(luxfi/pulsar = production library, luxfi/pulsar-mptc = NIST MPTC
submission framework) is collapsed into a single canonical home at
github.com/luxfi/pulsar. SINGLE-IMPL-PLAN.md (now marked COMPLETED)
describes the steps; this commit lands the final step.
Files moved (89 new):
16 submission docs (SUBMISSION, NIST-SUBMISSION, SPEC, SUITE,
PATENTS, AXIOM-INVENTORY, PROOF-CLAIMS, FIPS-TRACEABILITY,
TRUSTED-COMPUTING-BASE, HANZO-CRYPTO-SUITE, INFORMATION-
ARCHITECTURE, ROADMAP, CHANGELOG, SYNC-STATUS, STATUS-
SUBMISSION-READINESS, SINGLE-IMPL-PLAN)
10 docs/ supplements (ietf-draft-skeleton, magnetar, evaluation,
patent-claims, x-wing-sig, design-decisions, family-architecture,
threat-model, nist-mptc-category, patent-notes-draft)
19 proofs/ (13 EC theories + 2 EC lemmas + 4 metadata/READMEs +
lean-easycrypt-bridge.md)
14 jasmin/ (lib/* + ml-dsa-65 fetch + threshold/{combine,round1,
round2}.jazz + READMEs)
10 ct/dudect/ (Makefile, combine_ct.go, dudect_combine.c,
dudect_compat.h, dudect_verify.c, fetch.sh, README, run-
submission.sh, verify_ct.go) + ct/jasmin-ct-libjade.md
1 .github/workflows/ci.yml
1 test/interoperability/n1_class_test.go (19/19 N1 subtests)
1 bench/results/REPORT.md
11 scripts/ (cut-submission, check-high-assurance, check-lean-
bridge, nightly, extract-jasmin-ec, checks/{ec-admits, ec-
compile, ec-refinement-scaffold, ec-regressions, extraction,
jasmin}, checks/test/{go-unit, interop, kat, no-secret-logs})
Files merged (16 overlapping, mptc-canonical chosen because newer +
post-rewire-correct; pulsar versions still bore stale "Pulsar-M" /
"pulsarm" branding):
README.md, BLOCKERS.md, CONTRIBUTING.md, LICENSE, LICENSING.md,
SECURITY.md, .gitignore, bench/run_all.sh, vectors/dkg.json (v1.0.7
regen), scripts/{bench,build,gen_vectors,sbom,test}.sh,
spec/{pulsar,parameters,system-model}.tex,
CRYPTOGRAPHER-SIGN-OFF.md (path refs).
Pulsar-only spec supplements preserved:
spec/{blockers,design-decisions,family-architecture,nist-mptc-
category,patent-notes,threat-model}.tex.
Path rewrites applied: 158 occurrences across 26 files
github.com/luxfi/pulsar-mptc -> github.com/luxfi/pulsar
~/work/lux/pulsar-mptc/ -> ~/work/lux/pulsar/
luxfi/pulsar-mptc -> luxfi/pulsar
pulsar-mptc/ -> ./
Narrative collapses (no more two-repo framing):
SUBMISSION.md "At a glance" — single Repository row
SYNC-STATUS.md — rewritten to single-repo state
SINGLE-IMPL-PLAN.md — marked COMPLETED 2026-05-18
HANZO-CRYPTO-SUITE.md — tier 1 row = single canonical home
scripts/cut-submission.sh — replace/vendor dance removed; cuts
directly from in-tree canonical sources
go.mod: dropped mptc's replace directive (no longer needed since this
IS the canonical pulsar repo). Module path is github.com/luxfi/pulsar.
Deps: cloudflare/circl v1.6.3, golang.org/x/crypto v0.32.0.
Verification:
GOWORK=off go build ./... PASS
GOWORK=off go test -count=1 ./... PASS (1.4s)
GOWORK=off go test -race ./ref/... PASS (14.7s)
go test ./test/interoperability/ PASS (19/19 N1 subtests)
scripts/check-high-assurance.sh PASS
scripts/check-lean-bridge.sh PASS (5/5 bridges)
scripts/gen_vectors.sh byte-identical replay
scripts/cut-submission.sh --help PASS
167 lines
6.1 KiB
C
167 lines
6.1 KiB
C
/*
|
|
* Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
|
* See the file LICENSE for licensing terms.
|
|
*
|
|
* dudect_verify.c — dudect main loop driving pulsarm.Verify through
|
|
* the cgo bridge in verify_ct.go.
|
|
*
|
|
* dudect API contract: the user provides do_one_computation(data) and
|
|
* prepare_inputs(cfg, input, classes); dudect's `dudect_main` invokes
|
|
* them via the extern declarations at the bottom of dudect.h. See
|
|
* https://github.com/oreparaz/dudect/blob/master/src/dudect.h
|
|
*
|
|
* We define DUDECT_IMPLEMENTATION exactly once before including the
|
|
* header so the whole library compiles into this translation unit.
|
|
*
|
|
* dudect_compat.h is `-include`-d by the Makefile on AArch64 hosts;
|
|
* on x86 it is a no-op.
|
|
*/
|
|
|
|
#define DUDECT_IMPLEMENTATION
|
|
#include "dudect/src/dudect.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
/* Exported by libpulsar_verify (verify_ct.go). */
|
|
extern int pulsar_verify_ct_setup(void);
|
|
extern size_t pulsar_verify_ct_sig_size(void);
|
|
extern size_t pulsar_verify_ct_pool_size(void);
|
|
extern int pulsar_verify_ct_copy_pool(size_t idx, uint8_t *dst);
|
|
extern void pulsar_verify_ct(uint8_t *data);
|
|
|
|
/* Per-sample input width. Filled in main() after setup. */
|
|
static size_t g_chunk_size = 0;
|
|
static size_t g_pool_size = 0;
|
|
|
|
/*
|
|
* dudect API hook: populate cfg->number_measurements samples of
|
|
* cfg->chunk_size bytes each, and assign each sample a binary class
|
|
* label (0 = fixed class A, 1 = random class B). dudect supplies
|
|
* randombytes() / randombit() for us — same RNG it uses internally,
|
|
* so the class assignment is uncorrelated with anything Verify could
|
|
* see.
|
|
*
|
|
* Both classes are VALID signatures on the same (pk, message) —
|
|
* this is the OPERATIONALLY MEANINGFUL CT population for Verify
|
|
* (valid sigs are what an attacker can observe in a real
|
|
* exchange; Verify holds no secret, so timing differences over
|
|
* attacker-supplied garbage are not a confidentiality property).
|
|
* The valid-sig framing is a test-design choice, not a FIPS 204
|
|
* standard mandate — see verify_ct.go header for the full
|
|
* framing. Class A is always pool[0] (byte-identical across
|
|
* class-A samples as Welch's t-test requires); class B is
|
|
* pool[rand % pool_size] (varying valid sigs, drawn uniformly
|
|
* from the precomputed pool).
|
|
*
|
|
* Any timing difference detected by dudect under this design is a
|
|
* REAL data-dependent path in pulsar.Verify (i.e., circl's
|
|
* mldsa65.Verify), not a rejection-path artifact.
|
|
*/
|
|
void prepare_inputs(dudect_config_t *cfg, uint8_t *input_data, uint8_t *classes) {
|
|
for (size_t i = 0; i < cfg->number_measurements; i++) {
|
|
classes[i] = randombit();
|
|
uint8_t *slot = input_data + (size_t)i * cfg->chunk_size;
|
|
if (classes[i] == 0) {
|
|
/* Class A: pool[0] every time (Welch's t-test requires
|
|
* identical class-A inputs). */
|
|
(void)pulsar_verify_ct_copy_pool(0, slot);
|
|
} else {
|
|
/* Class B: a uniformly-drawn valid sig from the pool.
|
|
* dudect's randombit() / randombytes() are independent
|
|
* of anything Verify can observe. */
|
|
uint8_t pick_buf[8];
|
|
randombytes(pick_buf, sizeof pick_buf);
|
|
uint64_t pick = 0;
|
|
for (size_t k = 0; k < sizeof pick_buf; k++) {
|
|
pick = (pick << 8) | pick_buf[k];
|
|
}
|
|
(void)pulsar_verify_ct_copy_pool((size_t)(pick % g_pool_size), slot);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* dudect API hook: one measurement sample. Must depend on `data` (so
|
|
* the compiler cannot dead-code-eliminate the body), must NOT branch
|
|
* on `data` (or we measure our own branch, not Verify's), and must
|
|
* return a uint8_t that flows out of the function so the result is
|
|
* observed.
|
|
*/
|
|
uint8_t do_one_computation(uint8_t *data) {
|
|
pulsar_verify_ct(data);
|
|
uint8_t acc = 0;
|
|
for (size_t i = 0; i < g_chunk_size; i++) acc ^= data[i];
|
|
return acc;
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
(void)argc;
|
|
(void)argv;
|
|
|
|
int rc = pulsar_verify_ct_setup();
|
|
if (rc != 0) {
|
|
fprintf(stderr, "pulsar_verify_ct_setup failed: rc=%d\n", rc);
|
|
return 1;
|
|
}
|
|
g_chunk_size = pulsar_verify_ct_sig_size();
|
|
if (g_chunk_size == 0) {
|
|
fprintf(stderr, "pulsar_verify_ct_sig_size returned 0\n");
|
|
return 1;
|
|
}
|
|
g_pool_size = pulsar_verify_ct_pool_size();
|
|
if (g_pool_size == 0) {
|
|
fprintf(stderr, "pulsar_verify_ct_pool_size returned 0\n");
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
* SMOKE-TEST default (10 000 samples per batch). The full NIST
|
|
* submission run uses ~10^9 samples on a quiet, CPU-pinned host.
|
|
* Override per-batch sample count with DUDECT_SAMPLES, and limit
|
|
* total batches with DUDECT_MAX_BATCHES so smoke tests terminate
|
|
* even when the t-test stays under threshold.
|
|
*/
|
|
size_t number_measurements = 10000;
|
|
const char *env_n = getenv("DUDECT_SAMPLES");
|
|
if (env_n) {
|
|
long n = strtol(env_n, NULL, 10);
|
|
if (n > 0) number_measurements = (size_t)n;
|
|
}
|
|
size_t max_batches = 4;
|
|
const char *env_b = getenv("DUDECT_MAX_BATCHES");
|
|
if (env_b) {
|
|
long b = strtol(env_b, NULL, 10);
|
|
if (b > 0) max_batches = (size_t)b;
|
|
}
|
|
|
|
dudect_config_t cfg = {
|
|
.chunk_size = g_chunk_size,
|
|
.number_measurements = number_measurements,
|
|
};
|
|
dudect_ctx_t ctx;
|
|
if (dudect_init(&ctx, &cfg) != 0) {
|
|
fprintf(stderr, "dudect_init failed\n");
|
|
return 1;
|
|
}
|
|
|
|
fprintf(stderr, "dudect_verify: ML-DSA-65, chunk=%zu bytes, batch=%zu samples, max_batches=%zu, pool=%zu valid sigs\n",
|
|
g_chunk_size, number_measurements, max_batches, g_pool_size);
|
|
|
|
dudect_state_t state = DUDECT_NO_LEAKAGE_EVIDENCE_YET;
|
|
for (size_t batch = 0; batch < max_batches; batch++) {
|
|
state = dudect_main(&ctx);
|
|
if (state == DUDECT_LEAKAGE_FOUND) break;
|
|
}
|
|
dudect_free(&ctx);
|
|
|
|
if (state == DUDECT_LEAKAGE_FOUND) {
|
|
fprintf(stderr, "dudect_verify: LEAKAGE FOUND (t-statistic exceeded threshold)\n");
|
|
return 2;
|
|
}
|
|
fprintf(stderr, "dudect_verify: no leakage evidence after %zu batches of %zu samples\n",
|
|
max_batches, number_measurements);
|
|
return 0;
|
|
}
|