Skip to content

Commit 157cc19

Browse files
committed
Harden hashavatar 1.0.0 candidate
1 parent b87e74c commit 157cc19

5 files changed

Lines changed: 195 additions & 46 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@
1313
- Added `docs/STABILITY.md` to release metadata/package validation so future
1414
releases keep the stability policy in the published crate.
1515
- Added 1.0 release notes.
16+
- Hardened the 1.0 candidate after pentest review by keeping the renderer RNG
17+
seed copy in a `Zeroizing` guard until seeding, guarding the intermediate
18+
identity digest copy, making starry raster backgrounds identity-dependent,
19+
clamping gradient interpolation inputs, and removing the exact rejected byte
20+
count from `AvatarIdentityError` display text.
21+
- Documented target-specific `getrandom` considerations, XXH3 temporary
22+
preimage-copy overhead, and rectangle zero-size clamping behavior.
23+
- Added defensive digest-byte indexing and widened the avatar fuzz harness so it
24+
covers the full supported `64..=2048` dimension range.
1625
- No avatar families, backgrounds, visual layers, hash modes, output formats,
1726
or runtime dependencies were added in this release.
1827

docs/SECURITY_CONTROLS.md

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@
3737
digest, separate from the lower digest bytes commonly used for direct visual
3838
parameters.
3939
- The temporary 256-bit RNG seed copy is stored in `zeroize::Zeroizing`, so the
40-
digest-derived seed copy is scrubbed immediately after seeding the renderer
41-
RNG.
40+
digest-derived seed copy is scrubbed on scope exit. The final value passed to
41+
`StdRng::from_seed` is also held in a `Zeroizing` guard before the copy into
42+
`StdRng`.
4243
- The procedural RNG itself is `rand::rngs::StdRng`. Its expanded internal
4344
state is not zeroized on drop because `StdRng` does not currently implement
4445
`ZeroizeOnDrop`. In the default SHA-512 mode, recovering the original
@@ -54,18 +55,33 @@
5455
short-lived so digest bytes do not remain live in multiple memory locations
5556
longer than necessary.
5657
- Derived identity digests and temporary hash preimage buffers are zeroized
57-
when dropped.
58+
when dropped. The intermediate 64-byte digest returned by each hash backend is
59+
held in `Zeroizing` guards before being copied into `AvatarIdentity`.
5860
- The SHA-512 dependency is built with its `zeroize` feature so its block
59-
buffer uses upstream `ZeroizeOnDrop`. The optional BLAKE3 dependency is built
60-
with its `zeroize` feature, and `hashavatar` explicitly zeroizes the BLAKE3
61-
hasher and XOF reader after deriving the 64-byte digest. Tests assert these
62-
upstream zeroization traits remain available.
61+
buffer uses upstream `ZeroizeOnDrop`. SHA-512 `finalize()` still returns the
62+
digest output by value, so the output bytes have the same unavoidable
63+
by-value-copy caveats as the BLAKE3 and XXH3 paths. `hashavatar` wraps that
64+
returned digest in `Zeroizing` immediately. The optional BLAKE3 dependency is
65+
built with its `zeroize` feature, and `hashavatar` explicitly zeroizes the
66+
BLAKE3 hasher and XOF reader after deriving the 64-byte digest. Tests assert
67+
these upstream zeroization traits remain available.
6368
- Identity hash preimage allocation is sized from the tenant, style-version,
6469
and identity input lengths. The crate bounds and zeroizes those temporary
6570
buffers, but it does not hide input length from the allocator, OS-level heap
6671
profilers, or other same-process memory-observation tools. Very
6772
high-assurance callers that treat identifier length as sensitive should
6873
normalize or pad identifiers to a fixed length before calling this crate.
74+
- The optional XXH3-128 mode derives the crate's 64-byte identity digest by
75+
hashing four domain-separated chunks. Each chunk temporarily copies the
76+
bounded preimage into a zeroized buffer, so peak preimage memory is higher
77+
than SHA-512 or BLAKE3. Keep XXH3 for low-stakes, non-adversarial workloads
78+
where its non-cryptographic collision profile and extra temporary preimage
79+
copy are acceptable.
80+
- The crate seeds its own rendering RNG deterministically from identity digest
81+
bytes and does not use OS entropy for avatar rendering. The `rand` dependency
82+
may still bring a transitive `getrandom` dependency into the lockfile; WASM
83+
and embedded applications that use OS-backed randomness elsewhere in the same
84+
binary must configure and test `getrandom` for their target explicitly.
6985
- Encode APIs wrap temporary owned raster buffers in RAII zeroization guards, so
7086
pixel data is cleared during normal returns, encoder errors, and unwinding
7187
panics. JPEG export also wraps the temporary RGB flattening buffer in
@@ -90,10 +106,15 @@
90106
semaphore wrapper because concurrency primitives belong to the caller's
91107
async/runtime boundary.
92108
- Internal rectangle helpers use saturating or clamping arithmetic for edge and
93-
intersection calculations.
109+
intersection calculations. Rectangle size construction promotes zero
110+
dimensions to a one-pixel rectangle so rounded-down decorative features remain
111+
non-panicking; minimum-size rendering is covered by regression tests.
94112
- Raster frame-shape hit-testing uses integer arithmetic for circle, squircle,
95113
hexagon, and octagon masks, reducing platform-specific floating-point
96114
rounding in clipping decisions.
115+
- Decorative raster backgrounds are deterministic. Pattern and gradient modes
116+
are explicit by design, while the starry background now incorporates identity
117+
digest bytes in its local deterministic star-position generator.
97118
- Polygon rasterization returns immediately for empty polygons and zero-sized
98119
images, widens scanline interpolation math before rounding, and is covered by
99120
a dedicated fuzz harness for arbitrary image dimensions, degenerate polygons,

