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
79 changes: 73 additions & 6 deletions .github/workflows/build_and_deploy.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
name: Build report and deploy site

# Compiles document/main.tex to site/report.pdf and deploys the site via GitHub Pages
# Pipeline: generate -> verify -> build -> deploy.
#
# Generated artifacts (sweep CSVs, site/data.json, report figures, report.pdf) are NOT tracked in git

# Requires: Settings -> Pages -> Source = "GitHub Actions".

on:
push:
Expand All @@ -17,15 +21,74 @@ concurrency:
group: pages
cancel-in-progress: true

env:
GENERATED_PATHS: |
utils/all_size_capped_candidates.csv
utils/all_unbound_candidates.csv
utils/global_X_thresholds.csv
utils/master_extremes_summary.csv
site/data.json
document/content/images/sieve_step*.png
document/content/images/concentric_diagram.png
document/content/images/scatter_plot.png
document/content/images/comparison_regular.png
document/content/images/comparison_normalized.png
document/content/images/ideal_candidate_balanced.png
document/content/images/ideal_candidate_best.png

jobs:
generate:
runs-on: ubuntu-latest
timeout-minutes: 90
container:
image: sagemath/sagemath:10.4 # pinned; bump deliberately and re-verify
options: --user root # image's default user cannot write the runner workspace
steps:
- uses: actions/checkout@v4

- name: Restore generated artifacts (keyed on generator sources)
id: cache
uses: actions/cache@v4
with:
path: ${{ env.GENERATED_PATHS }}
key: generated-v1-${{ hashFiles('costs.sage', 'security.sage', 'slhdsa-2to40.sage', 'stateful.sage', 'export_site_data.sage', 'octopus_pmf.py', 'utils/*.py', 'Makefile') }}

- name: Generate (sweeps, derived tables, data.json, figures)
if: steps.cache.outputs.cache-hit != 'true'
run: |
apt-get update -qq && apt-get install -y -qq make
sage -pip install --quiet --no-cache-dir pandas seaborn
make sweeps derived data figures PYTHON="sage -python"

- name: Upload generated artifacts
uses: actions/upload-artifact@v4
with:
name: generated
path: ${{ env.GENERATED_PATHS }}

build_and_deploy:
needs: generate
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Set up Git repository
uses: actions/checkout@v4
- uses: actions/checkout@v4

- name: Place generated artifacts into the tree
uses: actions/download-artifact@v4
with:
name: generated

- uses: actions/setup-python@v5
with:
python-version: '3.12'
- uses: actions/setup-node@v4
with:
node-version: '22'

- name: Regression suite (generated artifacts required)
run: GENERATED_REQUIRED=1 python3 tests/test_regression.py

- name: Compile report
uses: xu-cheng/latex-action@v3
Expand All @@ -39,14 +102,18 @@ jobs:
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: . # whole repo, preserving the /site/... URL structure
path: . # whole tree, preserving the /site/... URL structure

- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

- name: Attach report to release (tags only)
- name: Attach data + report to release (tags only)
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v2
with:
files: site/report.pdf
files: |
site/report.pdf
site/data.json
utils/all_size_capped_candidates.csv
utils/all_unbound_candidates.csv
15 changes: 14 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,22 @@

*.sage.py

# Generated sweep outputs
# Generated artifacts
utils/outputs_specialized/
candidates.csv
utils/all_size_capped_candidates.csv
utils/all_unbound_candidates.csv
utils/global_X_thresholds.csv
utils/master_extremes_summary.csv
site/data.json
document/main.pdf
document/content/images/sieve_step*.png
document/content/images/concentric_diagram.png
document/content/images/scatter_plot.png
document/content/images/comparison_regular.png
document/content/images/comparison_normalized.png
document/content/images/ideal_candidate_balanced.png
document/content/images/ideal_candidate_best.png

