Skip to content

Commit 46b86f4

Browse files
authored
Constant time release evidence
* ci: bind equality evidence to exact release binaries Inspect production comparison paths after final linking and bind call closure, timing, formal results, provenance, and artifacts to the exact release identity. Reject ancestor evidence for a different release commit. * build: migrate to cargo-rail 0.18 Replace removed config synchronization with semantic migration checks while preserving disabled MSRV policy, push-only release effects, and the tagged v5.1 action contract.
1 parent b0563c6 commit 46b86f4

37 files changed

Lines changed: 2007 additions & 217 deletions
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"rscrypto" = "patch"
3+
---
4+
5+
Constant-time release evidence now inspects production equality paths in final
6+
linked binaries and binds disassembly, symbols, timing, formal results,
7+
provenance, and hashes to the exact release commit and build configuration.

.config/rail.toml

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,42 +28,24 @@ targets = [
2828
include_paths = true
2929
include_renamed = true
3030

31-
pin_transitives = false
32-
transitive_host = "root"
33-
3431
strict_version_compat = true
3532
exact_pin_handling = "warn"
3633
major_version_conflict = "warn"
37-
38-
msrv = false # Root package MSRV is authoritative; this single-crate workspace does not inherit workspace.package.
39-
msrv_source = "max"
40-
41-
detect_unused = true
42-
compiler_diag_cache = true
43-
remove_unused = true
44-
45-
prune_dead_features = true
4634
preserve_features = []
47-
48-
detect_undeclared_features = true
49-
fix_undeclared_features = true
5035
skip_undeclared_patterns = []
5136

5237
exclude = []
5338
include = []
5439
max_backups = 2
55-
sort_dependencies = true
56-
enforce_msrv_inheritance = false
5740
consumer_scope = "open" # Published library: preserve dormant public feature and dependency APIs.
41+
msrv_policy = { mode = "disabled" } # Root package MSRV remains authoritative in this single-crate workspace.
5842

5943

6044
[release]
6145
tag_prefix = "v"
6246
tag_format = "{prefix}{version}" # Single crate: vX.Y.Z
6347

6448
require_clean = true
65-
push = true
66-
create_github_release = false
6749
sign_tags = true
6850
publish_delay = 5
6951

@@ -75,7 +57,7 @@ unconventional_commits = "allow" # Required .changes files own bumps and user-f
7557
semver_check = "warn"
7658
change_dir = ".changes"
7759
require_change_files = ["rscrypto"]
78-
forge = "auto" # Release creation provider: auto, github, gitlab
60+
remote_effects = "push" # Release automation pushes commits/tags; the repository workflow owns forge releases.
7961

8062
[release.changelog]
8163
path = "CHANGELOG.md"
@@ -107,7 +89,6 @@ infrastructure = [
10789
]
10890
unknown_file_policy = "strict" # Unknown crate files build/test; unknown workspace files run infrastructure checks.
10991
confidence_profile = "balanced" # Planner confidence profile: strict, balanced, fast
110-
bot_pr_confidence_profile = "strict" # Optional planner profile override for bot-authored PRs
11192

11293
[change-detection.custom]
11394
cargo_graph = [

.github/actions/setup/action.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ runs:
6262
if: inputs.tools-mode != 'none'
6363
shell: bash
6464
env:
65-
CARGO_RAIL_VERSION: "0.17.3"
65+
CARGO_RAIL_VERSION: "0.18.0"
6666
CARGO_SEMVER_CHECKS_VERSION: "0.48.0"
6767
TOOLS_MODE: ${{ inputs.tools-mode }}
6868
run: scripts/ci/install-tools.sh "$TOOLS_MODE"

.github/workflows/_ci-suite.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ jobs:
7777
run_script: |
7878
mkdir -p target/cargo-rail
7979
cargo rail config validate --strict
80-
cargo rail config sync --check
80+
cargo rail config migrate --check
8181
cargo rail unify --check --explain --format json \
8282
--output target/cargo-rail/unify-result.json
8383
artifact_name: cargo-graph-${{ inputs.cache_key_prefix }}

.github/workflows/ci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ jobs:
4141
id: rail
4242
uses: loadingalias/cargo-rail-action@90f7f739028887508c6e8d8b7cc510dc612eb653 # v5.1.0
4343
with:
44-
version: "0.17.3"
44+
version: "0.18.0"
4545

4646
- name: Check Release Intent Coverage
4747
if: github.event_name == 'pull_request'

.github/workflows/ct.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ jobs:
170170
if [[ "$raw_artifacts" == "true" ]]; then
171171
package_args+=(--raw)
172172
fi
173-
python3 scripts/ct/package_evidence.py "${package_args[@]}"
173+
scripts/ct/python.sh scripts/ct/package_evidence.py "${package_args[@]}"
174174
175175
exit "$status"
176176
artifact_name: ct-${{ matrix.artifact_suffix }}

ct.toml

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,61 @@ release_report = "ct-report.json"
88
harness = "tools/ct-harness"
99
artifact_script = "scripts/ct/artifacts.sh"
1010

11+
[equality_evidence.release_binary]
12+
name = "rscrypto-ct-evidence"
13+
kind = "executable"
14+
profile = "release"
15+
backend = "llvm"
16+
features = ["std", "full", "parallel", "diag"]
17+
default_features = false
18+
owner_widths = [16, 28, 32, 48, 64, 1632, 2400, 3168]
19+
owner_symbols = [
20+
"ct_entry_owner_eq_16",
21+
"ct_entry_owner_eq_28",
22+
"ct_entry_owner_eq_32",
23+
"ct_entry_owner_eq_48",
24+
"ct_entry_owner_eq_64",
25+
"ct_entry_owner_eq_1632",
26+
"ct_entry_owner_eq_2400",
27+
"ct_entry_owner_eq_3168",
28+
]
29+
public_len_symbols = [
30+
"ct_entry_kmac256_verify",
31+
"ct_entry_mlkem512_decapsulate",
32+
"ct_entry_mlkem768_decapsulate",
33+
"ct_entry_mlkem1024_decapsulate",
34+
"ct_entry_argon2i_verify",
35+
"ct_entry_argon2d_verify",
36+
"ct_entry_argon2id_verify",
37+
"ct_entry_scrypt_verify",
38+
"ct_entry_rsa_pkcs1v15_sign_fixed_blinding",
39+
"ct_entry_rsa_pss_sign_fixed_blinding",
40+
"ct_entry_rsa_oaep_decrypt_fixed_blinding",
41+
"ct_entry_rsa_pkcs1v15_decrypt_fixed_blinding",
42+
"ct_entry_rsa_private_key_pkcs8_roundtrip",
43+
]
44+
formal_owner_widths = [16, 32, 48, 64]
45+
formal_limitation = "BINSEC owner kernels cover the representative 16-, 32-, 48-, and 64-byte production monomorphizations. The 28- and ML-KEM-sized owner monomorphizations require linked-binary disassembly/closure and heuristics on every release lane; BINSEC proof kernels are deferred because they add no caller-selectable shape and the large copies exceed the present proof budget."
46+
downstream_limitation = "Evidence binds this final linked harness executable and its exact compiler/linker configuration. It does not claim constant-time behavior for arbitrary downstream binaries that rebuild or inline rscrypto differently. Returning bool remains the T3.4 declassification limitation."
47+
48+
[[asm_public_operand]]
49+
primitive = "password.argon2i"
50+
root = "ct_entry_argon2i_verify"
51+
symbol = "rscrypto::auth::argon2::argon2_hash_with_kernel_inner"
52+
kind = "variable_latency_division"
53+
max_count = 2
54+
source = "src/auth/argon2/mod.rs:1751"
55+
rationale = "The divisions derive matrix dimensions and iteration bounds from validated public Argon2Params and the caller-visible output length. Password and salt contents do not feed the divisors."
56+
57+
[[asm_public_operand]]
58+
primitive = "password.argon2i"
59+
root = "ct_entry_argon2i_verify"
60+
symbol = "rscrypto::auth::argon2::fill_segment_inner"
61+
kind = "variable_latency_division"
62+
max_count = 2
63+
source = "src/auth/argon2/mod.rs:1434"
64+
rationale = "For the Argon2i retained root, address generation and segment positions are data-independent; these divisions use public lane, segment, and matrix dimensions rather than password-derived contents."
65+
1166
[operation_inventory]
1267
schema_version = 1
1368
authority = "ct.toml"
@@ -33,6 +88,9 @@ operations = ["kmac.variable_output_and_verify"]
3388
public_length = "The caller selects KMAC output length and supplies an expected buffer of that public length."
3489
secret_contents = "The computed KMAC output and expected bytes are traversed fully for each public-length chunk."
3590
tests = "KMAC NIST/Wycheproof/differential tests plus kmac256_valid_vs_invalid_tag DudeCT."
91+
evidence_symbols = ["ct_entry_kmac256_verify"]
92+
evidenced_call_count = 1
93+
limited_call_count = 0
3694

3795
[[public_len_comparison]]
3896
id = "rsa.public_modulus_and_profile_shapes"
@@ -42,6 +100,16 @@ operations = ["rsa.private_key_import_export_and_generation", "rsa.private_sign_
42100
public_length = "Widths derive from the public modulus, digest algorithm, DER/padding profile, or caller-visible encoded length."
43101
secret_contents = "Private representatives, decoded plaintext/hash fields, or validation intermediates are compared without content-dependent exits after public shape checks."
44102
tests = "RSA official/Wycheproof/differential/private-operation tests and RSA CT harness/DudeCT cases."
103+
evidence_symbols = [
104+
"ct_entry_rsa_pkcs1v15_sign_fixed_blinding",
105+
"ct_entry_rsa_pss_sign_fixed_blinding",
106+
"ct_entry_rsa_oaep_decrypt_fixed_blinding",
107+
"ct_entry_rsa_pkcs1v15_decrypt_fixed_blinding",
108+
"ct_entry_rsa_private_key_pkcs8_roundtrip",
109+
]
110+
evidenced_call_count = 7
111+
limited_call_count = 5
112+
limitation = "The two Miller-Rabin key-generation comparisons and three public verification comparisons are not reachable from the retained release equality roots. Their lengths remain public and their source traversal is audited, but T3.3 does not convert that source review into linked-binary evidence."
45113

46114
[[public_len_comparison]]
47115
id = "mlkem.parameter_set_shapes"
@@ -51,6 +119,13 @@ operations = ["kem.mlkem512", "kem.mlkem768", "kem.mlkem1024"]
51119
public_length = "Encoded key/ciphertext widths are fixed by the public ML-KEM parameter set and validated before comparison."
52120
secret_contents = "Decapsulation implicit-rejection ciphertext/key material is traversed fully at that public parameter-set width."
53121
tests = "ML-KEM ACVP, differential, property, operation, CT harness, and DudeCT cases for all parameter sets."
122+
evidence_symbols = [
123+
"ct_entry_mlkem512_decapsulate",
124+
"ct_entry_mlkem768_decapsulate",
125+
"ct_entry_mlkem1024_decapsulate",
126+
]
127+
evidenced_call_count = 1
128+
limited_call_count = 0
54129

55130
[[public_len_comparison]]
56131
id = "argon2.caller_selected_output"
@@ -60,6 +135,9 @@ operations = ["password.argon2i", "password.argon2d_and_argon2id"]
60135
public_length = "Primitive verification derives exactly the caller-supplied expected output length after public bounds checks."
61136
secret_contents = "The freshly derived password verifier and expected bytes are traversed fully before the temporary is cleared."
62137
tests = "Argon2 vectors/differentials/parallel tests and variant CT harness/DudeCT cases."
138+
evidence_symbols = ["ct_entry_argon2i_verify", "ct_entry_argon2d_verify", "ct_entry_argon2id_verify"]
139+
evidenced_call_count = 1
140+
limited_call_count = 0
63141

64142
[[public_len_comparison]]
65143
id = "scrypt.caller_selected_output"
@@ -69,6 +147,9 @@ operations = ["password.scrypt"]
69147
public_length = "Primitive verification derives exactly the caller-supplied expected output length after public minimum-length checks."
70148
secret_contents = "The freshly derived password verifier and expected bytes are traversed fully before the temporary is cleared."
71149
tests = "scrypt vectors/differentials and verification CT harness/DudeCT cases."
150+
evidence_symbols = ["ct_entry_scrypt_verify"]
151+
evidenced_call_count = 1
152+
limited_call_count = 0
72153

73154
[status]
74155
ct_claimed = "all required evidence has passed for the exact primitive/configuration pair"
@@ -113,9 +194,13 @@ kind = "staticlib"
113194
status = "covered"
114195
symbols = [
115196
"ct_entry_owner_eq_16",
197+
"ct_entry_owner_eq_28",
116198
"ct_entry_owner_eq_32",
117199
"ct_entry_owner_eq_48",
118200
"ct_entry_owner_eq_64",
201+
"ct_entry_owner_eq_1632",
202+
"ct_entry_owner_eq_2400",
203+
"ct_entry_owner_eq_3168",
119204
"ct_entry_secret_bytes32_debug_masked",
120205
"ct_entry_hmac_sha256_verify",
121206
"ct_entry_hmac_sha384_verify",
@@ -3110,6 +3195,7 @@ tier = "A"
31103195
claim = "ct-intended"
31113196
features = ["always"]
31123197
variants = ["Owner16", "Owner32", "Owner48", "Owner64"]
3198+
release_owner_widths = [16, 28, 32, 48, 64, 1632, 2400, 3168]
31133199
entrypoints = [
31143200
"Aes128GcmKey::eq",
31153201
"X25519SecretKey::eq",
@@ -3125,7 +3211,16 @@ required = ["tier_a"]
31253211
notes = "Public callers select a semantic owner, never a raw secret length. Source structure and black_box are not machine-code proofs; T3.3 owns exact release-binary evidence. Returning bool remains an explicit T3.4 limitation."
31263212
[primitive.harness]
31273213
status = "covered"
3128-
symbols = ["ct_entry_owner_eq_16", "ct_entry_owner_eq_32", "ct_entry_owner_eq_48", "ct_entry_owner_eq_64"]
3214+
symbols = [
3215+
"ct_entry_owner_eq_16",
3216+
"ct_entry_owner_eq_28",
3217+
"ct_entry_owner_eq_32",
3218+
"ct_entry_owner_eq_48",
3219+
"ct_entry_owner_eq_64",
3220+
"ct_entry_owner_eq_1632",
3221+
"ct_entry_owner_eq_2400",
3222+
"ct_entry_owner_eq_3168",
3223+
]
31293224

31303225
[[primitive]]
31313226
id = "secret_wrappers.exposure_and_display"

docs/constant-time.md

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,12 @@ evidence set includes:
146146

147147
- Stable harness entrypoints.
148148
- Build and host provenance.
149-
- LLVM IR, assembly, object, and symbol artifacts.
149+
- LLVM IR, assembly, pre-link objects, and symbol artifacts.
150+
- A fat-LTO final linked equality evidence executable, its exact linker
151+
command and linker identity, and post-link disassembly, symbols, and size.
150152
- Automated checks for suspicious generated-code patterns.
151-
- Empirical timing tests on native executable targets.
153+
- Empirical timing tests on native executable targets, bound to the hashed
154+
timing executable, disassembly, symbol map, and linker command.
152155
- ML-KEM DudeCT cases for key generation secret noise, encapsulation coins,
153156
decapsulation secret keys, implicit rejection, NTT, inverse NTT,
154157
product-domain conversion, basemul/dot products, and compress/decompress
@@ -157,6 +160,20 @@ evidence set includes:
157160
paths.
158161
- Miri and unsafe-code validation where the CT path uses unsafe Rust.
159162

163+
The linked equality executable retains production owner comparisons at the
164+
distinct 16-, 28-, 32-, 48-, 64-, 1632-, 2400-, and 3168-byte owner widths.
165+
The required owner timing cases remain the manifest-declared 16-, 32-, 48-,
166+
and 64-byte cases. Public-length internal comparisons are mapped in `ct.toml`
167+
to retained production entrypoints or to an explicit limitation; an uncovered
168+
call is not silently treated as binary evidence.
169+
170+
This executable is an unpublished evidence surface. It proves only its exact
171+
source, toolchain, backend, target, target features, feature set, profile, and
172+
linker configuration. It is not the crate's public API, it is not a sealed
173+
decision type, and it does not generalize to arbitrary downstream binaries.
174+
Equality still returns `bool`; that declassification limitation remains until
175+
T3.4.
176+
160177
Assembly triage is grouped by primitive, reachable symbol, finding kind, and
161178
artifact. Register-indexed memory is presented first, then conditional control
162179
flow, then indirect calls. `needs-binsec` means operand provenance remains
@@ -168,7 +185,8 @@ date. Source or disassembly movement invalidates it.
168185
BINSEC is required on the GNU Linux targets supported by the workflow. Every
169186
manifest-required kernel must report `secure`. Other target reports record
170187
BINSEC as `not_applicable` with the target policy reason; that status is not
171-
binary proof.
188+
binary proof. Each formal result is bound to its hashed proof driver,
189+
disassembly, configuration, solver log, candidate identity, and toolchain.
172190

173191
Statistical timing checks must be described precisely:
174192

docs/release.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ receive anything, the workflow:
167167
2. Revalidates configuration, the unified Cargo graph, dependency policy,
168168
audit results, SemVer, and successful CI for the exact commit.
169169
3. Requires complete Weekly CT/RSA and RISC-V native/CT evidence from that
170-
commit or a mechanically proven release-tooling-only ancestor.
170+
exact commit and crate version.
171171
4. Builds the `.crate` once, reproduces the source archive from the tag, and
172172
rejects dirty, private, local-only, or mismatched package contents.
173173
5. Captures repository controls and writes provenance attestations, an identity
@@ -176,11 +176,10 @@ receive anything, the workflow:
176176
crates.io token through OIDC, publishes the same crate, then downloads it
177177
from crates.io and verifies its SHA-256.
178178

179-
A release-tooling-only repair may reuse the newest successful ancestor with
180-
paired Weekly and RISC-V evidence. The checker permits only changelog,
181-
release/CT tooling, root-package version, and normalized local CT lockfile
182-
version changes. Runtime, dependency, feature, build, or test changes invalidate
183-
that exception.
179+
Any change after the evidence run—including a version-only or release-tooling
180+
change—creates a new release candidate and requires fresh paired Weekly and
181+
RISC-V evidence. Ancestor binaries are never promoted into an exact-commit
182+
constant-time claim.
184183

185184
## Recovery
186185

justfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ check-zeroize-evidence:
112112

113113
check-unify:
114114
cargo rail config validate --strict
115-
cargo rail config sync --check
115+
cargo rail config migrate --check
116116
cargo rail unify --check --explain
117117

118118
ci-check:

0 commit comments

Comments
 (0)