docs/release-notes/RELEASE_NOTES_1.0.0.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ series.
3636
- Public dimensions, identity inputs, and namespace components remain bounded.
3737
- Public render APIs return typed errors for invalid inputs instead of
3838
panicking.
39+
- `AvatarIdentityError` keeps the rejected length available through structured
40+
accessors, but its display text no longer prints the exact rejected byte
41+
count.
42+
- Temporary renderer seed and intermediate identity digest copies are guarded
43+
with `zeroize::Zeroizing` before their final required by-value copies.
44+
- Starry raster backgrounds incorporate identity digest bytes in their
45+
deterministic local star-position generator.
46+
- Internal identity digest byte access is defensive against future
47+
out-of-range renderer mistakes, and the avatar fuzz harness now samples the
48+
full supported dimension range.
3949
- The default build remains SHA-512 identity hashing plus WebP encoding.
4050
- Optional BLAKE3, XXH3-128, PNG, JPEG, and GIF support remain explicit Cargo
4151
features.

fuzz/fuzz_targets/avatar_inputs.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
use hashavatar::{
44
AvatarAccessory, AvatarBackground, AvatarColor, AvatarExpression, AvatarKind,
5-
AvatarOutputFormat, AvatarShape, AvatarSpec, AvatarStyleOptions, encode_avatar_style_for_id,
6-
render_avatar_svg_style_for_id,
5+
AvatarOutputFormat, AvatarShape, AvatarSpec, AvatarStyleOptions, MAX_AVATAR_DIMENSION,
6+
MIN_AVATAR_DIMENSION, encode_avatar_style_for_id, render_avatar_svg_style_for_id,
77
};
88
use libfuzzer_sys::fuzz_target;
99

@@ -18,7 +18,9 @@ fuzz_target!(|data: &[u8]| {
1818
let color = AvatarColor::from_byte(*data.get(4).unwrap_or(&0));
1919
let expression = AvatarExpression::from_byte(*data.get(5).unwrap_or(&0));
2020
let shape = AvatarShape::from_byte(*data.get(6).unwrap_or(&0));
21-
let size = 64 + u32::from(*data.get(2).unwrap_or(&0) % 8) * 64;
21+
let size_byte = u32::from(*data.get(2).unwrap_or(&0));
22+
let size = MIN_AVATAR_DIMENSION
23+
+ size_byte * (MAX_AVATAR_DIMENSION - MIN_AVATAR_DIMENSION) / u32::from(u8::MAX);
2224
let identity_len = data.len().min(128);
2325
let identity = &data[..identity_len];
2426
let Ok(spec) = AvatarSpec::new(size, size, 0) else {

0 commit comments

Comments
 (0)