# Compiled report — built and deployed by CI (.github/workflows/build_and_deploy.yml)
site/report.pdf
Expand Down
74 changes: 74 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Single entrypoint for regenerating every derived artifact.
#
# make generate — everything below, in dependency order
# make sweeps — the two parameter sweeps -> utils/*.csv (sage)
# make derived — thresholds + extremes from the sweep CSVs (python)
# make data — site/data.json for both site pages (sage)
# make figures — report figures from the generated data (python)
# make test — regression suite (validates artifacts <-> model)
# make clean-generated

SAGE ?= sage
PYTHON ?= python3

UTILS := utils
IMAGES := document/content/images

CSV_CAPPED := $(UTILS)/all_size_capped_candidates.csv
CSV_UNBOUND := $(UTILS)/all_unbound_candidates.csv

.PHONY: generate sweeps derived data figures test clean-generated

generate: sweeps derived data figures test

sweeps:
$(SAGE) slhdsa-2to40.sage --max-size 7856 > /dev/null
mv candidates.csv $(CSV_CAPPED)
$(SAGE) slhdsa-2to40.sage --max-size 99999999 > /dev/null
mv candidates.csv $(CSV_UNBOUND)

derived:
cd $(UTILS) && $(PYTHON) min_mult_search.py
$(PYTHON) $(UTILS)/extremes_summary.py

data:
$(SAGE) export_site_data.sage

figures:
cd $(IMAGES) && $(PYTHON) ../../../$(UTILS)/sieve_search_draw.py \
--input ../../../$(CSV_UNBOUND) --weights 1,1,1,1,1 --suffix _1
cd $(IMAGES) && $(PYTHON) ../../../$(UTILS)/sieve_search_draw.py \
--input ../../../$(CSV_UNBOUND) --weights 5,1,1.5,2,1 --suffix _2
cd $(UTILS) && $(PYTHON) venn_diagram_draw.py
mv $(UTILS)/euler_custom_minimax.png $(IMAGES)/concentric_diagram.png
cd $(IMAGES) && $(PYTHON) ../../../$(UTILS)/plot_concept_scatter.py
mv $(IMAGES)/conceptual_bounded_zones_origin_vector.png $(IMAGES)/scatter_plot.png
# Report Sec. "Metric-Normalized Weighted Distance": method comparison
cd $(IMAGES) && $(PYTHON) ../../../$(UTILS)/sieve_search_draw.py \
--input ../../../$(CSV_UNBOUND) --figure focus --distance regular \
--x-size 0.75 --x-kg 5 --x-sg 5 --x-sv 1.5 --x-cb 1 \
--weights 5,1.5,1.5,2,1 --out comparison_regular.png
cd $(IMAGES) && $(PYTHON) ../../../$(UTILS)/sieve_search_draw.py \
--input ../../../$(CSV_UNBOUND) --figure focus --distance normalized \
--x-size 0.75 --x-kg 5 --x-sg 5 --x-sv 1.5 --x-cb 1 \
--weights 5,1.5,1.5,2,1 --out comparison_normalized.png
# Report Sec. "Recommended Candidate Tuple": the two surviving candidates
cd $(IMAGES) && $(PYTHON) ../../../$(UTILS)/sieve_search_draw.py \
--input ../../../$(CSV_UNBOUND) --figure focus --distance regular --annotate \
--x-size 0.74 --x-kg 2 --x-sg 2 --x-sv 0.8 --x-cb 1 \
--weights 5,1,1,2,1 --out ideal_candidate_balanced.png
cd $(IMAGES) && $(PYTHON) ../../../$(UTILS)/sieve_search_draw.py \
--input ../../../$(CSV_UNBOUND) --figure focus --distance normalized --annotate \
--x-size 0.74 --x-kg 2 --x-sg 2 --x-sv 0.8 --x-cb 1 \
--weights 5,1,1,2,1 --out ideal_candidate_best.png

test:
$(PYTHON) tests/test_regression.py

