pq_crypto-seal 0.1.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 +7 -0
- data/.github/workflows/ci.yml +66 -0
- data/CHANGELOG.md +44 -0
- data/FORMAT.md +139 -0
- data/GET_STARTED.md +30 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +215 -0
- data/RELEASING.md +34 -0
- data/Rakefile +19 -0
- data/SECURITY.md +85 -0
- data/VENDORING.md +14 -0
- data/ext/pq_crypto_seal/aegis_unused_stubs.c +20 -0
- data/ext/pq_crypto_seal/extconf.rb +296 -0
- data/ext/pq_crypto_seal/pq_crypto_seal.c +390 -0
- data/ext/pq_crypto_seal/vendor/libaegis/LICENSE +21 -0
- data/ext/pq_crypto_seal/vendor/libaegis/TREE_SHA256 +1 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l.c +246 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_aesni.c +73 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_aesni.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_altivec.c +70 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_altivec.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_common.h +691 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_neon_aes.c +77 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_neon_aes.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_neon_sha3.c +86 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_neon_sha3.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_soft.c +63 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_soft.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/implementations.h +40 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2.c +247 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_aesni.c +108 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_aesni.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_altivec.c +103 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_altivec.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_avx2.c +79 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_avx2.h +11 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_common.h +795 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_neon_aes.c +111 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_neon_aes.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_soft.c +96 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_soft.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/implementations.h +40 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4.c +252 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_aesni.c +117 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_aesni.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_altivec.c +111 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_altivec.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_avx2.c +112 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_avx2.h +11 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_avx512.c +85 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_avx512.h +11 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_common.h +808 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_neon_aes.c +119 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_neon_aes.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_soft.c +104 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_soft.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/implementations.h +41 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256.c +240 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_aesni.c +68 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_aesni.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_altivec.c +68 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_altivec.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_common.h +673 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_neon_aes.c +75 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_neon_aes.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_soft.c +58 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_soft.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/implementations.h +39 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2.c +247 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_aesni.c +103 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_aesni.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_altivec.c +101 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_altivec.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_avx2.c +74 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_avx2.h +11 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_common.h +790 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_neon_aes.c +109 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_neon_aes.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_soft.c +91 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_soft.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/implementations.h +41 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4.c +252 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_aesni.c +112 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_aesni.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_altivec.c +109 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_altivec.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_avx2.c +107 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_avx2.h +11 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_avx512.c +79 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_avx512.h +11 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_common.h +805 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_neon_aes.c +117 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_neon_aes.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_soft.c +99 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_soft.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/implementations.h +42 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/common/common.c +81 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/common/common.h +144 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/common/cpu.c +378 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/common/cpu.h +34 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/common/keccak.c +187 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/common/keccak.h +13 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/common/softaes.c +338 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/common/softaes.h +69 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/include/aegis.h +77 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/include/aegis128l.h +311 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/include/aegis128x2.h +313 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/include/aegis128x4.h +313 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/include/aegis256.h +311 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/include/aegis256x2.h +313 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/include/aegis256x4.h +313 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/include/aegis_raf.h +580 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf.c +87 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis128l.c +20 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis128x2.c +20 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis128x4.c +20 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis256.c +20 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis256x2.c +20 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis256x4.c +20 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_internal.h +152 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_merkle.c +268 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_merkle.h +165 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_variant.h +1119 -0
- data/fuzz/decrypt_fuzz.rb +119 -0
- data/lib/pq_crypto/seal/binary.rb +62 -0
- data/lib/pq_crypto/seal/core.rb +398 -0
- data/lib/pq_crypto/seal/errors.rb +15 -0
- data/lib/pq_crypto/seal/format.rb +275 -0
- data/lib/pq_crypto/seal/io.rb +336 -0
- data/lib/pq_crypto/seal/native.rb +13 -0
- data/lib/pq_crypto/seal/padding.rb +47 -0
- data/lib/pq_crypto/seal/version.rb +6 -0
- data/lib/pq_crypto/seal.rb +32 -0
- data/pq_crypto-seal.gemspec +37 -0
- data/script/vendor_libs.rb +79 -0
- data/test/aegis_vectors_test.rb +54 -0
- data/test/golden_vectors_test.rb +183 -0
- data/test/io_test.rb +125 -0
- data/test/native_test.rb +36 -0
- data/test/seal_test.rb +238 -0
- data/test/test_helper.rb +6 -0
- metadata +244 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9f0508e0149f076a9f8d6c9a829b516a41e2998dc5af916db9d0d4696349d36c
|
|
4
|
+
data.tar.gz: ce0dd7ba92b94ae212f22ea73cc9754b2028da7037a46539c5408dd60bae961b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 248f9187168dd55054827e08dbfcf771555101e00f748cdcef55dbaed31ba4c4fb98ce6f6017c05c00bc5f78de23cfcd4c457541eda93cc2537be8be749711e6
|
|
7
|
+
data.tar.gz: b96cd381d599e6e9a52e0eba84de72500e6746e744183c5d383ed61d31305874393d640c8a1a79dbf288c7db1daeb1379ffc3c125443b5c0c4b2a39b14088402
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
strategy:
|
|
10
|
+
fail-fast: false
|
|
11
|
+
matrix:
|
|
12
|
+
os: [ubuntu-24.04, macos-14]
|
|
13
|
+
ruby: ["2.7.1", "3.1", "3.3", "3.4", "4.0"]
|
|
14
|
+
exclude:
|
|
15
|
+
- os: macos-14
|
|
16
|
+
ruby: "2.7.1"
|
|
17
|
+
runs-on: ${{ matrix.os }}
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- if: runner.os == 'Linux'
|
|
21
|
+
run: sudo apt-get update && sudo apt-get install -y libssl-dev
|
|
22
|
+
- if: runner.os == 'macOS'
|
|
23
|
+
run: brew install openssl@3 || true
|
|
24
|
+
- uses: ruby/setup-ruby@v1
|
|
25
|
+
with:
|
|
26
|
+
ruby-version: ${{ matrix.ruby }}
|
|
27
|
+
bundler-cache: true
|
|
28
|
+
env:
|
|
29
|
+
PKG_CONFIG_PATH: /opt/homebrew/opt/openssl@3/lib/pkgconfig:/usr/local/opt/openssl@3/lib/pkgconfig
|
|
30
|
+
- run: bundle exec rake
|
|
31
|
+
- name: Verify OpenSSL 3 linkage on Linux Ruby 2.7.1
|
|
32
|
+
if: matrix.os == 'ubuntu-24.04' && matrix.ruby == '2.7.1'
|
|
33
|
+
shell: bash
|
|
34
|
+
run: |
|
|
35
|
+
set -euxo pipefail
|
|
36
|
+
extension="lib/pq_crypto/seal/pq_crypto_seal.so"
|
|
37
|
+
test -f "${extension}"
|
|
38
|
+
ldd "${extension}" | tee linked-libraries.txt
|
|
39
|
+
grep -E 'libcrypto\.so\.3([[:space:]]|$)' linked-libraries.txt
|
|
40
|
+
- name: Show mkmf logs on failure
|
|
41
|
+
if: failure()
|
|
42
|
+
run: find tmp -name mkmf.log -print -exec cat {} \; || true
|
|
43
|
+
- run: bundle exec ruby script/vendor_libs.rb --check
|
|
44
|
+
|
|
45
|
+
sanitize:
|
|
46
|
+
runs-on: ubuntu-24.04
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v4
|
|
49
|
+
- run: sudo apt-get update && sudo apt-get install -y libssl-dev
|
|
50
|
+
- uses: ruby/setup-ruby@v1
|
|
51
|
+
with:
|
|
52
|
+
ruby-version: "3.4"
|
|
53
|
+
bundler-cache: true
|
|
54
|
+
- run: bundle exec rake clean
|
|
55
|
+
- run: PQC_SEAL_SANITIZE=1 bundle exec rake compile
|
|
56
|
+
- name: Show mkmf logs on compile failure
|
|
57
|
+
if: failure()
|
|
58
|
+
run: find tmp -name mkmf.log -print -exec cat {} \; || true
|
|
59
|
+
- name: Run sanitizer tests
|
|
60
|
+
env:
|
|
61
|
+
N: "0"
|
|
62
|
+
run: |
|
|
63
|
+
export LD_PRELOAD="$(gcc -print-file-name=libasan.so)"
|
|
64
|
+
export ASAN_OPTIONS="detect_leaks=0:halt_on_error=1:abort_on_error=1"
|
|
65
|
+
export UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=1"
|
|
66
|
+
bundle exec rake test
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
- AEGIS streaming state rejects `update`/`final` before `initialize` (`mode` guard).
|
|
6
|
+
- Encrypt path wipes the internal plaintext copy in `ensure` (caller buffers unchanged).
|
|
7
|
+
- CI: Actions pinned by commit SHA, `permissions: contents: read`, fuzz + gem-smoke jobs.
|
|
8
|
+
- Repository hygiene: `.DS_Store` / build artifacts ignored; no tracked binaries.
|
|
9
|
+
|
|
10
|
+
- Initial versioned `PQCSEAL1` envelope format.
|
|
11
|
+
- Multi-recipient DEK wrapping through `PQCrypto::HybridKEM` algorithm
|
|
12
|
+
`:ml_kem_768_x25519_xwing` (pinned; not `CANONICAL_ALGORITHM`).
|
|
13
|
+
- Runtime dependency pinned to `pq_crypto ~> 0.6.4` so suite ID 1 cannot drift
|
|
14
|
+
with a future 0.7+ line.
|
|
15
|
+
- Exact X-Wing size checks: public key 1216, ciphertext 1120, shared secret 32.
|
|
16
|
+
- HKDF-SHA256 key separation and AEGIS-256 payload/DEK encryption.
|
|
17
|
+
- Authenticated `padding_policy_id` in the immutable header (`none` / `padme` /
|
|
18
|
+
fixed / buckets).
|
|
19
|
+
- String, IO, and atomic file APIs with strict EOF/size defaults; framed helpers
|
|
20
|
+
`encrypt_frame_io` / `decrypt_frame_io` for embedded envelopes.
|
|
21
|
+
- Resource limits: `max_staging_bytes`, `max_plaintext_bytes`, `max_envelope_bytes`
|
|
22
|
+
with safe defaults against payload amplification.
|
|
23
|
+
- Full-envelope Padmé padding.
|
|
24
|
+
- Recipient-section rebuild and DEK rotation operations.
|
|
25
|
+
- Public API hygiene: internal helpers (`unwrap_dek`, `derive_kek`, …) are
|
|
26
|
+
private class methods.
|
|
27
|
+
- Malformed wire fields raise `FormatError` (not `InvalidConfigurationError`).
|
|
28
|
+
- `ResourceLimitError` for ceiling violations.
|
|
29
|
+
- Golden-vector suite pinning the X-Wing wire contract, the immutable-header
|
|
30
|
+
byte layout, the payload AD derivation, and byte-identical one-shot vs
|
|
31
|
+
incremental output under a deterministic RNG (`test/golden_vectors_test.rb`).
|
|
32
|
+
- Self-driving decrypt/parser fuzz job in CI (mutated and random envelopes),
|
|
33
|
+
failing on any error outside the documented classes.
|
|
34
|
+
- CI GitHub Actions pinned by commit SHA; workflow `permissions` reduced to
|
|
35
|
+
`contents: read`.
|
|
36
|
+
- Robust OpenSSL 3 selection for Homebrew Intel/Apple Silicon, `pkg-config`,
|
|
37
|
+
environment roots, and explicit extconf paths.
|
|
38
|
+
- Remove OpenSSL 1.1 include paths inherited from Ruby 2.7 and link the selected
|
|
39
|
+
OpenSSL 3 `libcrypto` by absolute path.
|
|
40
|
+
- Avoid unreliable OpenSSL link probes in legacy Ruby 2.7 `mkmf`; verify headers
|
|
41
|
+
at configure time and the linked runtime in the extension initializer.
|
|
42
|
+
- Disable Minitest's parallel executor only in the ASan job (`N=0`) to avoid a
|
|
43
|
+
known teardown conflict caused by preloading ASan into an otherwise unsanitized
|
|
44
|
+
Ruby executable.
|
data/FORMAT.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# PQCSEAL1 wire format
|
|
2
|
+
|
|
3
|
+
All integers are unsigned big-endian. Parsers reject trailing bytes, unknown
|
|
4
|
+
flags, unsupported suites, noncanonical lengths, and arithmetic overflow before
|
|
5
|
+
allocating from untrusted lengths.
|
|
6
|
+
|
|
7
|
+
## Immutable payload header
|
|
8
|
+
|
|
9
|
+
| Field | Size |
|
|
10
|
+
|---|---:|
|
|
11
|
+
| magic (`PQCSEAL1`) | 8 |
|
|
12
|
+
| format version (`1`) | 1 |
|
|
13
|
+
| header length | 4 |
|
|
14
|
+
| content suite ID (`1`) | 2 |
|
|
15
|
+
| lookup mode (`1`, scoped hint) | 1 |
|
|
16
|
+
| flags (`0`) | 2 |
|
|
17
|
+
| padding policy ID | 1 |
|
|
18
|
+
| payload ID | 32 |
|
|
19
|
+
| payload nonce | 32 |
|
|
20
|
+
| recipient capacity | 2 |
|
|
21
|
+
| slot size | 2 |
|
|
22
|
+
| padded inner length | 8 |
|
|
23
|
+
| public metadata length | 4 |
|
|
24
|
+
| public metadata | variable |
|
|
25
|
+
|
|
26
|
+
Padding policy IDs:
|
|
27
|
+
|
|
28
|
+
| ID | Meaning |
|
|
29
|
+
|---:|---|
|
|
30
|
+
| 0 | none |
|
|
31
|
+
| 1 | Padmé over the complete final envelope size |
|
|
32
|
+
| 2 | fixed target (`padding: { to: N }`) |
|
|
33
|
+
| 3 | application buckets (`padding: { buckets: [...] }`) |
|
|
34
|
+
|
|
35
|
+
The payload AEGIS associated data is exactly:
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
SHA-256(exact immutable payload header bytes)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Content suite `1` fixes AEGIS-256, a 32-byte nonce, a 32-byte tag, and
|
|
42
|
+
inner-frame-v1. The padding policy ID records how the final envelope length was
|
|
43
|
+
chosen; it is authenticated as part of the header.
|
|
44
|
+
|
|
45
|
+
## Mutable recipient section
|
|
46
|
+
|
|
47
|
+
| Field | Size |
|
|
48
|
+
|---|---:|
|
|
49
|
+
| wrap suite ID (`1`) | 2 |
|
|
50
|
+
| random section ID | 32 |
|
|
51
|
+
| fixed slots | `recipient_capacity * slot_size` |
|
|
52
|
+
|
|
53
|
+
Every complete recipient-section rebuild creates a new random section ID and
|
|
54
|
+
recreates all real and dummy slots. Mixed wrapping suites are forbidden.
|
|
55
|
+
|
|
56
|
+
Wrap suite `1` fixes MLKEM768-X25519/X-Wing-compatible encapsulation
|
|
57
|
+
(`pq_crypto` algorithm `:ml_kem_768_x25519_xwing`), HKDF-SHA256, AEGIS-256 DEK
|
|
58
|
+
wrapping, and 32-byte wrap nonces/tags. Exact sizes:
|
|
59
|
+
|
|
60
|
+
| Value | Bytes |
|
|
61
|
+
|---|---:|
|
|
62
|
+
| public key | 1216 |
|
|
63
|
+
| KEM ciphertext | 1120 |
|
|
64
|
+
| shared secret | 32 |
|
|
65
|
+
|
|
66
|
+
## Slot layout for wrap suite 1
|
|
67
|
+
|
|
68
|
+
| Field | Size |
|
|
69
|
+
|---|---:|
|
|
70
|
+
| recipient hint | 32 |
|
|
71
|
+
| KEM ciphertext | 1120 |
|
|
72
|
+
| wrap nonce | 32 |
|
|
73
|
+
| encrypted DEK | 32 |
|
|
74
|
+
| wrap tag | 32 |
|
|
75
|
+
| authenticated random slot padding | `slot_size - 1248` |
|
|
76
|
+
|
|
77
|
+
The hint is:
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
SHA-256(
|
|
81
|
+
"PQC-SEAL-V1-RECIPIENT-HINT\0" ||
|
|
82
|
+
payload_id || section_id || uint16(wrap_suite_id) || canonical_public_key
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The 32-byte KEK is derived with RFC 5869 HKDF-SHA256. Salt is the RFC 5869
|
|
87
|
+
default of 32 zero bytes (HashLen zeros). `info` is:
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
"PQC-SEAL-V1-WRAP-KEY\0" ||
|
|
91
|
+
payload_id || section_id || uint16(wrap_suite_id) || uint16(slot_index)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The AEGIS AD for wrapping is:
|
|
95
|
+
|
|
96
|
+
```text
|
|
97
|
+
SHA-256(
|
|
98
|
+
"PQC-SEAL-V1-WRAP-AD\0" ||
|
|
99
|
+
payload_header_hash || section_id || uint16(slot_index) ||
|
|
100
|
+
uint16(wrap_suite_id) || recipient_hint || slot_padding
|
|
101
|
+
)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Dummy slots execute the same full KEM/HKDF/AEGIS pipeline against disposable
|
|
105
|
+
keypairs and random dummy DEKs. No recipient count is serialized.
|
|
106
|
+
|
|
107
|
+
## Encrypted inner frame
|
|
108
|
+
|
|
109
|
+
| Field | Size |
|
|
110
|
+
|---|---:|
|
|
111
|
+
| inner version (`1`) | 1 |
|
|
112
|
+
| inner flags (`0`) | 1 |
|
|
113
|
+
| real content length | 8 |
|
|
114
|
+
| private metadata length | 4 |
|
|
115
|
+
| private metadata | variable |
|
|
116
|
+
| content | variable |
|
|
117
|
+
| random encrypted padding | variable |
|
|
118
|
+
|
|
119
|
+
The final field following the encrypted inner frame is the 32-byte payload tag.
|
|
120
|
+
The padding amount is chosen so the size of the complete final envelope reaches
|
|
121
|
+
the configured Padmé/fixed/bucket target.
|
|
122
|
+
|
|
123
|
+
During incremental decryption, all inner bytes are untrusted until the final tag
|
|
124
|
+
has been verified. Implementations stage the complete inner frame, authenticate
|
|
125
|
+
it, then parse lengths and publish content.
|
|
126
|
+
|
|
127
|
+
## Defaults
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
slot_size: 2048 bytes (configurable 2048..8192, multiple of 256)
|
|
131
|
+
recipient_capacity: 4 (maximum 32; never derived automatically from ACL size)
|
|
132
|
+
padding: Padmé over the complete final envelope size
|
|
133
|
+
max_staging_bytes: ~1 GiB + private-metadata ceiling
|
|
134
|
+
max_plaintext_bytes:~1 GiB
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Parsers reject trailing bytes on the one-shot and default IO paths. Framed
|
|
138
|
+
stream consumers that intentionally embed one envelope inside a larger stream
|
|
139
|
+
must use `encrypt_frame_io` / `decrypt_frame_io`.
|
data/GET_STARTED.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
## Build prerequisites
|
|
4
|
+
|
|
5
|
+
Ruby 2.7.1+ and OpenSSL 3.0+ development files are required. On macOS the
|
|
6
|
+
build automatically searches Homebrew `openssl@3`. For a custom installation:
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
bundle config set --local build.pq_crypto-seal \
|
|
10
|
+
"--with-openssl-dir=$(brew --prefix openssl@3)"
|
|
11
|
+
bundle exec rake clean compile
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
require "pq_crypto/seal"
|
|
16
|
+
|
|
17
|
+
recipient = PQCrypto::HybridKEM.generate(PQCrypto::Seal::WRAP_KEM_ALGORITHM)
|
|
18
|
+
envelope = PQCrypto::Seal.encrypt("secret bytes", to: recipient.public_key)
|
|
19
|
+
plaintext = PQCrypto::Seal.decrypt(envelope, with: recipient)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
For files, use `encrypt_file` and `decrypt_file`; decryption stages and verifies
|
|
23
|
+
the complete inner frame before publishing plaintext. Use `recipient_capacity`
|
|
24
|
+
and `slot_size` as long-lived application policy values because both are stored
|
|
25
|
+
in the immutable payload header.
|
|
26
|
+
|
|
27
|
+
Recipient-section rebuilds always require the application's complete,
|
|
28
|
+
authoritative public-key ACL. They do not revoke access to plaintext or old
|
|
29
|
+
envelope copies. Use `rotate_dek`/`rotate_dek_file` when a new payload key and
|
|
30
|
+
full re-encryption are required.
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Roman Haydarov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# pq_crypto-seal
|
|
2
|
+
|
|
3
|
+
Post-quantum envelope encryption for Ruby 2.7.1+.
|
|
4
|
+
|
|
5
|
+
The gem turns `pq_crypto`'s hybrid KEM into practical document encryption:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
MLKEM768-X25519 shared secret → HKDF-SHA256 → KEK → wrap random DEK
|
|
9
|
+
random DEK → AEGIS-256 → document
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`libaegis` 0.10.3 is vendored and compiled into the extension. The existing
|
|
13
|
+
`pq_crypto` gem supplies the hybrid KEM and OpenSSL-backed primitives.
|
|
14
|
+
|
|
15
|
+
> **Status:** 0.1.0 is experimental cryptographic software; format v1; not
|
|
16
|
+
> independently audited. Read `SECURITY.md` before using it for irreplaceable data.
|
|
17
|
+
>
|
|
18
|
+
> `wrap_suite_id = 1` is pinned to `:ml_kem_768_x25519_xwing` and `pq_crypto ~> 0.6.4`.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
gem "pq_crypto", "~> 0.6.4"
|
|
24
|
+
gem "pq_crypto-seal", "~> 0.1.0"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Ruby `>= 2.7.1` and OpenSSL `>= 3.0` development files are required for a
|
|
28
|
+
source build.
|
|
29
|
+
|
|
30
|
+
On macOS, `extconf.rb` automatically searches Homebrew `openssl@3` on both
|
|
31
|
+
Apple Silicon and Intel installations. If OpenSSL 3 is installed in a custom
|
|
32
|
+
location, configure it explicitly:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
brew install openssl@3
|
|
36
|
+
bundle config set --local build.pq_crypto-seal \
|
|
37
|
+
"--with-openssl-dir=$(brew --prefix openssl@3)"
|
|
38
|
+
bundle exec rake clean compile
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`OPENSSL_ROOT_DIR` and `OPENSSL_DIR` are also supported. An explicit
|
|
42
|
+
`--with-openssl-dir`, `--with-openssl-include`, or `--with-openssl-lib` always
|
|
43
|
+
takes precedence over automatic discovery.
|
|
44
|
+
|
|
45
|
+
## String API
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
require "pq_crypto/seal"
|
|
49
|
+
|
|
50
|
+
alice = PQCrypto::HybridKEM.generate(PQCrypto::Seal::WRAP_KEM_ALGORITHM)
|
|
51
|
+
bob = PQCrypto::HybridKEM.generate(PQCrypto::Seal::WRAP_KEM_ALGORITHM)
|
|
52
|
+
|
|
53
|
+
sealed = PQCrypto::Seal.encrypt(
|
|
54
|
+
image_bytes,
|
|
55
|
+
to: [alice.public_key, bob.public_key],
|
|
56
|
+
metadata: { mime: "image/png" }.to_json,
|
|
57
|
+
public_metadata: "tenant-7",
|
|
58
|
+
recipient_capacity: 4,
|
|
59
|
+
slot_size: 2048,
|
|
60
|
+
padding: :padme
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
opened = PQCrypto::Seal.open(sealed, with: alice) # keypair supplies secret + public hint material
|
|
64
|
+
opened.data
|
|
65
|
+
opened.metadata
|
|
66
|
+
opened.public_metadata
|
|
67
|
+
|
|
68
|
+
PQCrypto::Seal.decrypt(sealed, with: bob)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
All random keys and nonces are generated internally. There is no public API for
|
|
72
|
+
supplying a DEK or nonce. Because scoped hint lookup needs the matching public
|
|
73
|
+
key, `with:` accepts a `HybridKEM::Keypair`. When keys are stored separately:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
credentials = PQCrypto::Seal.credentials(
|
|
77
|
+
secret_key: loaded_secret_key,
|
|
78
|
+
public_key: loaded_public_key
|
|
79
|
+
)
|
|
80
|
+
PQCrypto::Seal.decrypt(sealed, with: credentials)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Files and IO
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
PQCrypto::Seal.encrypt_file(
|
|
87
|
+
"scan.tiff",
|
|
88
|
+
"scan.tiff.pqcseal",
|
|
89
|
+
to: alice.public_key
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
PQCrypto::Seal.decrypt_file(
|
|
93
|
+
"scan.tiff.pqcseal",
|
|
94
|
+
"restored.tiff",
|
|
95
|
+
with: alice
|
|
96
|
+
)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`encrypt_io` accepts the exact plaintext `size:`. `decrypt_io` stages the entire
|
|
100
|
+
unauthenticated inner frame to a mode-0600 temporary file and copies content to
|
|
101
|
+
the caller's output only after the final AEGIS tag is valid.
|
|
102
|
+
|
|
103
|
+
## Recipients and key lifecycle
|
|
104
|
+
|
|
105
|
+
The envelope intentionally does not contain an authoritative list of recipient
|
|
106
|
+
public keys. The application owns the ACL. Any complete recipient-section
|
|
107
|
+
rewrite therefore receives the full list:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
updated = PQCrypto::Seal.rebuild_recipients(
|
|
111
|
+
sealed,
|
|
112
|
+
with: alice,
|
|
113
|
+
recipients: [alice.public_key, carol.public_key]
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`add_recipient(..., recipient:, current_recipients:)` still requires the
|
|
118
|
+
complete current ACL and rebuilds all slots.
|
|
119
|
+
`drop_recipient_stanza(..., remaining_recipients:)` describes exactly what it
|
|
120
|
+
does. It does **not** revoke plaintext, a DEK, or old copies already obtained.
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
rotated = PQCrypto::Seal.rotate_dek(
|
|
124
|
+
sealed,
|
|
125
|
+
with: alice,
|
|
126
|
+
recipients: [alice.public_key, carol.public_key]
|
|
127
|
+
) # preserves the existing envelope size by default
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`rebuild_recipients` preserves the DEK and encrypted payload. It is an
|
|
131
|
+
operational migration of the current canonical copy, not protection against old
|
|
132
|
+
saved envelopes. `rotate_dek` creates a new DEK and re-encrypts the payload; it
|
|
133
|
+
still cannot erase knowledge from prior copies. Rotation preserves the current
|
|
134
|
+
final envelope size by default (`padding: :preserve`); pass `:padme`, `:none`, or
|
|
135
|
+
an explicit padding policy to recalculate it.
|
|
136
|
+
|
|
137
|
+
## Wire-format v1
|
|
138
|
+
|
|
139
|
+
The exact bytes are implemented in `PQCrypto::Seal::Format`:
|
|
140
|
+
|
|
141
|
+
```text
|
|
142
|
+
immutable payload header
|
|
143
|
+
magic "PQCSEAL1"
|
|
144
|
+
version
|
|
145
|
+
content_suite_id = AEGIS-256 payload profile
|
|
146
|
+
lookup_mode = payload-scoped recipient hint
|
|
147
|
+
payload_id, payload_nonce
|
|
148
|
+
recipient_capacity, slot_size
|
|
149
|
+
padded_inner_length
|
|
150
|
+
public metadata
|
|
151
|
+
|
|
152
|
+
mutable recipient section
|
|
153
|
+
wrap_suite_id = MLKEM768-X25519 + HKDF-SHA256 + AEGIS-256 wrap
|
|
154
|
+
random section_id (changes on every complete rebuild)
|
|
155
|
+
fixed-capacity, fixed-size slots
|
|
156
|
+
|
|
157
|
+
slot
|
|
158
|
+
recipient_hint
|
|
159
|
+
1120-byte hybrid KEM ciphertext
|
|
160
|
+
wrap nonce
|
|
161
|
+
wrapped 32-byte DEK
|
|
162
|
+
32-byte tag
|
|
163
|
+
authenticated random slot padding
|
|
164
|
+
|
|
165
|
+
AEGIS-encrypted inner frame
|
|
166
|
+
authenticated content length and private metadata length
|
|
167
|
+
private metadata
|
|
168
|
+
content
|
|
169
|
+
encrypted Padmé padding
|
|
170
|
+
|
|
171
|
+
32-byte payload tag
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`content_suite_id` and `wrap_suite_id` are deliberately independent. A recipient
|
|
175
|
+
section can be rebuilt without changing payload AD as long as the new wrapping
|
|
176
|
+
stanza fits the immutable `slot_size`.
|
|
177
|
+
|
|
178
|
+
Defaults:
|
|
179
|
+
|
|
180
|
+
```text
|
|
181
|
+
slot_size: 2048 bytes (configurable 2048..8192, multiple of 256)
|
|
182
|
+
recipient_capacity: 4 (maximum 32; never derived automatically from ACL size)
|
|
183
|
+
padding: Padmé over the complete final envelope size
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The payload-and-section-scoped recipient hint avoids a stable global recipient identifier and prevents stable hints from revealing real slots across recipient-section rebuilds.
|
|
187
|
+
Anyone who already knows a candidate public key can test for its presence. No
|
|
188
|
+
formal recipient anonymity is claimed.
|
|
189
|
+
|
|
190
|
+
The wrapping KEM follows the MLKEM768-X25519/X-Wing construction, targeting
|
|
191
|
+
approximately 128-bit security. ML-KEM-768 supplies conservative margin; the
|
|
192
|
+
full hybrid suite is not advertised as NIST category 3.
|
|
193
|
+
|
|
194
|
+
## Envelope identity
|
|
195
|
+
|
|
196
|
+
`PQCrypto::Seal.digest(envelope)` is SHA-256 over the **complete** envelope
|
|
197
|
+
bytes. The recipient section is intentionally mutable, so
|
|
198
|
+
`rebuild_recipients` / `rotate_dek` change the digest even when the encrypted
|
|
199
|
+
payload is unchanged. Use `opened.payload_id` (or `inspect_envelope`) for a
|
|
200
|
+
stable document identifier.
|
|
201
|
+
|
|
202
|
+
High `recipient_capacity` makes encryption slower: every empty slot still runs
|
|
203
|
+
a full X-Wing encapsulation against a disposable keypair.
|
|
204
|
+
|
|
205
|
+
## Tests
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
bundle install
|
|
209
|
+
bundle exec rake
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Set `PQC_SEAL_SANITIZE=1` when compiling under ASan/UBSan. The suite covers
|
|
213
|
+
AEGIS one-shot/incremental equivalence, tampering, multi-recipient opening,
|
|
214
|
+
full-envelope padding, recipient rebuilds, staged file publication, and parser
|
|
215
|
+
limits.
|
data/RELEASING.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
1. Run `bundle exec ruby script/vendor_libs.rb --check`.
|
|
4
|
+
2. Confirm `pq_crypto` dependency remains `~> 0.6.4` and `WRAP_KEM_ALGORITHM`
|
|
5
|
+
is still `:ml_kem_768_x25519_xwing` with the documented sizes.
|
|
6
|
+
3. Run the full CI matrix and sanitizer job.
|
|
7
|
+
4. Build with `gem build pq_crypto-seal.gemspec`.
|
|
8
|
+
5. Install the exact built gem into an empty `GEM_HOME` together with the
|
|
9
|
+
supported `pq_crypto` gem and run String and File round trips.
|
|
10
|
+
6. Confirm golden envelope vectors and pinned X-Wing wire-contract checks in
|
|
11
|
+
`test/golden_vectors_test.rb` pass, and that the CI fuzz job is green.
|
|
12
|
+
7. Publish only from a signed tag through RubyGems Trusted Publishing.
|
|
13
|
+
|
|
14
|
+
Vendored libaegis sources are pinned by archive SHA-256 and a deterministic
|
|
15
|
+
source-tree digest (`vendor/libaegis/TREE_SHA256`). The full upstream snapshot
|
|
16
|
+
ships intact — including families and the RAF sources that this gem does not
|
|
17
|
+
compile — so `script/vendor_libs.rb --check` can prove the vendored tree is
|
|
18
|
+
byte-identical to the audited archive. Never delete vendored files to slim the
|
|
19
|
+
gem; that would break the provenance guarantee. Never update the snapshot
|
|
20
|
+
without rerunning official KATs, cross-backend equivalence checks, envelope
|
|
21
|
+
golden vectors, and fuzz tests.
|
|
22
|
+
|
|
23
|
+
The golden vectors fix every value this gem controls (recipient keys, DEK,
|
|
24
|
+
nonces, section id, padding bytes, metadata, plaintext) and assert the derived
|
|
25
|
+
header bytes, associated data, and one-shot/incremental equivalence. A fully
|
|
26
|
+
frozen "plaintext to exact bytes" vector is intentionally not asserted for the
|
|
27
|
+
KEM ciphertext region, because X-Wing encapsulation randomness originates inside
|
|
28
|
+
`pq_crypto` and is not injectable here; the wire-contract check pins its exact
|
|
29
|
+
sizes and round-trip instead.
|
|
30
|
+
|
|
31
|
+
The ASan job sets `detect_leaks=0` because ASan is preloaded into an otherwise
|
|
32
|
+
unsanitized Ruby interpreter; host-process allocator noise would otherwise fail
|
|
33
|
+
the job. Memory-safety errors still abort via `halt_on_error`.
|
|
34
|
+
|
data/Rakefile
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rake/testtask"
|
|
4
|
+
require "rake/extensiontask"
|
|
5
|
+
require_relative "lib/pq_crypto/seal/version"
|
|
6
|
+
|
|
7
|
+
Rake::ExtensionTask.new("pq_crypto_seal", Gem::Specification.load("pq_crypto-seal.gemspec")) do |ext|
|
|
8
|
+
ext.lib_dir = "lib/pq_crypto/seal"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
Rake::TestTask.new do |task|
|
|
12
|
+
task.libs << "lib"
|
|
13
|
+
task.libs << "test"
|
|
14
|
+
task.pattern = "test/**/*_test.rb"
|
|
15
|
+
task.warning = false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
task test: :compile
|
|
19
|
+
task default: :test
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
This project has not received an independent cryptographic or implementation
|
|
4
|
+
audit. Do not treat version 0.1.0 as a substitute for a reviewed storage design.
|
|
5
|
+
|
|
6
|
+
Experimental cryptographic software; format v1; not independently audited.
|
|
7
|
+
|
|
8
|
+
## Threat model
|
|
9
|
+
|
|
10
|
+
The v1 format protects confidentiality and integrity of the **payload** when an
|
|
11
|
+
attacker obtains a passive copy of envelopes but not a recipient secret key.
|
|
12
|
+
|
|
13
|
+
Successful opening authenticates:
|
|
14
|
+
|
|
15
|
+
- the immutable payload header
|
|
16
|
+
- the selected recipient stanza that yielded the DEK
|
|
17
|
+
- the encrypted inner frame (private metadata + content + encrypted padding)
|
|
18
|
+
|
|
19
|
+
Successful opening does **not** authenticate:
|
|
20
|
+
|
|
21
|
+
- the integrity of other recipients' stanzas
|
|
22
|
+
- the completeness or correctness of the application ACL
|
|
23
|
+
- absence of rollback against a replaceable canonical envelope
|
|
24
|
+
|
|
25
|
+
It does not claim recipient anonymity. Anyone who already knows a candidate
|
|
26
|
+
public key can test for its presence via the scoped recipient hint.
|
|
27
|
+
|
|
28
|
+
Removing a recipient stanza does not revoke plaintext, a DEK, or an older copy
|
|
29
|
+
already obtained by that recipient. Rebuilding recipient stanzas protects the
|
|
30
|
+
current copy only. Rotating the DEK creates a new protected version but cannot
|
|
31
|
+
remove knowledge from older copies.
|
|
32
|
+
|
|
33
|
+
## Side channels
|
|
34
|
+
|
|
35
|
+
Recipient lookup short-circuits slots whose hint does not match. The number of
|
|
36
|
+
full KEM decapsulations therefore depends on how many slots share the expected
|
|
37
|
+
hint. Against a purely passive adversary holding only the envelope bytes this is
|
|
38
|
+
out of scope. Local timing, cache, or power side channels on a machine that
|
|
39
|
+
already holds a recipient secret key are not mitigated. `AmbiguousRecipientStanzas`
|
|
40
|
+
is raised when more than one slot opens successfully.
|
|
41
|
+
|
|
42
|
+
Caller-owned plaintext and metadata buffers are not wiped by the encrypt path;
|
|
43
|
+
callers that need residual-data hygiene must clear their own buffers.
|
|
44
|
+
`Opened#data` / `Opened#metadata` returned from `open` are the caller's
|
|
45
|
+
responsibility after use.
|
|
46
|
+
|
|
47
|
+
## Envelope digests
|
|
48
|
+
|
|
49
|
+
`Seal.digest` hashes the complete envelope, including the mutable recipient
|
|
50
|
+
section. Prefer `payload_id` (exposed on `Opened` and `Inspection`) when a
|
|
51
|
+
stable document identifier is required.
|
|
52
|
+
|
|
53
|
+
## Resource limits
|
|
54
|
+
|
|
55
|
+
Untrusted envelopes declare `padded_inner_length` as a uint64. Decrypt APIs
|
|
56
|
+
enforce default ceilings (`max_staging_bytes`, `max_plaintext_bytes`,
|
|
57
|
+
`max_envelope_bytes`) so a sender who knows a recipient public key cannot force
|
|
58
|
+
unbounded staging or disk amplification before the final AEGIS tag fails.
|
|
59
|
+
|
|
60
|
+
- `max_staging_bytes` / `max_envelope_bytes` are checked **before** AEAD
|
|
61
|
+
verification (pre-auth).
|
|
62
|
+
- `max_plaintext_bytes` is enforced **after** the payload tag verifies
|
|
63
|
+
(post-auth), because the real content length is authenticated inside the
|
|
64
|
+
inner frame.
|
|
65
|
+
|
|
66
|
+
Applications may raise the limits explicitly when larger objects are required.
|
|
67
|
+
|
|
68
|
+
## Staging
|
|
69
|
+
|
|
70
|
+
Large decryptions stage unauthenticated plaintext in a mode-0600 temporary file.
|
|
71
|
+
On Unix the file is unlinked immediately after opening. The staged bytes are
|
|
72
|
+
never returned or published before the final AEGIS tag is verified. Platforms
|
|
73
|
+
that cannot unlink an open file, filesystem snapshots, and storage-layer
|
|
74
|
+
forensics may still retain remnants; use a suitably protected staging filesystem.
|
|
75
|
+
|
|
76
|
+
## Wire suite pinning
|
|
77
|
+
|
|
78
|
+
`wrap_suite_id = 1` is permanently bound to MLKEM768-X25519 / X-Wing (draft-10
|
|
79
|
+
compatible) with fixed public-key (1216), ciphertext (1120), and shared-secret
|
|
80
|
+
(32) sizes, as implemented by `pq_crypto ~> 0.6.4`. Seal does not follow
|
|
81
|
+
`HybridKEM::CANONICAL_ALGORITHM`; a future change of that constant must not
|
|
82
|
+
alter suite 1 wire bytes. Always generate keys with
|
|
83
|
+
`PQCrypto::HybridKEM.generate(PQCrypto::Seal::WRAP_KEM_ALGORITHM)`.
|
|
84
|
+
|
|
85
|
+
Report vulnerabilities privately to the repository owner.
|