hecks 3.6.0 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/bluebook/meta_validator/modes.rb +14 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot/disk_cache.rb +28 -3
- data/lib/hecks/bluebook/meta_validator/verdict_cache/prebuilt.rb +44 -0
- data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +3 -17
- data/lib/hecks/bluebook/meta_validator.rb +4 -2
- data/lib/hecks/cache_dir.rb +10 -0
- data/lib/hecks/corpus/routes.rb +1 -1
- data/lib/hecks/deploy/bluebook/deploy.bluebook +109 -0
- data/lib/hecks/deploy/oidc.json +10 -0
- data/lib/hecks/doors/usage_cache.rb +19 -9
- data/lib/hecks/fuzzing/coverage_campaign.rb +42 -17
- data/lib/hecks/fuzzing/failure_triage.rb +80 -0
- data/lib/hecks/fuzzing/form_census/chapter_forms.rb +38 -0
- data/lib/hecks/fuzzing/form_census/reference_forms.rb +49 -0
- data/lib/hecks/fuzzing/form_census.rb +28 -52
- data/lib/hecks/fuzzing/isolated_boot/postgres_scratch.rb +22 -7
- data/lib/hecks/fuzzing/isolated_boot/scratch_schema.rb +27 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +6 -2
- data/lib/hecks/fuzzing/mutation/checks.rb +76 -0
- data/lib/hecks/fuzzing/mutation/finders.rb +136 -0
- data/lib/hecks/fuzzing/mutation/judge.rb +94 -0
- data/lib/hecks/fuzzing/mutation/observation.rb +38 -0
- data/lib/hecks/fuzzing/mutation/operators.rb +52 -0
- data/lib/hecks/fuzzing/mutation/report.rb +78 -0
- data/lib/hecks/fuzzing/mutation/run.rb +99 -0
- data/lib/hecks/fuzzing/mutation/selection.rb +32 -0
- data/lib/hecks/fuzzing/mutation/site.rb +21 -0
- data/lib/hecks/fuzzing/mutation/source_map.rb +59 -0
- data/lib/hecks/fuzzing/mutation.rb +12 -0
- data/lib/hecks/fuzzing/properties/catalog.rb +10 -59
- data/lib/hecks/fuzzing/properties/guaranteed_by_construction.rb +63 -0
- data/lib/hecks/fuzzing/properties/read_model_names.rb +50 -0
- data/lib/hecks/fuzzing/properties/read_models.rb +138 -0
- data/lib/hecks/fuzzing/properties/role_checks.rb +50 -0
- data/lib/hecks/fuzzing/properties.rb +7 -0
- data/lib/hecks/fuzzing/replay/observation.rb +1 -1
- data/lib/hecks/fuzzing/replay/role_check.rb +49 -0
- data/lib/hecks/fuzzing/replay/session.rb +22 -6
- data/lib/hecks/fuzzing/replay.rb +1 -0
- data/lib/hecks/fuzzing/runtime_coverage.rb +70 -0
- data/lib/hecks/fuzzing/shrinker/value_simplification.rb +64 -0
- data/lib/hecks/fuzzing/shrinker.rb +3 -0
- data/lib/hecks/fuzzing.rb +3 -0
- data/lib/hecks/gate/stages.yml +12 -2
- data/lib/hecks/hecks/adapters/deploy_toolchain/answers.rb +2 -1
- data/lib/hecks/hecks/adapters/github_rulesets.rb +7 -5
- data/lib/hecks/hecks/adapters/process_pool.rb +26 -4
- data/lib/hecks/hecks/adapters/site_toolchain/prober.rb +64 -0
- data/lib/hecks/hecks/adapters/site_toolchain.rb +41 -5
- data/lib/hecks/hecks/custodian.behaviors +6 -0
- data/lib/hecks/hecks/custodian.bluebook +37 -0
- data/lib/hecks/hecks/hecks.hecksagon +6 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +4 -1
- data/lib/hecks/projections/deploy/box/roll_scripts.rb +6 -4
- data/lib/hecks/projections/deploy/box/settings/patterns.rb +4 -0
- data/lib/hecks/projections/deploy/box/settings/types.rb +7 -3
- data/lib/hecks/projections/deploy/box/settings.rb +18 -0
- data/lib/hecks/projections/deploy/box/shared_database.rb +69 -0
- data/lib/hecks/projections/deploy/box/templates/Makefile.tmpl +6 -7
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/box/templates/provision-database.sh.tmpl +97 -0
- data/lib/hecks/projections/deploy/box/templates/restore-to-rds.sh.tmpl +4 -2
- data/lib/hecks/projections/deploy/box/templates/verify-copy.sh.tmpl +2 -1
- data/lib/hecks/projections/deploy/box.rb +14 -2
- data/lib/hecks/projections/deploy/shared_database_instance/templates/Makefile.tmpl +11 -0
- data/lib/hecks/projections/deploy/shared_database_instance/templates/README.md.tmpl +25 -0
- data/lib/hecks/projections/deploy/shared_database_instance/templates/rds.yaml.tmpl +180 -0
- data/lib/hecks/projections/deploy/shared_database_instance.rb +106 -0
- data/lib/hecks/projections/deploy/vercel/settings.rb +83 -0
- data/lib/hecks/projections/deploy/vercel.rb +134 -0
- data/lib/hecks/projections/site/probe.rb +131 -0
- data/lib/hecks/projections/site/site_host/dockerfile.rb +1 -1
- data/lib/hecks/projections/site/site_host/templates.rb +12 -4
- data/lib/hecks/projections.rb +2 -0
- data/lib/hecks/quality_control/adapters/github_checks.rb +9 -2
- data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +1 -0
- data/lib/hecks/quality_control/cli/qa_sweep/seed_loop.rb +10 -2
- data/lib/hecks/runtime/value/validation.rb +34 -3
- data/lib/hecks/rust_build/native_build.rb +29 -1
- data/lib/hecks/rust_build/wasm.rb +16 -3
- data/lib/hecks/site/bluebook/site.bluebook +31 -0
- data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
- data/lib/hecks/tools/deploy_recipe/generation.rb +59 -0
- data/lib/hecks/tools/deploy_recipe/targets.rb +56 -2
- data/lib/hecks/tools/deploy_recipe/worlds.rb +2 -1
- data/lib/hecks/tools/deploy_recipe.rb +20 -10
- data/lib/hecks/tools/fuzz_sweep/options.rb +21 -10
- data/lib/hecks/tools/fuzz_sweep/reporting.rb +25 -3
- data/lib/hecks/tools/fuzz_sweep.rb +26 -9
- data/lib/hecks/tools/lanes/files.rb +2 -4
- data/lib/hecks/tools/lanes/live.rb +3 -4
- data/lib/hecks/tools/lanes/ruleset.rb +23 -1
- data/lib/hecks/tools/lanes.rb +12 -1
- data/lib/hecks/tools/mutation_run.rb +73 -0
- data/lib/hecks/tools.rb +1 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +3 -1
- data/prebuilt/4e6c3cd1f578a38732ea5454dae52562d03d8e9886216077a5c34cbf589f3463.marshal +0 -0
- data/prebuilt/911cdcc56b5a094ed26583a4c76401dba6455b1f434722ed22b850bc1b1bdd28.marshal +0 -0
- data/prebuilt/verdicts-3230a251570aa03666708d16f92e646feab6a3f8cc2a3540c017d748e41d41a1.json +1 -0
- data/qa/settings.yml +1 -0
- data/rust/codegen/src/fielded.rs +3 -3
- data/rust/codegen/src/json_codec.rs +2 -2
- data/rust/codegen/src/naming.rs +20 -0
- data/rust/host/Cargo.toml +6 -2
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/api.rs +28 -5
- data/rust/host/src/auth.rs +11 -1
- data/rust/host/src/boundary_fuzz/api.rs +260 -0
- data/rust/host/src/boundary_fuzz/auth.rs +119 -0
- data/rust/host/src/boundary_fuzz/expr_json.rs +158 -0
- data/rust/host/src/boundary_fuzz/ir.rs +79 -0
- data/rust/host/src/boundary_fuzz/server.rs +227 -0
- data/rust/host/src/boundary_fuzz/web.rs +125 -0
- data/rust/host/src/commerce_ir.rs +459 -0
- data/rust/host/src/extension.rs +100 -0
- data/rust/host/src/fuzz_support.rs +165 -0
- data/rust/host/src/ir.rs +4 -448
- data/rust/host/src/journal.rs +1 -1
- data/rust/host/src/lib.rs +39 -0
- data/rust/host/src/main.rs +10 -44
- data/rust/host/src/mint.rs +1 -1
- data/rust/host/src/payments/tests.rs +8 -8
- data/rust/host/src/payments.rs +10 -10
- data/rust/host/src/rate_limit.rs +27 -41
- data/rust/host/src/secrets.rs +44 -0
- data/rust/host/src/server.rs +6 -1
- data/rust/host/src/web/commerce.rs +113 -0
- data/rust/host/src/web/newsletter.rs +2 -1
- data/rust/host/src/web/newsletter_send.rs +1 -1
- data/rust/host/src/web/registration_receipt.rs +2 -2
- data/rust/host/src/web/registrations.rs +49 -49
- data/rust/host/src/web/resend_webhook.rs +299 -0
- data/rust/host/src/web.rs +55 -49
- data/rust/parser/fuzz/.gitignore +4 -0
- data/rust/parser/fuzz/Cargo.lock +89 -0
- data/rust/parser/fuzz/Cargo.toml +27 -0
- data/rust/parser/fuzz/README.md +19 -0
- data/rust/parser/fuzz/fuzz_targets/chapter.rs +50 -0
- data/rust/parser/fuzz/fuzz_targets/resolve.rs +35 -0
- data/rust/parser/src/parse/command.rs +14 -2
- data/rust/parser/src/parse/mod.rs +25 -0
- data/rust/parser/src/ruby_value.rs +95 -6
- data/rust/src/kernel/json.rs +34 -3
- metadata +58 -2
data/qa/settings.yml
CHANGED
data/rust/codegen/src/fielded.rs
CHANGED
|
@@ -30,7 +30,7 @@ pub fn emit_fielded_flat(exemplar: &Exemplar, struct_name: &str, attributes: &[J
|
|
|
30
30
|
let scalar = scalar.unwrap();
|
|
31
31
|
Some(exemplar.render(
|
|
32
32
|
"fielded_arm_optional_scalar",
|
|
33
|
-
&[("\"tmpl_field\"", naming::ruby_inspect_string(&key)), ("tmpl_ident", ident.clone()), ("tmpl_value_expr_placeholder(v)", naming::scalar_to_value(scalar, "v").unwrap())],
|
|
33
|
+
&[("\"tmpl_field\"", naming::ruby_inspect_string(&key)), ("tmpl_ident", ident.clone()), ("tmpl_value_expr_placeholder(v)", naming::scalar_to_value(scalar, &naming::deref_scalar(scalar, "v")).unwrap())],
|
|
34
34
|
))
|
|
35
35
|
} else if optional {
|
|
36
36
|
if is_fielded_nested(crate::attr::type_name(attr), value_objects_by_name) {
|
|
@@ -97,7 +97,7 @@ pub fn emit_fielded_record(exemplar: &Exemplar, aggregate: &Json, value_objects_
|
|
|
97
97
|
} else if let Some(scalar) = scalar {
|
|
98
98
|
Some(exemplar.render(
|
|
99
99
|
"fielded_arm_optional_scalar",
|
|
100
|
-
&[("\"tmpl_field\"", naming::ruby_inspect_string(&key)), ("tmpl_ident", ident.clone()), ("tmpl_value_expr_placeholder(v)", naming::scalar_to_value(scalar, "v").unwrap())],
|
|
100
|
+
&[("\"tmpl_field\"", naming::ruby_inspect_string(&key)), ("tmpl_ident", ident.clone()), ("tmpl_value_expr_placeholder(v)", naming::scalar_to_value(scalar, &naming::deref_scalar(scalar, "v")).unwrap())],
|
|
101
101
|
))
|
|
102
102
|
} else if is_fielded_nested(crate::attr::type_name(attr), value_objects_by_name) {
|
|
103
103
|
Some(exemplar.render("fielded_arm_optional_nested", &[("\"tmpl_field\"", naming::ruby_inspect_string(&key)), ("tmpl_ident", ident)]))
|
|
@@ -184,7 +184,7 @@ fn items_arms(exemplar: &Exemplar, attributes: &[Json], value_objects_by_name: &
|
|
|
184
184
|
let shape = format!("fielded_items_arm_list_{}{}", if optional(attr) { "optional_" } else { "" }, if scalar.is_some() { "scalar" } else { "nested" });
|
|
185
185
|
let mut subs: Vec<(&str, String)> = vec![("\"tmpl_field\"", naming::ruby_inspect_string(&key)), ("tmpl_ident", ident)];
|
|
186
186
|
if let Some(scalar) = scalar {
|
|
187
|
-
subs.push(("tmpl_value_expr_placeholder(v)", naming::scalar_to_value(scalar, "v").unwrap()));
|
|
187
|
+
subs.push(("tmpl_value_expr_placeholder(v)", naming::scalar_to_value(scalar, &naming::deref_scalar(scalar, "v")).unwrap()));
|
|
188
188
|
}
|
|
189
189
|
arms.push(format!(" {}", exemplar.render(&shape, &subs)));
|
|
190
190
|
}
|
|
@@ -130,7 +130,7 @@ pub fn list_element_from_json_mapper(struct_name: &str, key: &str, attr: &Json,
|
|
|
130
130
|
/// Inverse of `list_element_from_json_mapper`: scalar elements have no `.to_json()` method.
|
|
131
131
|
pub fn list_element_to_json_expr(attr: &Json) -> String {
|
|
132
132
|
match naming::effective_scalar_type(crate::attr::type_name(attr)) {
|
|
133
|
-
Some(scalar) => scalar_to_json_expr(scalar, "x"),
|
|
133
|
+
Some(scalar) => scalar_to_json_expr(scalar, &naming::deref_scalar(scalar, "x")),
|
|
134
134
|
None => "x.to_json()".to_string(),
|
|
135
135
|
}
|
|
136
136
|
}
|
|
@@ -392,7 +392,7 @@ pub fn emit_to_json_flat(exemplar: &Exemplar, struct_name: &str, attributes: &[J
|
|
|
392
392
|
} else if list {
|
|
393
393
|
format!("crate::kernel::Json::Array(self.{ident}.iter().map(|x| {}).collect())", elem_to_json.as_deref().unwrap())
|
|
394
394
|
} else if field_optional && scalar.is_some() {
|
|
395
|
-
format!("self.{ident}.as_ref().map(|v| {}).unwrap_or(crate::kernel::Json::Null)", scalar_to_json_expr(scalar.unwrap(), "v"))
|
|
395
|
+
format!("self.{ident}.as_ref().map(|v| {}).unwrap_or(crate::kernel::Json::Null)", scalar_to_json_expr(scalar.unwrap(), &naming::deref_scalar(scalar.unwrap(), "v")))
|
|
396
396
|
} else if field_optional {
|
|
397
397
|
format!("self.{ident}.as_ref().map(|v| v.to_json()).unwrap_or(crate::kernel::Json::Null)")
|
|
398
398
|
} else if let Some(scalar) = scalar {
|
data/rust/codegen/src/naming.rs
CHANGED
|
@@ -248,6 +248,16 @@ pub fn screaming_snake(name: &str) -> String {
|
|
|
248
248
|
out.to_uppercase()
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
/// The expression for a scalar reached through a reference (`v` in `as_ref().map(|v| ..)`, `x` in
|
|
252
|
+
/// `iter().map(|x| ..)`): the copy types are dereferenced, since `Value::Int(v)` takes an `i64`, not an
|
|
253
|
+
/// `&i64`; a `String` is left to the `.clone()` the scalar helpers already apply.
|
|
254
|
+
pub fn deref_scalar(type_name: &str, ident: &str) -> String {
|
|
255
|
+
match type_name {
|
|
256
|
+
"Integer" | "Float" | "TrueClass" | "FalseClass" => format!("*{ident}"),
|
|
257
|
+
_ => ident.to_string(),
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
251
261
|
pub fn scalar_to_value(type_name: &str, rust_expr: &str) -> Option<String> {
|
|
252
262
|
match type_name {
|
|
253
263
|
"String" => Some(format!("Value::Str({rust_expr}.clone())")),
|
|
@@ -316,6 +326,16 @@ pub fn ruby_inspect_string(s: &str) -> String {
|
|
|
316
326
|
mod tests {
|
|
317
327
|
use super::*;
|
|
318
328
|
|
|
329
|
+
#[test]
|
|
330
|
+
fn a_scalar_reached_through_a_reference_is_dereferenced_when_it_is_a_copy_type() {
|
|
331
|
+
// `self.level.as_ref().map(|v| Field::Value(Value::Int(v)))` does not compile: `v` is `&i64`.
|
|
332
|
+
assert_eq!(scalar_to_value("Integer", &deref_scalar("Integer", "v")).as_deref(), Some("Value::Int(*v)"));
|
|
333
|
+
assert_eq!(scalar_to_value("Float", &deref_scalar("Float", "v")).as_deref(), Some("Value::Float(*v)"));
|
|
334
|
+
assert_eq!(scalar_to_value("TrueClass", &deref_scalar("TrueClass", "v")).as_deref(), Some("Value::Bool(*v)"));
|
|
335
|
+
// A String is cloned, so the reference is left alone.
|
|
336
|
+
assert_eq!(scalar_to_value("String", &deref_scalar("String", "v")).as_deref(), Some("Value::Str(v.clone())"));
|
|
337
|
+
}
|
|
338
|
+
|
|
319
339
|
#[test]
|
|
320
340
|
fn refuses_an_aggregate_whose_module_name_is_a_rust_keyword() {
|
|
321
341
|
let refusal = reserved_name_refusal("shop", "shop", &["Pizza", "Match", "Type"]).expect("refused");
|
data/rust/host/Cargo.toml
CHANGED
|
@@ -6,6 +6,10 @@ edition = "2021"
|
|
|
6
6
|
# Empty table: makes this crate its own workspace root instead of joining `rust`'s.
|
|
7
7
|
[workspace]
|
|
8
8
|
|
|
9
|
+
[lib]
|
|
10
|
+
name = "rust_host"
|
|
11
|
+
path = "src/lib.rs"
|
|
12
|
+
|
|
9
13
|
[[bin]]
|
|
10
14
|
name = "bootstrap"
|
|
11
15
|
path = "src/main.rs"
|
|
@@ -61,6 +65,6 @@ async-trait = "0.1.92"
|
|
|
61
65
|
debug = 0
|
|
62
66
|
|
|
63
67
|
# The tests compile wasm modules through wasmtime's Cranelift, which is far slower unoptimized.
|
|
64
|
-
# Optimize dependencies only; the host crate
|
|
65
|
-
[profile.
|
|
68
|
+
# Optimize dependencies only, and only for cargo test; the host crate and cargo build stay at opt-level 0.
|
|
69
|
+
[profile.test.package."*"]
|
|
66
70
|
opt-level = 2
|
data/rust/host/HECKS_RELEASE
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.9.0
|
data/rust/host/src/api.rs
CHANGED
|
@@ -479,8 +479,13 @@ fn apply_identity(presentation: &Value, aggregate: &Value, args: &mut Value, exi
|
|
|
479
479
|
}
|
|
480
480
|
Some("sequence") => {
|
|
481
481
|
let prefix = rule.get("prefix").and_then(|v| v.as_str()).unwrap_or("");
|
|
482
|
-
let pad = rule.get("pad").and_then(|v| v.as_u64()).unwrap_or(3)
|
|
483
|
-
|
|
482
|
+
let pad = rule.get("pad").and_then(|v| v.as_u64()).unwrap_or(3);
|
|
483
|
+
// `pad` rides in as free-form config JSON, and a width like 10^15 would have the
|
|
484
|
+
// formatter allocate that many bytes: refuse it rather than abort the process.
|
|
485
|
+
if pad > MAX_IDENTITY_PAD {
|
|
486
|
+
return Err(internal_error(&format!("the identity pad {pad} for {name} is wider than the {MAX_IDENTITY_PAD} this host will mint")));
|
|
487
|
+
}
|
|
488
|
+
Some(format!("{prefix}{:0>pad$}", existing + 1, pad = pad as usize))
|
|
484
489
|
}
|
|
485
490
|
// `port` delegates to a Ruby adapter this host has no runtime for
|
|
486
491
|
// (ADR 0007); refusing names the real reason rather than
|
|
@@ -499,6 +504,9 @@ fn apply_identity(presentation: &Value, aggregate: &Value, args: &mut Value, exi
|
|
|
499
504
|
Ok(())
|
|
500
505
|
}
|
|
501
506
|
|
|
507
|
+
/// The widest zero-padded `sequence` identity `apply_identity` will mint.
|
|
508
|
+
const MAX_IDENTITY_PAD: u64 = 64;
|
|
509
|
+
|
|
502
510
|
/// The wire key an identity value has to be wrapped in — whatever the
|
|
503
511
|
/// target value object's own single attribute is actually called. This
|
|
504
512
|
/// domain always spells it "value"; nothing here assumes that.
|
|
@@ -727,10 +735,21 @@ fn compare_sort_keys(left: Option<&Value>, right: Option<&Value>, descending: bo
|
|
|
727
735
|
}
|
|
728
736
|
}
|
|
729
737
|
|
|
738
|
+
/// Numbers, then strings, then anything else (all equal, so the id breaks the tie). A column can
|
|
739
|
+
/// hold mixed types, and `sort_by` panics on a comparison that is not a total order: comparing a
|
|
740
|
+
/// number with a string as `""` made `5`, `""` and `3` each equal to a neighbour but not to each other.
|
|
730
741
|
fn compare_values(left: &Value, right: &Value) -> Ordering {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
742
|
+
fn rank(value: &Value) -> u8 {
|
|
743
|
+
match value {
|
|
744
|
+
Value::Number(_) => 0,
|
|
745
|
+
Value::String(_) => 1,
|
|
746
|
+
_ => 2,
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
match (left, right) {
|
|
750
|
+
(Value::Number(a), Value::Number(b)) => a.as_f64().unwrap_or(0.0).total_cmp(&b.as_f64().unwrap_or(0.0)),
|
|
751
|
+
(Value::String(a), Value::String(b)) => a.cmp(b),
|
|
752
|
+
_ => rank(left).cmp(&rank(right)),
|
|
734
753
|
}
|
|
735
754
|
}
|
|
736
755
|
|
|
@@ -1594,3 +1613,7 @@ mod tests {
|
|
|
1594
1613
|
);
|
|
1595
1614
|
}
|
|
1596
1615
|
}
|
|
1616
|
+
|
|
1617
|
+
#[cfg(test)]
|
|
1618
|
+
#[path = "boundary_fuzz/api.rs"]
|
|
1619
|
+
mod boundary_fuzz;
|
data/rust/host/src/auth.rs
CHANGED
|
@@ -882,13 +882,19 @@ fn sign(secret: &str, payload: &str) -> String {
|
|
|
882
882
|
|
|
883
883
|
fn verify_sig(secret: &str, token: &str) -> Option<String> {
|
|
884
884
|
let (payload, sig) = token.rsplit_once('.')?;
|
|
885
|
-
if sign(secret, payload)
|
|
885
|
+
if constant_time_eq(sign(secret, payload).as_bytes(), sig.as_bytes()) {
|
|
886
886
|
Some(payload.to_string())
|
|
887
887
|
} else {
|
|
888
888
|
None
|
|
889
889
|
}
|
|
890
890
|
}
|
|
891
891
|
|
|
892
|
+
/// Compares two signatures without stopping at the first differing byte, so how long a forged
|
|
893
|
+
/// cookie takes to refuse does not tell the caller how many leading characters were right.
|
|
894
|
+
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
|
|
895
|
+
a.len() == b.len() && a.iter().zip(b).fold(0u8, |diff, (x, y)| diff | (x ^ y)) == 0
|
|
896
|
+
}
|
|
897
|
+
|
|
892
898
|
fn hex_encode(bytes: &[u8]) -> String {
|
|
893
899
|
bytes.iter().map(|b| format!("{b:02x}")).collect()
|
|
894
900
|
}
|
|
@@ -1341,3 +1347,7 @@ mod tests {
|
|
|
1341
1347
|
assert_eq!(state["role"]["value"], "Admin", "a lower ordinal must never move the snapshot backward");
|
|
1342
1348
|
}
|
|
1343
1349
|
}
|
|
1350
|
+
|
|
1351
|
+
#[cfg(test)]
|
|
1352
|
+
#[path = "boundary_fuzz/auth.rs"]
|
|
1353
|
+
mod boundary_fuzz;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
//! Seeded fuzz of the `/api` request-shaping helpers that run on caller-controlled input before
|
|
2
|
+
//! anything reaches the database: body parsing, identity minting, preconditions, sorting and
|
|
3
|
+
//! query-argument shaping. Each must refuse bad input with a structured answer, never panic, and
|
|
4
|
+
//! leave the arguments it was given untouched when it refuses.
|
|
5
|
+
|
|
6
|
+
use super::*;
|
|
7
|
+
use crate::fuzz_support::*;
|
|
8
|
+
|
|
9
|
+
fn envelope_is_structured(response: &Value) {
|
|
10
|
+
let status = response["statusCode"].as_u64().expect("a refusal carries its status");
|
|
11
|
+
assert!((400..600).contains(&status), "a refusal is a 4xx/5xx, got {status}");
|
|
12
|
+
assert_eq!(response["headers"]["content-type"], "application/json");
|
|
13
|
+
let body: Value = serde_json::from_str(response["body"].as_str().expect("a JSON body")).expect("the body is JSON");
|
|
14
|
+
assert!(body["error"].is_string() && body["message"].is_string(), "the refusal names itself: {body}");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fn client_aggregate() -> Value {
|
|
18
|
+
json!({
|
|
19
|
+
"name": "Client",
|
|
20
|
+
"identified_by": ["reference.value"],
|
|
21
|
+
"attributes": [
|
|
22
|
+
{"name": "reference", "type": "ClientReference", "list": false, "optional": false},
|
|
23
|
+
{"name": "name", "type": "ClientName", "list": false, "optional": false},
|
|
24
|
+
{"name": "owner", "type": "Reference<Member>", "list": false, "optional": true},
|
|
25
|
+
{"name": "status", "type": "String", "list": false, "optional": true}
|
|
26
|
+
],
|
|
27
|
+
"value_objects": [
|
|
28
|
+
{"name": "ClientReference", "attributes": [{"name": "value", "type": "String"}], "closed_set": false, "members": []},
|
|
29
|
+
{"name": "ClientName", "attributes": [{"name": "value", "type": "String"}], "closed_set": false, "members": []}
|
|
30
|
+
],
|
|
31
|
+
"entities": [], "commands": [], "queries": [], "lifecycle": {"field": "status"}
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#[test]
|
|
36
|
+
fn parsed_body_refuses_everything_that_is_not_an_object_with_a_structured_400() {
|
|
37
|
+
for seed in 0..seeds() {
|
|
38
|
+
guarded(&format!("seed {seed}"), move || {
|
|
39
|
+
let mut rng = Rng::new(seed);
|
|
40
|
+
for _ in 0..50 {
|
|
41
|
+
let text = match rng.below(4) {
|
|
42
|
+
0 => random_text(&mut rng),
|
|
43
|
+
1 => {
|
|
44
|
+
let valid = random_value(&mut rng, 0).to_string();
|
|
45
|
+
mutate_text(&mut rng, &valid)
|
|
46
|
+
}
|
|
47
|
+
2 => random_value(&mut rng, 0).to_string(),
|
|
48
|
+
_ => format!("{}{}", " \t\n".repeat(rng.below(3)), random_value(&mut rng, 1)),
|
|
49
|
+
};
|
|
50
|
+
match parsed_body(&text) {
|
|
51
|
+
Ok(body) => {
|
|
52
|
+
assert!(body.is_object(), "seed {seed}: {text:?} parsed to a non-object");
|
|
53
|
+
assert!(text.trim().is_empty() || serde_json::from_str::<Value>(&text).is_ok(), "seed {seed}: {text:?} is not JSON yet was accepted");
|
|
54
|
+
}
|
|
55
|
+
Err(refusal) => {
|
|
56
|
+
assert_eq!(refusal["statusCode"], 400, "seed {seed}: {text:?}");
|
|
57
|
+
envelope_is_structured(&refusal);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#[test]
|
|
66
|
+
fn parsed_body_survives_depth_width_and_size() {
|
|
67
|
+
guarded("depth and size", || {
|
|
68
|
+
for text in [
|
|
69
|
+
"[".repeat(1_000_000),
|
|
70
|
+
format!("{}1{}", "[".repeat(100_000), "]".repeat(100_000)),
|
|
71
|
+
format!("{}1{}", "{\"a\":".repeat(100_000), "}".repeat(100_000)),
|
|
72
|
+
format!("[{}]", vec!["1"; 500_000].join(",")),
|
|
73
|
+
format!("{{\"a\":\"{}\"}}", "x".repeat(16 * 1024 * 1024)),
|
|
74
|
+
] {
|
|
75
|
+
let _ = parsed_body(&text);
|
|
76
|
+
}
|
|
77
|
+
for text in ["[]", "1", "\"s\"", "null", "true", "{", "{\"a\":1}{}", "{} {}", "\u{feff}{}", "{\"a\":NaN}", "{\"a\":1e999}"] {
|
|
78
|
+
if let Err(refusal) = parsed_body(text) {
|
|
79
|
+
assert_eq!(refusal["statusCode"], 400, "{text:?}");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
assert_eq!(parsed_body(" ").unwrap(), json!({}));
|
|
83
|
+
assert!(parsed_body("{\"a\":1,\"a\":2}").unwrap().is_object());
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#[test]
|
|
88
|
+
fn slugify_always_answers_a_clean_non_empty_slug() {
|
|
89
|
+
for seed in 0..seeds() {
|
|
90
|
+
guarded(&format!("seed {seed}"), move || {
|
|
91
|
+
let mut rng = Rng::new(seed);
|
|
92
|
+
for _ in 0..100 {
|
|
93
|
+
let text = random_text(&mut rng);
|
|
94
|
+
let slug = slugify(&text);
|
|
95
|
+
assert!(!slug.is_empty(), "seed {seed}: {text:?} slugged to nothing");
|
|
96
|
+
assert!(slug.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'), "seed {seed}: {text:?} -> {slug:?}");
|
|
97
|
+
assert!(!slug.starts_with('-') && !slug.ends_with('-') && !slug.contains("--"), "seed {seed}: {text:?} -> {slug:?}");
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#[test]
|
|
104
|
+
fn identity_minting_survives_any_rule_and_leaves_args_alone_when_it_refuses() {
|
|
105
|
+
for seed in 0..seeds() {
|
|
106
|
+
guarded(&format!("seed {seed}"), move || {
|
|
107
|
+
let mut rng = Rng::new(seed ^ 0x1D);
|
|
108
|
+
let aggregate = client_aggregate();
|
|
109
|
+
for _ in 0..50 {
|
|
110
|
+
let mut rule = serde_json::Map::new();
|
|
111
|
+
for key in ["field", "strategy", "source", "prefix", "pad"] {
|
|
112
|
+
if rng.chance(5) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
let value = match key {
|
|
116
|
+
"field" if rng.chance(2) => json!(*rng.pick(&["reference", "name", "owner", "status", "missing", ""])),
|
|
117
|
+
"strategy" if rng.chance(2) => json!(*rng.pick(&["slug", "sequence", "port", "nope", ""])),
|
|
118
|
+
"source" if rng.chance(2) => json!(*rng.pick(&["name", "reference", "owner", "missing", ""])),
|
|
119
|
+
"pad" if rng.chance(2) => json!(*rng.pick(&[0u64, 1, 3, 64, 65, 1_000, 1_000_000, 1u64 << 40, 1u64 << 62, u64::MAX])),
|
|
120
|
+
_ => random_value(&mut rng, 2),
|
|
121
|
+
};
|
|
122
|
+
rule.insert(key.to_string(), value);
|
|
123
|
+
}
|
|
124
|
+
let presentation = json!({"collections": {"Client": {"identity": Value::Object(rule)}}});
|
|
125
|
+
let mut args = random_value(&mut rng, 0);
|
|
126
|
+
if rng.chance(2) {
|
|
127
|
+
args = json!({"name": random_value(&mut rng, 2)});
|
|
128
|
+
}
|
|
129
|
+
let before = args.clone();
|
|
130
|
+
let existing = *rng.pick(&[0usize, 1, 41, 999_999, usize::MAX - 1]);
|
|
131
|
+
match apply_identity(&presentation, &aggregate, &mut args, existing) {
|
|
132
|
+
Ok(()) => {}
|
|
133
|
+
Err(refusal) => {
|
|
134
|
+
envelope_is_structured(&refusal);
|
|
135
|
+
assert_eq!(args, before, "seed {seed}: a refused identity must not have touched the args");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#[test]
|
|
144
|
+
fn an_absurd_sequence_pad_is_refused_rather_than_allocating_it() {
|
|
145
|
+
let aggregate = client_aggregate();
|
|
146
|
+
for pad in [65u64, 1_000_000, 1 << 40, u64::MAX] {
|
|
147
|
+
let presentation = json!({"collections": {"Client": {"identity": {"field": "reference", "strategy": "sequence", "prefix": "C-", "pad": pad}}}});
|
|
148
|
+
let mut args = json!({"name": {"value": "Acme"}});
|
|
149
|
+
let refusal = apply_identity(&presentation, &aggregate, &mut args, 1).expect_err("a pad this wide is refused");
|
|
150
|
+
envelope_is_structured(&refusal);
|
|
151
|
+
assert_eq!(args, json!({"name": {"value": "Acme"}}), "pad {pad}: nothing was minted");
|
|
152
|
+
}
|
|
153
|
+
let presentation = json!({"collections": {"Client": {"identity": {"field": "reference", "strategy": "sequence", "prefix": "C-", "pad": 64}}}});
|
|
154
|
+
let mut args = json!({});
|
|
155
|
+
apply_identity(&presentation, &aggregate, &mut args, 0).expect("the widest allowed pad still mints");
|
|
156
|
+
assert_eq!(args["reference"]["value"].as_str().unwrap().len(), 2 + 64);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#[test]
|
|
160
|
+
fn preconditions_survive_any_config_and_instances_shape() {
|
|
161
|
+
for seed in 0..seeds() {
|
|
162
|
+
guarded(&format!("seed {seed}"), move || {
|
|
163
|
+
let mut rng = Rng::new(seed ^ 0xC0);
|
|
164
|
+
for _ in 0..50 {
|
|
165
|
+
let domain = json!({"name": random_text(&mut rng), "aggregates": [client_aggregate(), {"name": "Member", "lifecycle": random_value(&mut rng, 2)}]});
|
|
166
|
+
let aggregate = client_aggregate();
|
|
167
|
+
let rule = if rng.chance(3) {
|
|
168
|
+
random_value(&mut rng, 0)
|
|
169
|
+
} else {
|
|
170
|
+
json!({"field": *rng.pick(&["owner", "name", "status", "missing"]), "state": random_value(&mut rng, 2), "message": random_value(&mut rng, 2)})
|
|
171
|
+
};
|
|
172
|
+
let args = if rng.chance(3) { random_value(&mut rng, 0) } else { json!({"owner": random_value(&mut rng, 2)}) };
|
|
173
|
+
let instances = if rng.chance(3) { random_value(&mut rng, 0) } else { json!({format!("{}::Member#m1", domain_name(&domain)): random_value(&mut rng, 2)}) };
|
|
174
|
+
if let Err(refusal) = check_precondition(&domain, &aggregate, &rule, &args, &instances) {
|
|
175
|
+
envelope_is_structured(&refusal);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#[test]
|
|
183
|
+
fn sorting_records_of_mixed_types_never_panics_on_an_inconsistent_order() {
|
|
184
|
+
// `sort_by` may panic when its comparator is not a total order, so a column holding numbers,
|
|
185
|
+
// empty strings, strings, nulls and objects together must still order consistently.
|
|
186
|
+
for seed in 0..seeds() {
|
|
187
|
+
guarded(&format!("seed {seed}"), move || {
|
|
188
|
+
let mut rng = Rng::new(seed ^ 0x50);
|
|
189
|
+
for _ in 0..10 {
|
|
190
|
+
let mut records: Vec<(String, Value)> = (0..20 + rng.below(200))
|
|
191
|
+
.map(|i| {
|
|
192
|
+
let cell = match rng.below(8) {
|
|
193
|
+
0 => json!(rng.below(5)),
|
|
194
|
+
1 => json!(rng.below(5) as f64 / 2.0),
|
|
195
|
+
2 => json!(""),
|
|
196
|
+
3 => json!(*rng.pick(&["a", "b", "B", "10", "9", "é", "🍕"])),
|
|
197
|
+
4 => Value::Null,
|
|
198
|
+
5 => json!({"cents": rng.below(50)}),
|
|
199
|
+
6 => json!({"value": random_text(&mut rng)}),
|
|
200
|
+
_ => random_value(&mut rng, 2),
|
|
201
|
+
};
|
|
202
|
+
(format!("id{}", i % 17), json!({"col": cell}))
|
|
203
|
+
})
|
|
204
|
+
.collect();
|
|
205
|
+
for descending in [false, true] {
|
|
206
|
+
let spec = Some(SortSpec { field: "col".to_string(), descending });
|
|
207
|
+
sort_records(&mut records, &spec);
|
|
208
|
+
sort_records(&mut records, &None);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#[test]
|
|
216
|
+
fn query_arguments_and_sort_params_survive_hostile_input() {
|
|
217
|
+
for seed in 0..seeds() {
|
|
218
|
+
guarded(&format!("seed {seed}"), move || {
|
|
219
|
+
let mut rng = Rng::new(seed ^ 0x9A);
|
|
220
|
+
let aggregate = client_aggregate();
|
|
221
|
+
let domain = json!({"name": "Crm", "aggregates": [aggregate.clone()]});
|
|
222
|
+
for _ in 0..30 {
|
|
223
|
+
let named = if rng.chance(3) {
|
|
224
|
+
random_value(&mut rng, 0)
|
|
225
|
+
} else {
|
|
226
|
+
json!({"attributes": [
|
|
227
|
+
{"name": *rng.pick(&["name", "owner", "reference", "x"]), "type": *rng.pick(&["ClientName", "String", "Reference<Member>", "Nope", "Integer"]), "optional": rng.chance(2)},
|
|
228
|
+
random_value(&mut rng, 2)
|
|
229
|
+
]})
|
|
230
|
+
};
|
|
231
|
+
let mut params: HashMap<String, String> = HashMap::new();
|
|
232
|
+
for _ in 0..rng.below(5) {
|
|
233
|
+
params.insert(random_text(&mut rng), random_text(&mut rng));
|
|
234
|
+
}
|
|
235
|
+
params.insert("name".to_string(), random_text(&mut rng));
|
|
236
|
+
let _ = query_args_from_params(&aggregate, &named, ¶ms);
|
|
237
|
+
|
|
238
|
+
let sort: HashMap<String, String> = [("sort", random_text(&mut rng)), ("direction", random_text(&mut rng))].into_iter().map(|(k, v)| (k.to_string(), v)).collect();
|
|
239
|
+
let config = random_value(&mut rng, 0);
|
|
240
|
+
let _ = resolve_sort(&domain, &config, "Client", &aggregate, &sort);
|
|
241
|
+
let _ = resolve_sort(&domain, &json!({}), "Client", &aggregate, &HashMap::from([("sort".to_string(), "__state__".to_string())]));
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#[test]
|
|
248
|
+
fn identity_sources_and_scalars_render_any_json_value() {
|
|
249
|
+
for seed in 0..seeds() {
|
|
250
|
+
guarded(&format!("seed {seed}"), move || {
|
|
251
|
+
let mut rng = Rng::new(seed ^ 0x5C);
|
|
252
|
+
for _ in 0..50 {
|
|
253
|
+
let args = json!({"name": random_value(&mut rng, 0)});
|
|
254
|
+
let _ = dig_source(&args, "name");
|
|
255
|
+
let _ = dig_source(&random_value(&mut rng, 0), &random_text(&mut rng));
|
|
256
|
+
let _ = scalar_to_string(&random_value(&mut rng, 0));
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
//! Seeded fuzz of the signed-cookie and token readers: whatever text arrives in a `Cookie`
|
|
2
|
+
//! header or a query string, it either verifies as exactly what this host signed or yields
|
|
3
|
+
//! nothing. A forged, truncated or tampered token must never read as a session, and none of it
|
|
4
|
+
//! may panic.
|
|
5
|
+
|
|
6
|
+
use super::*;
|
|
7
|
+
use crate::fuzz_support::*;
|
|
8
|
+
|
|
9
|
+
const SECRET: &str = "fuzz-secret";
|
|
10
|
+
|
|
11
|
+
fn session(rng: &mut Rng) -> Session {
|
|
12
|
+
Session { identity_id: random_text(rng), email: random_text(rng), name: random_text(rng), role: if rng.chance(2) { Some(random_text(rng)) } else { None } }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
#[test]
|
|
16
|
+
fn a_signed_cookie_reads_back_as_the_session_it_was_made_from() {
|
|
17
|
+
for seed in 0..seeds() {
|
|
18
|
+
guarded(&format!("seed {seed}"), move || {
|
|
19
|
+
let mut rng = Rng::new(seed);
|
|
20
|
+
for _ in 0..30 {
|
|
21
|
+
let original = session(&mut rng);
|
|
22
|
+
let cookie = session_cookie(SECRET, &original);
|
|
23
|
+
let read = parse_session_cookie(SECRET, &cookie).unwrap_or_else(|| panic!("seed {seed}: own cookie for {:?} refused", original.email));
|
|
24
|
+
assert_eq!((read.identity_id, read.email, read.name, read.role), (original.identity_id, original.email, original.name, original.role), "seed {seed}");
|
|
25
|
+
assert!(parse_session_cookie("another-secret", &cookie).is_none(), "seed {seed}: a different secret verified the cookie");
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#[test]
|
|
32
|
+
fn no_arbitrary_or_tampered_token_ever_verifies() {
|
|
33
|
+
for seed in 0..seeds() {
|
|
34
|
+
guarded(&format!("seed {seed}"), move || {
|
|
35
|
+
let mut rng = Rng::new(seed ^ 0x70C);
|
|
36
|
+
let genuine_cookie = session_cookie(SECRET, &session(&mut rng));
|
|
37
|
+
let genuine_account = account_token(SECRET, &random_text(&mut rng), 60);
|
|
38
|
+
let genuine_purpose = purpose_token(SECRET, "reset", json!({"email": random_text(&mut rng)}), 60);
|
|
39
|
+
for _ in 0..40 {
|
|
40
|
+
let junk = match rng.below(4) {
|
|
41
|
+
0 => random_text(&mut rng),
|
|
42
|
+
1 => format!("{}.{}", random_text(&mut rng), random_text(&mut rng)),
|
|
43
|
+
2 => ".".repeat(rng.below(4)),
|
|
44
|
+
_ => String::from_utf8_lossy(&random_bytes(&mut rng)).into_owned(),
|
|
45
|
+
};
|
|
46
|
+
assert!(parse_session_cookie(SECRET, &junk).is_none(), "seed {seed}: {junk:?} read as a session");
|
|
47
|
+
assert!(verify_account_token(SECRET, &junk).is_none(), "seed {seed}: {junk:?} read as an account token");
|
|
48
|
+
assert!(verify_purpose_token(SECRET, "reset", &junk).is_none(), "seed {seed}: {junk:?} read as a purpose token");
|
|
49
|
+
|
|
50
|
+
for (genuine, verifies) in [
|
|
51
|
+
(&genuine_cookie, (|t: &str| parse_session_cookie(SECRET, t).is_some()) as fn(&str) -> bool),
|
|
52
|
+
(&genuine_account, |t: &str| verify_account_token(SECRET, t).is_some()),
|
|
53
|
+
(&genuine_purpose, |t: &str| verify_purpose_token(SECRET, "reset", t).is_some()),
|
|
54
|
+
] {
|
|
55
|
+
assert!(verifies(genuine), "seed {seed}: the untampered token must verify");
|
|
56
|
+
let tampered = mutate_text(&mut rng, genuine);
|
|
57
|
+
assert!(tampered == *genuine || !verifies(&tampered), "seed {seed}: tampered token {tampered:?} verified");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#[test]
|
|
65
|
+
fn a_token_for_one_purpose_never_verifies_for_another_or_as_an_account_token() {
|
|
66
|
+
let token = purpose_token(SECRET, "reset", json!({"email": "a@example.com"}), 60);
|
|
67
|
+
assert!(verify_purpose_token(SECRET, "reset", &token).is_some());
|
|
68
|
+
for other in ["", "RESET", "reset ", "reset:", "invite", "reset:fuzz-secret"] {
|
|
69
|
+
assert!(verify_purpose_token(SECRET, other, &token).is_none(), "purpose {other:?}");
|
|
70
|
+
}
|
|
71
|
+
assert!(verify_account_token(SECRET, &token).is_none());
|
|
72
|
+
assert!(parse_session_cookie(SECRET, &token).is_none());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#[test]
|
|
76
|
+
fn an_expired_or_unexpiring_signed_payload_is_not_a_session() {
|
|
77
|
+
let sign_payload = |payload: Value| {
|
|
78
|
+
let encoded = base64_encode(payload.to_string().as_bytes());
|
|
79
|
+
format!("{encoded}.{}", sign(SECRET, &encoded))
|
|
80
|
+
};
|
|
81
|
+
let person = json!({"identity_id": "i", "email": "e", "name": "n"});
|
|
82
|
+
for exp in [json!(0), json!(1), json!(-1), json!(1.5), json!("9999999999"), json!(null), json!(u64::MAX)] {
|
|
83
|
+
let mut payload = person.clone();
|
|
84
|
+
payload["exp"] = exp.clone();
|
|
85
|
+
let verdict = parse_session_cookie(SECRET, &sign_payload(payload)).is_some();
|
|
86
|
+
assert_eq!(verdict, exp == json!(u64::MAX), "exp {exp}");
|
|
87
|
+
}
|
|
88
|
+
assert!(parse_session_cookie(SECRET, &sign_payload(person)).is_none(), "no exp is no session");
|
|
89
|
+
for payload in [json!([]), json!(1), json!("s"), json!(null), json!({"exp": u64::MAX}), json!({"exp": u64::MAX, "identity_id": 1, "email": "e", "name": "n"})] {
|
|
90
|
+
assert!(parse_session_cookie(SECRET, &sign_payload(payload.clone())).is_none(), "{payload}");
|
|
91
|
+
assert!(verify_account_token(SECRET, &sign_payload(payload.clone())).is_none() || payload.get("email").is_some(), "{payload}");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#[test]
|
|
96
|
+
fn signature_comparison_checks_every_byte_and_the_length() {
|
|
97
|
+
assert!(constant_time_eq(b"", b""));
|
|
98
|
+
assert!(constant_time_eq(b"abc", b"abc"));
|
|
99
|
+
assert!(!constant_time_eq(b"abc", b"abd"));
|
|
100
|
+
assert!(!constant_time_eq(b"abc", b"xbc"));
|
|
101
|
+
assert!(!constant_time_eq(b"abc", b"ab"));
|
|
102
|
+
assert!(!constant_time_eq(b"ab", b"abc"));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#[test]
|
|
106
|
+
fn base64_decoding_and_cookie_names_take_any_text() {
|
|
107
|
+
for seed in 0..seeds() {
|
|
108
|
+
guarded(&format!("seed {seed}"), move || {
|
|
109
|
+
let mut rng = Rng::new(seed ^ 0xBA5E);
|
|
110
|
+
for _ in 0..100 {
|
|
111
|
+
let text = random_text(&mut rng);
|
|
112
|
+
let _ = base64_decode(&text);
|
|
113
|
+
let _ = resolve_account_cookie(Some(&text));
|
|
114
|
+
let bytes = random_bytes(&mut rng);
|
|
115
|
+
assert_eq!(base64_decode(&base64_encode(&bytes)), bytes, "seed {seed}");
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|