clean-generated:
rm -f $(CSV_CAPPED) $(CSV_UNBOUND)
rm -f $(UTILS)/global_X_thresholds.csv $(UTILS)/master_extremes_summary.csv
rm -f site/data.json site/report.pdf
rm -f $(IMAGES)/sieve_step*.png $(IMAGES)/concentric_diagram.png $(IMAGES)/scatter_plot.png
rm -f $(IMAGES)/comparison_regular.png $(IMAGES)/comparison_normalized.png
rm -f $(IMAGES)/ideal_candidate_balanced.png $(IMAGES)/ideal_candidate_best.png
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Scripts for exploring SPHINCS+ parameter tradeoffs, computing security levels, s
- [octopus_pmf.py](octopus_pmf.py) computes the PMF of the Octopus authentication set size (MIT license, from https://github.com/MehdiAbri/PORS-FP)
- [slhdsa-2to40.sage](slhdsa-2to40.sage) sweeps SLH-DSA parameter tuples and filters them by security level and signature size
- [run_experiments.py](run_experiments.py) runs the full parameter sweep and writes results to CSV
- [export_site_data.sage](export_site_data.sage) exports [site/data.json](site/data.json), the dataset both site pages load (run after regenerating the sweep CSVs)
- [export_site_data.sage](export_site_data.sage) exports `site/data.json`, the dataset both site pages load
- [utils/](utils/) helper scripts for searching and visualising the parameter space
- [tests/](tests/) regression suite, run `python3 tests/test_regression.py`
- [document/](document/) LaTeX source for the accompanying technical report
Expand Down
29 changes: 22 additions & 7 deletions costs.sage
Original file line number Diff line number Diff line change
Expand Up @@ -39,23 +39,38 @@ hashbytes = 16 # 16 bytes = 128 bits
counter_size = 4 # 4 bytes = 32 bits
randomness_size = 16 # 16 bytes = 128 bits

# Compression function calls per hash operation
# SHA-256 block size = 512 bits, with 65 bits for padding/length
# Each value = number of compression calls for that operation
# -----------------------------------------------------------------------------
# Hash-cost convention
# -----------------------------------------------------------------------------
# Two SHA-256 compression-counting conventions are supported, selected via the
# HASH_CONVENTION environment variable ('cached' or 'uncached'):
#
# cached (default): FIPS 205 SHA-2 layout with the PK.seed midstate cached
#
# uncached: the original convention of this script: every call pays for its full input (2 compressions)
#
# Usage: HASH_CONVENTION=uncached sage costs.sage --params ...
#
HASH_CONVENTION = os.environ.get('HASH_CONVENTION', 'cached').lower()
assert HASH_CONVENTION in ('cached', 'uncached'), (
"HASH_CONVENTION must be 'cached' or 'uncached', got %r" % HASH_CONVENTION)

C_Th1 = 1 # Tweakable hash, 1-block: PKseed (128) + Tweak (96) + m1 (128)
C_Th1c = 1 # Tweakable hash, 1-block + counter: PKseed + Tweak + m1 + counter (32)
C_Th2 = 2 # Tweakable hash, 2-block: PKseed + Tweak + m1 + m2
C_Th2 = 1 if HASH_CONVENTION == 'cached' else 2 # Tweakable hash with hash convention
C_Hmsg = 2 # Message hash: PKseed + PKroot (128) + R (256) + m (256)
C_PRFmsg = 2 # Message PRF: SKprf (128) + Opt + m (256) + counter
C_PRF = 1 # PRF: PKseed + SKseed + Tweak

def compute_Th(n):
"""
Compute compression calls for tweakable hash of n values.
Compression calls for a tweakable hash over n hash-sized values, under the active HASH_CONVENTION

Input: PKseed (128) + Tweak (96) + n*hashbytes (n*128) + padding (65)
SHA-256 block size: 512 bits
cached: ceil((22*8 + 128*n + 65) / 512)
uncached: ceil((128 + 96 + 128*n + 65) / 512)
"""
if HASH_CONVENTION == 'cached':
return ceil((22*8 + 128*n + 65)/512)
return ceil((128 + 96 + 128*n + 65)/512)

# =============================================================================
Expand Down
Loading
Loading