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
5 changes: 5 additions & 0 deletions .changes/add-websocket-accept-digest-a6f2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"rscrypto" = "minor"
---

Add an allocation-free RFC 6455 WebSocket accept-digest capability behind the explicit `websocket-sha1` feature without exposing general-purpose SHA-1.
38 changes: 36 additions & 2 deletions .config/benchmark-matrix.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,17 @@
"default_features": ["parallel", "full"],
"crates": {
"checksum": ["crc"],
"hashes": ["sha2", "sha3", "kmac_cshake", "ascon", "xxh3", "rapidhash", "blake2", "blake3"],
"hashes": [
"sha2",
"sha3",
"websocket_sha1",
"kmac_cshake",
"ascon",
"xxh3",
"rapidhash",
"blake2",
"blake3"
],
"auth": ["auth", "kmac_cshake", "password_hashing", "rsa"],
"rsa": ["rsa"],
"aead": ["aead"]
Expand All @@ -27,6 +37,12 @@
"required": true,
"features": ["parallel", "sha3"]
},
"websocket_sha1": {
"binary": "websocket_sha1",
"kind": "criterion",
"required": true,
"features": ["websocket-sha1"]
},
"kmac_cshake": {
"binary": "kmac_cshake",
"kind": "criterion",
Expand Down Expand Up @@ -151,6 +167,11 @@
"sha3-256": { "crate": "hashes", "bench": "sha3", "filter": "sha3-256" },
"sha3-384": { "crate": "hashes", "bench": "sha3", "filter": "sha3-384" },
"sha3-512": { "crate": "hashes", "bench": "sha3", "filter": "sha3-512" },
"websocket-accept-digest": {
"crate": "hashes",
"bench": "websocket_sha1",
"filter": "websocket-accept-digest"
},
"shake128": { "crate": "hashes", "bench": "sha3", "filter": "shake128" },
"shake256": { "crate": "hashes", "bench": "sha3", "filter": "shake256" },
"cshake256": { "crate": "auth", "bench": "kmac_cshake", "filter": "cshake256" },
Expand Down Expand Up @@ -213,6 +234,7 @@
"sha3-256",
"sha3-384",
"sha3-512",
"websocket-accept-digest",
"shake128",
"shake256",
"cshake256",
Expand Down Expand Up @@ -316,6 +338,7 @@
"sha3-256",
"sha3-384",
"sha3-512",
"websocket-accept-digest",
"shake128",
"shake256",
"cshake256",
Expand All @@ -337,6 +360,7 @@
"sha3-256",
"sha3-384",
"sha3-512",
"websocket-accept-digest",
"shake128",
"shake256",
"cshake256",
Expand All @@ -360,6 +384,7 @@
"crc",
"sha2",
"sha3",
"websocket_sha1",
"kmac_cshake",
"ascon",
"auth",
Expand All @@ -371,7 +396,16 @@
"kernels": ["blake3"],
"checksum_comp": ["crc"],
"checksum_kernels": ["crc"],
"hashes_comp": ["sha2", "sha3", "kmac_cshake", "ascon", "xxh3", "rapidhash", "blake3"],
"hashes_comp": [
"sha2",
"sha3",
"websocket_sha1",
"kmac_cshake",
"ascon",
"xxh3",
"rapidhash",
"blake3"
],
"auth_comp": ["auth"],
"aead_comp": ["aead"],
"hashes_kernels": ["blake3"]
Expand Down
4 changes: 1 addition & 3 deletions .github/workflows/_ci-suite.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,7 @@ jobs:
name: Cargo Graph Assurance
uses: ./.github/workflows/_rust-job.yaml
with:
# PR runners are provisioned from protected main, whose pre-transition
# definition still enables RunsOn MagicCache until this change lands.
runner: ubuntu-latest
runner: runs-on=${{ github.run_id }}/runner=linux-x64-ci
timeout_minutes: 90
tools_mode: rail
toolchain_components: "rust-src"
Expand Down
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,10 @@ ascon-hash = []
xxh3 = []
rapidhash = []

# Compatibility-only WebSocket handshake capability. Deliberately excluded
# from every umbrella feature, including `full`.
websocket-sha1 = []

hmac = ["sha2"]
hmac-sha3 = ["sha3"]
hkdf = ["hmac"]
Expand Down Expand Up @@ -238,6 +242,7 @@ scrypt = { version = "0.12.0", default-features = false }
sha2 = "0.11.0"
sha2_010 = { package = "sha2", version = "0.10.9" }
sha3 = "0.12.0"
sha1 = { version = "0.11.0", default-features = false }
keccak = "0.2.1"
xxhash-rust = { version = "0.8.18", features = ["xxh3"] }
rapidhash = "=4.5.1"
Expand Down Expand Up @@ -366,6 +371,11 @@ name = "sha3"
harness = false
required-features = ["sha3"]

[[bench]]
name = "websocket_sha1"
harness = false
required-features = ["websocket-sha1"]

[[bench]]
name = "ascon"
harness = false
Expand Down
8 changes: 5 additions & 3 deletions THREAT_MODEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ Review the `ct_intended` candidate core before the rest of the repository:

This order prioritizes secret-dependent computation; it does not remove public
parsers, dispatch, or unsafe kernels from the security boundary. Public parsing,
raw hashes, checksums, non-cryptographic hashes, public-key verification math,
benchmark paths, and unlisted build configurations carry no blanket
constant-time claim.
raw hashes, compatibility-only WebSocket accept digests, checksums,
non-cryptographic hashes, public-key verification math, benchmark paths, and
unlisted build configurations carry no blanket constant-time claim.

## System boundary

Expand Down Expand Up @@ -118,6 +118,7 @@ Ordered by exposure to untrusted input:
| Secret-bearing compute | Sign, decrypt, decapsulate, derive; the release-evidenced subset of `ct.toml` | Timing leakage, incorrect arithmetic |
| `unsafe` low-level code | SIMD/assembly kernels, raw buffer helpers, zeroization, and dispatch | Undefined behavior, divergence from the portable authority |
| Dispatch | `src/platform`, `src/backend` | Selecting a kernel the CPU cannot run, or one that produces wrong output |
| Compatibility operations | `hashes::legacy::WebSocketAcceptDigest::compute` | Capability expansion or treating broken SHA-1 collision resistance as authentication |

## Mitigations and evidence

Expand All @@ -130,6 +131,7 @@ Ordered by exposure to untrusted input:
| Oracle behavior | Opaque errors, failed-open output clearing, single-bit failure shape | AEAD and verification tests, fuzz targets |
| Secret exposure at rest | Zeroize at the last owned use and on drop, masked `Debug` and errors, and sealed fixed-size comparison only on semantic secret owners | [`docs/secret-ownership.md`](docs/secret-ownership.md), [`docs/secret-lifecycle.md`](docs/secret-lifecycle.md), `scripts/check/zeroize-evidence.sh`, and `tests/secret_redaction.rs` |
| Supply chain | Minimal optional runtime dependencies, `cargo deny`, `cargo audit`, signed tags, Trusted Publishing, release attestations | [`deny.toml`](deny.toml), [`.github/workflows/release.yaml`](.github/workflows/release.yaml), [`docs/release.md`](docs/release.md) |
| Legacy primitive misuse | Semantic-only API, explicit leaf feature, no umbrella membership, no raw/streaming SHA-1 | Feature-boundary check, compile-fail root-surface doctest, RFC/oracle tests |

## Known gaps

Expand Down
35 changes: 35 additions & 0 deletions benches/websocket_sha1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//! RFC 6455 WebSocket accept-digest comparison benchmark.

use core::hint::black_box;

use criterion::{Criterion, Throughput, criterion_group, criterion_main};
use rscrypto::hashes::legacy::WebSocketAcceptDigest;
use sha1::{Digest as _, Sha1};

const WEBSOCKET_GUID: &[u8] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
const REPRESENTATIVE_KEY: &[u8] = b"dGhlIHNhbXBsZSBub25jZQ==";

fn websocket_accept_digest(c: &mut Criterion) {
let mut group = c.benchmark_group("websocket-accept-digest/24-byte-key");
group.throughput(Throughput::Bytes(
REPRESENTATIVE_KEY.len().strict_add(WEBSOCKET_GUID.len()) as u64,
));

group.bench_function("rscrypto", |b| {
b.iter(|| black_box(WebSocketAcceptDigest::compute(black_box(REPRESENTATIVE_KEY))))
});

group.bench_function("rustcrypto", |b| {
b.iter(|| {
let mut sha1 = Sha1::new();
sha1.update(black_box(REPRESENTATIVE_KEY));
sha1.update(WEBSOCKET_GUID);
black_box(sha1.finalize())
})
});

group.finish();
}

criterion_group!(benches, websocket_accept_digest);
criterion_main!(benches);
4 changes: 4 additions & 0 deletions docs/benchmarking.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ shape-compatible:
| ---------------------------- | --------------------------------------------------------------------------- |
| AEAD | RustCrypto AEADs, `aws-lc-rs`, `ring`, `aegis` |
| SHA-2 / HMAC / HKDF / PBKDF2 | RustCrypto, `aws-lc-rs`, `ring` |
| WebSocket accept digest | RustCrypto `sha1`, with identical field bytes and fixed RFC 6455 GUID |
| BLAKE2 / BLAKE3 | RustCrypto, `dryoc`, upstream `blake3` |
| ECDSA P-256/P-384 | RustCrypto `p256`/`p384`, `aws-lc-rs`, `ring` |
| Ed25519 / X25519 | dalek, `aws-lc-rs`, `ring` where API-compatible, `dryoc` |
Expand Down Expand Up @@ -101,6 +102,9 @@ Some common libraries are not primary benchmark baselines:
batched inverse with reusable scratch and cleanup. Use it with the fixed- and
caller-entropy signing rows to distinguish inverse cost from CRT
exponentiation and whole-operation overhead.
- The WebSocket row measures one 24-byte `Sec-WebSocket-Key` field value plus
the fixed 36-byte RFC 6455 GUID. Both implementations receive the same two
slices; Base64 and HTTP parsing are intentionally outside the timed region.
- `ring` X25519 is excluded from static-key Diffie-Hellman rows because its
public API exposes an ephemeral agreement shape that consumes the private key.
- `dryoc` XChaCha20-Poly1305 is excluded from one-shot AEAD rows because the
Expand Down
15 changes: 15 additions & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,21 @@ rscrypto = { version = "0.8.1", features = ["full", "portable-only"] }
| `aegis256` | -- | AEGIS-256 |
| `ascon-aead` | -- | Ascon-AEAD128 |

### Compatibility-only features

| Feature | Pulls in | Enables |
| ---------------- | -------- | ------------------------------------------------------------------------------- |
| `websocket-sha1` | -- | RFC 6455 `hashes::legacy::WebSocketAcceptDigest`; no raw or streaming SHA-1 API |

`websocket-sha1` is deliberately excluded from `crypto-hashes`, `hashes`, and
`full`. Cargo's `--all-features` activates it because that flag activates every
declared leaf, but no rscrypto feature activates it transitively. The operation
exists only for WebSocket handshake compatibility: it hashes the field bytes
and RFC 6455 GUID, returns the public 20-byte digest, and leaves HTTP parsing and
Base64 encoding to the caller. SHA-1 collision resistance is broken; do not use
this capability for authentication, signatures, certificates, content
identity, or any new protocol.

### Auxiliary features

| Feature | Effect |
Expand Down
1 change: 1 addition & 0 deletions docs/test-vector-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ the concrete inputs and outputs of each cryptography API.
| SHA-384 | `tests/sha2_official_vectors.rs`, `testdata/sha2/sha384.blb` | Streaming/oneshot and dispatch equivalence; shared SHA-512-family boundary test panics before overlength absorption | Inputs beyond the FIPS 180-4 length field panic |
| SHA-512 | `tests/sha2_official_vectors.rs`, `tests/sha512_differential.rs`, `testdata/sha2/sha512.blb` | Streaming/oneshot and dispatch equivalence; `message_length_above_boundary_panics` covers the encoded-length boundary | Inputs beyond the FIPS 180-4 length field panic |
| SHA-512/256 | `tests/sha2_official_vectors.rs`, `testdata/sha2/sha512_256.blb` | Streaming/oneshot and dispatch equivalence; shared SHA-512-family boundary test panics before overlength absorption | Inputs beyond the FIPS 180-4 length field panic |
| WebSocket accept digest | RFC 6455 example plus private SHA-1 KATs in `src/hashes/legacy/sha1.rs`; RustCrypto `sha1` differential coverage in `tests/websocket_sha1.rs` and `fuzz/target_impls/hash_websocket_sha1.rs` | Byte-preservation checks and key lengths crossing 55/56/63/64-byte padding and block boundaries | Compatibility-only SHA-1 capability; no collision-resistance or authentication claim |
| SHA3-224/256/384/512 | `tests/sha3_official_vectors.rs`, `tests/sha3_differential.rs`, `testdata/sha3/sha3_*.blb` | Streaming/oneshot and dispatch equivalence | No invalid input class |
| SHAKE128 | `tests/sha3_official_vectors.rs`, `tests/shake128_differential.rs`, `testdata/sha3/shake128.blb` | XOF output-length boundaries | No invalid input class |
| SHAKE256 | `tests/sha3_official_vectors.rs`, `tests/shake256_differential.rs`, `testdata/sha3/shake256.blb` | XOF output-length boundaries | No invalid input class |
Expand Down
13 changes: 13 additions & 0 deletions docs/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,19 @@ Aliases: `hashes::crypto::AsconXof128` and `hashes::crypto::AsconXof128Reader`.
`Blake2bKey` and `Blake2sKey` make caller-facing key validation explicit while
borrowing key bytes without allocation or copying.

## Compatibility-only protocol operations

Feature: `websocket-sha1` (not included by `crypto-hashes`, `hashes`, or `full`).

| Type | Output | Standard |
| --------------------------------------- | ------ | -------- |
| `hashes::legacy::WebSocketAcceptDigest` | 20B | RFC 6455 |

`WebSocketAcceptDigest::compute` hashes the caller's field value byte-for-byte
with the fixed RFC GUID. It exposes neither raw nor streaming SHA-1. The digest
is public compatibility data, not an authentication result; the caller owns
HTTP parsing and Base64 encoding.

## Fast hashes

Features: `fast-hashes` or `xxh3` / `rapidhash`.
Expand Down
Loading