pq_crypto-seal 0.1.0 → 0.1.2
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/CHANGELOG.md +69 -0
- data/FORMAT.md +24 -9
- data/GET_STARTED.md +23 -7
- data/README.md +40 -15
- data/RELEASING.md +9 -7
- data/SECURITY.md +78 -17
- data/VENDORING.md +13 -0
- data/lib/pq_crypto/seal/binary.rb +50 -41
- data/lib/pq_crypto/seal/core.rb +198 -335
- data/lib/pq_crypto/seal/envelope.rb +158 -0
- data/lib/pq_crypto/seal/files.rb +147 -0
- data/lib/pq_crypto/seal/format.rb +193 -165
- data/lib/pq_crypto/seal/io.rb +94 -300
- data/lib/pq_crypto/seal/io_helpers.rb +184 -0
- data/lib/pq_crypto/seal/models.rb +46 -0
- data/lib/pq_crypto/seal/padding.rb +199 -28
- data/lib/pq_crypto/seal/recipients.rb +245 -0
- data/lib/pq_crypto/seal/resource_limits.rb +56 -0
- data/lib/pq_crypto/seal/secrets.rb +21 -0
- data/lib/pq_crypto/seal/streaming.rb +270 -0
- data/lib/pq_crypto/seal/version.rb +2 -1
- data/lib/pq_crypto/seal.rb +18 -6
- data/pq_crypto-seal.gemspec +23 -8
- metadata +12 -95
- data/.github/workflows/ci.yml +0 -66
- data/Gemfile +0 -7
- data/Rakefile +0 -19
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l.c +0 -246
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_aesni.c +0 -73
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_aesni.h +0 -9
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_altivec.c +0 -70
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_altivec.h +0 -9
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_common.h +0 -691
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_neon_aes.c +0 -77
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_neon_aes.h +0 -9
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_neon_sha3.c +0 -86
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_neon_sha3.h +0 -9
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_soft.c +0 -63
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_soft.h +0 -9
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/implementations.h +0 -40
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2.c +0 -247
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_aesni.c +0 -108
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_aesni.h +0 -9
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_altivec.c +0 -103
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_altivec.h +0 -9
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_avx2.c +0 -79
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_avx2.h +0 -11
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_common.h +0 -795
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_neon_aes.c +0 -111
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_neon_aes.h +0 -9
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_soft.c +0 -96
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_soft.h +0 -9
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/implementations.h +0 -40
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4.c +0 -252
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_aesni.c +0 -117
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_aesni.h +0 -9
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_altivec.c +0 -111
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_altivec.h +0 -9
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_avx2.c +0 -112
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_avx2.h +0 -11
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_avx512.c +0 -85
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_avx512.h +0 -11
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_common.h +0 -808
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_neon_aes.c +0 -119
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_neon_aes.h +0 -9
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_soft.c +0 -104
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_soft.h +0 -9
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/implementations.h +0 -41
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2.c +0 -247
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_aesni.c +0 -103
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_aesni.h +0 -9
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_altivec.c +0 -101
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_altivec.h +0 -9
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_avx2.c +0 -74
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_avx2.h +0 -11
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_common.h +0 -790
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_neon_aes.c +0 -109
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_neon_aes.h +0 -9
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_soft.c +0 -91
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_soft.h +0 -9
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/implementations.h +0 -41
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4.c +0 -252
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_aesni.c +0 -112
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_aesni.h +0 -9
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_altivec.c +0 -109
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_altivec.h +0 -9
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_avx2.c +0 -107
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_avx2.h +0 -11
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_avx512.c +0 -79
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_avx512.h +0 -11
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_common.h +0 -805
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_neon_aes.c +0 -117
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_neon_aes.h +0 -9
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_soft.c +0 -99
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_soft.h +0 -9
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/implementations.h +0 -42
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf.c +0 -87
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis128l.c +0 -20
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis128x2.c +0 -20
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis128x4.c +0 -20
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis256.c +0 -20
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis256x2.c +0 -20
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis256x4.c +0 -20
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_internal.h +0 -152
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_merkle.c +0 -268
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_merkle.h +0 -165
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_variant.h +0 -1119
- data/fuzz/decrypt_fuzz.rb +0 -119
- data/script/vendor_libs.rb +0 -79
- data/test/aegis_vectors_test.rb +0 -54
- data/test/golden_vectors_test.rb +0 -183
- data/test/io_test.rb +0 -125
- data/test/native_test.rb +0 -36
- data/test/seal_test.rb +0 -238
- data/test/test_helper.rb +0 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0e2f25bfc50a4e95892fb3c9e25992ff359bd16cc65535dacf3863d943c34e52
|
|
4
|
+
data.tar.gz: 40462d41fa188924ba4abbd5d76b4ad22c48377964d70a4544bae919ec4f5140
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8a3f087d4c922637fcbf5af86cae6a9e118b688215f9a218c6572e524555643e2d444f7452416575c2904f08a8c34742adc0e2cb4bfab6e33a0267015f03d177
|
|
7
|
+
data.tar.gz: e1746e895c1be25bd755fc358c4eac0291b21c6c638b7d87fb1603e08229783e942adcecbdc4c095e4ae1fde9ca00d49a7f472a89cc1ef183339a94f9546e025
|
data/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,76 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.2
|
|
4
|
+
|
|
5
|
+
### Security
|
|
6
|
+
|
|
7
|
+
- **Default padding enforcement on decrypt.** `decrypt` / `open` / `decrypt_io` /
|
|
8
|
+
`decrypt_file` now default to `required_padding: :from_header`. Padmé / none
|
|
9
|
+
get a full canonical size check from authenticated lengths. Fixed / buckets
|
|
10
|
+
are not self-describing on the wire, so `:from_header` only accepts the
|
|
11
|
+
authenticated policy id (envelope still opens); pass
|
|
12
|
+
`required_padding: { to: N }` / `{ buckets: [...] }` for full target
|
|
13
|
+
enforcement. Pass `required_padding: false` to opt out entirely.
|
|
14
|
+
- **RUP-safe IO staging.** `decrypt_io` / `decrypt_file` / DEK rotation stage
|
|
15
|
+
**ciphertext** on the temporary 0600 file and only materialise plaintext after
|
|
16
|
+
the AEGIS tag verifies (second pass). Unauthenticated plaintext is no longer
|
|
17
|
+
written to disk before authentication.
|
|
18
|
+
- **Honest recipient ACL API.** Removed `drop_recipient_stanza` and
|
|
19
|
+
`drop_recipient_stanza_file`. Those names suggested revocation while old copies
|
|
20
|
+
and the DEK remain valid. Use `rebuild_recipients` / `rebuild_recipients_file`
|
|
21
|
+
with the application's complete authoritative ACL for the current canonical
|
|
22
|
+
copy only; use `rotate_dek` when a new payload key is required.
|
|
23
|
+
- **Capacity / slot growth path.** `recipient_capacity` and `slot_size` remain
|
|
24
|
+
immutable in the payload header for a given envelope, but `rotate_dek` /
|
|
25
|
+
`rotate_dek_file` now accept optional `recipient_capacity:` and `slot_size:`
|
|
26
|
+
overrides because they re-encrypt the authenticated plaintext under a new DEK.
|
|
27
|
+
- **At-rest draft pinning documented.** SECURITY.md / FORMAT.md / VENDORING.md
|
|
28
|
+
state that suite 1 and content suite 1 are pinned to X-Wing draft-10 and
|
|
29
|
+
AEGIS-256 (IETF CFRG draft-18 vectors via libaegis 0.10.3), not to future RFC
|
|
30
|
+
text. External KATs are referenced; self-generated golden vectors pin Seal's
|
|
31
|
+
own wire layout only.
|
|
32
|
+
- **CI / release hygiene matched to claims.** Workflow Actions pinned by full
|
|
33
|
+
commit SHA, top-level `permissions: contents: read`, and jobs for fuzz,
|
|
34
|
+
gem-smoke, and `release_contract_check`. `.DS_Store` is gitignored; the
|
|
35
|
+
release-contract checker rejects tracked copies.
|
|
36
|
+
|
|
37
|
+
### Breaking (0.1.x experimental)
|
|
38
|
+
|
|
39
|
+
- Default `required_padding` is `:from_header` (was effectively off / `nil`).
|
|
40
|
+
- `drop_recipient_stanza` / `drop_recipient_stanza_file` removed from the public API.
|
|
41
|
+
|
|
42
|
+
## 0.1.1
|
|
43
|
+
|
|
44
|
+
- Release hygiene: CI Actions pinned by commit SHA, `permissions: contents: read`,
|
|
45
|
+
fuzz + gem-smoke + `release_contract_check` jobs; ignore `.DS_Store` / binaries.
|
|
46
|
+
- Aggressive internal refactor: parsing, recipient wrapping, one-shot operations,
|
|
47
|
+
streaming, atomic files, padding policies, and resource limits now live in
|
|
48
|
+
focused objects instead of large branching methods. Public API and wire v1
|
|
49
|
+
remain unchanged.
|
|
50
|
+
- Padding enforcement now validates both policy id and the canonical target;
|
|
51
|
+
parameterized fixed/bucket requirements cannot masquerade as one another.
|
|
52
|
+
- Added exact deterministic envelope/stream equivalence, official X-Wing
|
|
53
|
+
draft-10 decapsulation KATs, partial-read/write regression tests, and a
|
|
54
|
+
release-contract checker.
|
|
55
|
+
- Release gem contains production code plus only the AEGIS-256 sources needed
|
|
56
|
+
to compile; tests, CI, fuzz harnesses, RAF, and unused AEGIS families stay out.
|
|
57
|
+
- CI restores pinned Actions, sanitizer fuzz smoke, and isolated built-gem E2E.
|
|
58
|
+
|
|
59
|
+
- IO: all output paths use `write_all` (reject silent partial writes).
|
|
60
|
+
- Decrypt APIs accept `required_padding:` and can enforce Padmé / none / fixed /
|
|
61
|
+
buckets after successful AEAD (wire format unchanged).
|
|
62
|
+
- Runtime dependency pinned to `pq_crypto = 0.6.4`.
|
|
63
|
+
- Resource limits applied to `inspect_envelope`, `rebuild_recipients`,
|
|
64
|
+
`rotate_dek`, and `rebuild_recipients_file`.
|
|
65
|
+
- Default one-shot limits lowered to 64 MiB (use file/IO APIs for larger docs).
|
|
66
|
+
- Document that `rotate_dek` allocates a new `payload_id`.
|
|
67
|
+
- CI: Actions pinned by commit SHA; `permissions: contents: read`; fuzz and
|
|
68
|
+
gem-smoke jobs restored.
|
|
69
|
+
- Repository hygiene: ignore `.DS_Store` and built binaries.
|
|
70
|
+
|
|
3
71
|
## 0.1.0
|
|
4
72
|
|
|
73
|
+
|
|
5
74
|
- AEGIS streaming state rejects `update`/`final` before `initialize` (`mode` guard).
|
|
6
75
|
- Encrypt path wipes the internal plaintext copy in `ensure` (caller buffers unchanged).
|
|
7
76
|
- CI: Actions pinned by commit SHA, `permissions: contents: read`, fuzz + gem-smoke jobs.
|
data/FORMAT.md
CHANGED
|
@@ -38,9 +38,10 @@ The payload AEGIS associated data is exactly:
|
|
|
38
38
|
SHA-256(exact immutable payload header bytes)
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
Content suite `1` fixes AEGIS-256
|
|
42
|
-
inner-frame-v1. The padding policy ID records
|
|
43
|
-
chosen; it is authenticated as part of the
|
|
41
|
+
Content suite `1` fixes AEGIS-256 (libaegis 0.10.3; CFRG draft-18 KATs), a
|
|
42
|
+
32-byte nonce, a 32-byte tag, and inner-frame-v1. The padding policy ID records
|
|
43
|
+
how the final envelope length was chosen; it is authenticated as part of the
|
|
44
|
+
header.
|
|
44
45
|
|
|
45
46
|
## Mutable recipient section
|
|
46
47
|
|
|
@@ -53,9 +54,10 @@ chosen; it is authenticated as part of the header.
|
|
|
53
54
|
Every complete recipient-section rebuild creates a new random section ID and
|
|
54
55
|
recreates all real and dummy slots. Mixed wrapping suites are forbidden.
|
|
55
56
|
|
|
56
|
-
Wrap suite `1` fixes MLKEM768-X25519/X-Wing-compatible encapsulation
|
|
57
|
-
(`pq_crypto` algorithm `:ml_kem_768_x25519_xwing`), HKDF-SHA256, AEGIS-256
|
|
58
|
-
wrapping, and 32-byte wrap nonces/tags.
|
|
57
|
+
Wrap suite `1` fixes MLKEM768-X25519/X-Wing **draft-10**-compatible encapsulation
|
|
58
|
+
(`pq_crypto = 0.6.4` algorithm `:ml_kem_768_x25519_xwing`), HKDF-SHA256, AEGIS-256
|
|
59
|
+
DEK wrapping, and 32-byte wrap nonces/tags. The suite id freezes that behaviour
|
|
60
|
+
for at-rest data even if a later RFC text diverges. Exact sizes:
|
|
59
61
|
|
|
60
62
|
| Value | Bytes |
|
|
61
63
|
|---|---:|
|
|
@@ -121,8 +123,9 @@ The padding amount is chosen so the size of the complete final envelope reaches
|
|
|
121
123
|
the configured Padmé/fixed/bucket target.
|
|
122
124
|
|
|
123
125
|
During incremental decryption, all inner bytes are untrusted until the final tag
|
|
124
|
-
has been verified. Implementations stage the complete inner frame,
|
|
125
|
-
|
|
126
|
+
has been verified. Implementations stage the complete **ciphertext** inner frame,
|
|
127
|
+
authenticate the tag, materialise plaintext only after verification, then parse
|
|
128
|
+
lengths and publish content.
|
|
126
129
|
|
|
127
130
|
## Defaults
|
|
128
131
|
|
|
@@ -131,9 +134,21 @@ slot_size: 2048 bytes (configurable 2048..8192, multiple of 256)
|
|
|
131
134
|
recipient_capacity: 4 (maximum 32; never derived automatically from ACL size)
|
|
132
135
|
padding: Padmé over the complete final envelope size
|
|
133
136
|
max_staging_bytes: ~1 GiB + private-metadata ceiling
|
|
134
|
-
max_plaintext_bytes
|
|
137
|
+
max_plaintext_bytes:64 MiB by default
|
|
135
138
|
```
|
|
136
139
|
|
|
137
140
|
Parsers reject trailing bytes on the one-shot and default IO paths. Framed
|
|
138
141
|
stream consumers that intentionally embed one envelope inside a larger stream
|
|
139
142
|
must use `encrypt_frame_io` / `decrypt_frame_io`.
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
## Receiver-side padding checks (0.1.2+)
|
|
146
|
+
|
|
147
|
+
`padding_policy_id` is authenticated but does not by itself encode fixed targets
|
|
148
|
+
or bucket lists. Decrypt APIs default to `required_padding: :from_header`:
|
|
149
|
+
|
|
150
|
+
- Padmé / none — full canonical size enforcement
|
|
151
|
+
- fixed / buckets — accept authenticated policy id; full target checks need
|
|
152
|
+
`required_padding: { to: N }` or `{ buckets: [...] }`
|
|
153
|
+
|
|
154
|
+
Pass `required_padding: false` to skip enforcement.
|
data/GET_STARTED.md
CHANGED
|
@@ -19,12 +19,28 @@ envelope = PQCrypto::Seal.encrypt("secret bytes", to: recipient.public_key)
|
|
|
19
19
|
plaintext = PQCrypto::Seal.decrypt(envelope, with: recipient)
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
For files, use `encrypt_file` and `decrypt_file`; decryption stages
|
|
23
|
-
the
|
|
24
|
-
and `slot_size` as long-lived application policy values
|
|
25
|
-
in the immutable payload header.
|
|
22
|
+
For files, use `encrypt_file` and `decrypt_file`; decryption stages ciphertext,
|
|
23
|
+
verifies the AEGIS tag, then materialises plaintext before publishing. Use
|
|
24
|
+
`recipient_capacity` and `slot_size` as long-lived application policy values:
|
|
25
|
+
both are stored in the immutable payload header. To grow them after encryption,
|
|
26
|
+
call `rotate_dek` / `rotate_dek_file` with new `recipient_capacity:` /
|
|
27
|
+
`slot_size:` (full re-encryption under a new DEK).
|
|
26
28
|
|
|
27
29
|
Recipient-section rebuilds always require the application's complete,
|
|
28
|
-
authoritative public-key ACL. They do not revoke access to plaintext
|
|
29
|
-
envelope copies.
|
|
30
|
-
|
|
30
|
+
authoritative public-key ACL. They do not revoke access to plaintext, the DEK,
|
|
31
|
+
or old envelope copies. Prefer `rebuild_recipients` over inventing “drop”
|
|
32
|
+
helpers. Use `rotate_dek` when a new payload key is required.
|
|
33
|
+
|
|
34
|
+
Decrypt defaults to `required_padding: :from_header`: Padmé/none get a full size
|
|
35
|
+
check; fixed/buckets open with a policy-id check (pass `{ to: }` / `{ buckets: }`
|
|
36
|
+
for full target enforcement).
|
|
37
|
+
|
|
38
|
+
## Large documents
|
|
39
|
+
|
|
40
|
+
Default one-shot limits are 64 MiB (`max_plaintext_bytes` / related staging and
|
|
41
|
+
envelope ceilings). For larger objects use `encrypt_file` / `decrypt_file` or
|
|
42
|
+
`encrypt_io` / `decrypt_io` and raise the limits explicitly, for example:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
PQCrypto::Seal.decrypt_file(src, dst, with: credentials, max_plaintext_bytes: 512 * 1024 * 1024)
|
|
46
|
+
```
|
data/README.md
CHANGED
|
@@ -12,16 +12,17 @@ random DEK → AEGIS-256 → document
|
|
|
12
12
|
`libaegis` 0.10.3 is vendored and compiled into the extension. The existing
|
|
13
13
|
`pq_crypto` gem supplies the hybrid KEM and OpenSSL-backed primitives.
|
|
14
14
|
|
|
15
|
-
> **Status:** 0.1.
|
|
15
|
+
> **Status:** 0.1.2 is experimental cryptographic software; format v1; not
|
|
16
16
|
> independently audited. Read `SECURITY.md` before using it for irreplaceable data.
|
|
17
17
|
>
|
|
18
|
-
> `wrap_suite_id = 1` is pinned to `:ml_kem_768_x25519_xwing` and `pq_crypto
|
|
18
|
+
> `wrap_suite_id = 1` is pinned to `:ml_kem_768_x25519_xwing` and `pq_crypto = 0.6.4`
|
|
19
|
+
> (X-Wing draft-10 wire sizes). Content suite 1 is AEGIS-256 via libaegis 0.10.3.
|
|
19
20
|
|
|
20
21
|
## Installation
|
|
21
22
|
|
|
22
23
|
```ruby
|
|
23
24
|
gem "pq_crypto", "~> 0.6.4"
|
|
24
|
-
gem "pq_crypto-seal", "~> 0.1.
|
|
25
|
+
gem "pq_crypto-seal", "~> 0.1.2"
|
|
25
26
|
```
|
|
26
27
|
|
|
27
28
|
Ruby `>= 2.7.1` and OpenSSL `>= 3.0` development files are required for a
|
|
@@ -96,17 +97,19 @@ PQCrypto::Seal.decrypt_file(
|
|
|
96
97
|
)
|
|
97
98
|
```
|
|
98
99
|
|
|
99
|
-
`encrypt_io` accepts the exact plaintext `size:`. `decrypt_io` stages the
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
`encrypt_io` accepts the exact plaintext `size:`. `decrypt_io` stages the
|
|
101
|
+
**ciphertext** inner frame to a mode-0600 temporary file, verifies the AEGIS tag,
|
|
102
|
+
then materialises plaintext and copies content to the caller's output. Decrypt
|
|
103
|
+
APIs default to `required_padding: :from_header`.
|
|
102
104
|
|
|
103
105
|
## Recipients and key lifecycle
|
|
104
106
|
|
|
105
107
|
The envelope intentionally does not contain an authoritative list of recipient
|
|
106
|
-
public keys. The application owns the ACL. Any
|
|
107
|
-
rewrite therefore receives the full list:
|
|
108
|
+
public keys. The application owns and must synchronise the ACL. Any recipient-section
|
|
109
|
+
rewrite therefore receives the **full** list:
|
|
108
110
|
|
|
109
111
|
```ruby
|
|
112
|
+
# Remove bob from the *current* copy only — not a cryptographic revocation.
|
|
110
113
|
updated = PQCrypto::Seal.rebuild_recipients(
|
|
111
114
|
sealed,
|
|
112
115
|
with: alice,
|
|
@@ -115,9 +118,9 @@ updated = PQCrypto::Seal.rebuild_recipients(
|
|
|
115
118
|
```
|
|
116
119
|
|
|
117
120
|
`add_recipient(..., recipient:, current_recipients:)` still requires the
|
|
118
|
-
complete current ACL and rebuilds all slots.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
+
complete current ACL and rebuilds all slots. There is no `drop_recipient_*`
|
|
122
|
+
helper: “drop” names falsely suggest revocation while old copies and the DEK
|
|
123
|
+
remain valid.
|
|
121
124
|
|
|
122
125
|
```ruby
|
|
123
126
|
rotated = PQCrypto::Seal.rotate_dek(
|
|
@@ -129,11 +132,15 @@ rotated = PQCrypto::Seal.rotate_dek(
|
|
|
129
132
|
|
|
130
133
|
`rebuild_recipients` preserves the DEK and encrypted payload. It is an
|
|
131
134
|
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
|
|
135
|
+
saved envelopes. `rotate_dek` creates a new DEK, a **new payload_id**, and re-encrypts the payload; it
|
|
133
136
|
still cannot erase knowledge from prior copies. Rotation preserves the current
|
|
134
137
|
final envelope size by default (`padding: :preserve`); pass `:padme`, `:none`, or
|
|
135
138
|
an explicit padding policy to recalculate it.
|
|
136
139
|
|
|
140
|
+
`recipient_capacity` and `slot_size` are immutable for a given payload header.
|
|
141
|
+
To grow either, re-encrypt via `rotate_dek` / `rotate_dek_file` with
|
|
142
|
+
`recipient_capacity:` / `slot_size:` overrides.
|
|
143
|
+
|
|
137
144
|
## Wire-format v1
|
|
138
145
|
|
|
139
146
|
The exact bytes are implemented in `PQCrypto::Seal::Format`:
|
|
@@ -187,9 +194,10 @@ The payload-and-section-scoped recipient hint avoids a stable global recipient i
|
|
|
187
194
|
Anyone who already knows a candidate public key can test for its presence. No
|
|
188
195
|
formal recipient anonymity is claimed.
|
|
189
196
|
|
|
190
|
-
The wrapping KEM follows the MLKEM768-X25519/X-Wing construction
|
|
191
|
-
approximately 128-bit security.
|
|
192
|
-
full hybrid suite is not advertised
|
|
197
|
+
The wrapping KEM follows the MLKEM768-X25519/X-Wing construction (draft-10
|
|
198
|
+
wire sizes pinned by suite id 1), targeting approximately 128-bit security.
|
|
199
|
+
ML-KEM-768 supplies conservative margin; the full hybrid suite is not advertised
|
|
200
|
+
as NIST category 3. See `SECURITY.md` for at-rest draft-pinning risk.
|
|
193
201
|
|
|
194
202
|
## Envelope identity
|
|
195
203
|
|
|
@@ -213,3 +221,20 @@ Set `PQC_SEAL_SANITIZE=1` when compiling under ASan/UBSan. The suite covers
|
|
|
213
221
|
AEGIS one-shot/incremental equivalence, tampering, multi-recipient opening,
|
|
214
222
|
full-envelope padding, recipient rebuilds, staged file publication, and parser
|
|
215
223
|
limits.
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
## Padding enforcement on decrypt
|
|
227
|
+
|
|
228
|
+
After AEAD verification, decrypt APIs **default** to
|
|
229
|
+
`required_padding: :from_header`:
|
|
230
|
+
|
|
231
|
+
- Padmé / none — full canonical size check
|
|
232
|
+
- fixed / buckets — opens with policy-id check only; pass parameters for full
|
|
233
|
+
target enforcement
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
PQCrypto::Seal.decrypt(envelope, with: credentials) # :from_header
|
|
237
|
+
PQCrypto::Seal.decrypt(envelope, with: credentials, required_padding: :padme)
|
|
238
|
+
PQCrypto::Seal.decrypt(envelope, with: credentials, required_padding: { to: 4096 })
|
|
239
|
+
PQCrypto::Seal.decrypt(envelope, with: credentials, required_padding: false) # opt out
|
|
240
|
+
```
|
data/RELEASING.md
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
# Releasing
|
|
2
2
|
|
|
3
3
|
1. Run `bundle exec ruby script/vendor_libs.rb --check`.
|
|
4
|
-
2. Confirm `pq_crypto` dependency remains
|
|
4
|
+
2. Confirm `pq_crypto` dependency remains `= 0.6.4` and `WRAP_KEM_ALGORITHM`
|
|
5
5
|
is still `:ml_kem_768_x25519_xwing` with the documented sizes.
|
|
6
|
-
3. Run
|
|
7
|
-
4.
|
|
8
|
-
5.
|
|
6
|
+
3. Run `bundle exec ruby script/release_contract_check.rb` (also enforced in CI).
|
|
7
|
+
4. Run the full CI matrix: test, sanitize, fuzz, release_contract, gem-smoke.
|
|
8
|
+
5. Build with `gem build pq_crypto-seal.gemspec`.
|
|
9
|
+
6. Re-run the release-contract checker against the built gem path.
|
|
10
|
+
7. Install the exact built gem into an empty `GEM_HOME` together with the
|
|
9
11
|
supported `pq_crypto` gem and run String and File round trips.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
8. Confirm golden envelope vectors, X-Wing draft-10 KATs, and AEGIS draft-18
|
|
13
|
+
KATs pass, and that the CI fuzz job is green.
|
|
14
|
+
9. Publish only from a signed tag through RubyGems Trusted Publishing.
|
|
13
15
|
|
|
14
16
|
Vendored libaegis sources are pinned by archive SHA-256 and a deterministic
|
|
15
17
|
source-tree digest (`vendor/libaegis/TREE_SHA256`). The full upstream snapshot
|
data/SECURITY.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Security
|
|
2
2
|
|
|
3
3
|
This project has not received an independent cryptographic or implementation
|
|
4
|
-
audit. Do not treat version 0.1.
|
|
4
|
+
audit. Do not treat version 0.1.2 as a substitute for a reviewed storage design.
|
|
5
5
|
|
|
6
6
|
Experimental cryptographic software; format v1; not independently audited.
|
|
7
7
|
|
|
@@ -25,10 +25,34 @@ Successful opening does **not** authenticate:
|
|
|
25
25
|
It does not claim recipient anonymity. Anyone who already knows a candidate
|
|
26
26
|
public key can test for its presence via the scoped recipient hint.
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
### Recipients, ACL ownership, and non-revocation
|
|
29
|
+
|
|
30
|
+
The envelope is **not** an authoritative ACL. The application must store and
|
|
31
|
+
synchronise the complete recipient public-key list itself. Every recipient-section
|
|
32
|
+
rewrite (`rebuild_recipients` / `add_recipient` and the `*_file` variants)
|
|
33
|
+
requires that full list and only protects the **current canonical copy**.
|
|
34
|
+
|
|
35
|
+
Rebuilding the recipient section does **not** revoke:
|
|
36
|
+
|
|
37
|
+
- plaintext already opened by a removed party
|
|
38
|
+
- the DEK (unchanged across rebuilds)
|
|
39
|
+
- older envelope bytes the removed party already copied
|
|
40
|
+
|
|
41
|
+
`rotate_dek` allocates a new DEK and a new `payload_id`, re-encrypts the payload,
|
|
42
|
+
and still cannot erase knowledge from prior copies. Treat any former recipient
|
|
43
|
+
who retained an old envelope and their secret key as still able to open that old
|
|
44
|
+
copy.
|
|
45
|
+
|
|
46
|
+
There is intentionally **no** `drop_recipient_*` API: a “drop” name encourages
|
|
47
|
+
the false belief that access was revoked.
|
|
48
|
+
|
|
49
|
+
### Capacity and slot size
|
|
50
|
+
|
|
51
|
+
`recipient_capacity` and `slot_size` are stored in the **immutable** payload
|
|
52
|
+
header and are associated-data for the payload AEAD. They cannot change for a
|
|
53
|
+
given encrypted payload without full re-encryption. Use `rotate_dek` /
|
|
54
|
+
`rotate_dek_file` with optional `recipient_capacity:` / `slot_size:` when the
|
|
55
|
+
application needs a larger slot table after authenticating the current payload.
|
|
32
56
|
|
|
33
57
|
## Side channels
|
|
34
58
|
|
|
@@ -44,6 +68,28 @@ callers that need residual-data hygiene must clear their own buffers.
|
|
|
44
68
|
`Opened#data` / `Opened#metadata` returned from `open` are the caller's
|
|
45
69
|
responsibility after use.
|
|
46
70
|
|
|
71
|
+
|
|
72
|
+
## payload_id stability
|
|
73
|
+
|
|
74
|
+
`payload_id` is stable across `rebuild_recipients` / add operations.
|
|
75
|
+
`rotate_dek` re-encrypts the payload with a fresh DEK and **allocates a new
|
|
76
|
+
`payload_id`** — treat the result as a new document identity. Applications that
|
|
77
|
+
need a stable external id across DEK rotation must track it outside the envelope.
|
|
78
|
+
|
|
79
|
+
## Padding policy enforcement
|
|
80
|
+
|
|
81
|
+
The authenticated header carries `padding_policy_id`. Decrypt/open APIs default
|
|
82
|
+
to `required_padding: :from_header`:
|
|
83
|
+
|
|
84
|
+
- Padmé / none — full check of policy id **and** canonical size from
|
|
85
|
+
authenticated inner lengths
|
|
86
|
+
- fixed / buckets — policy id only (targets are not on the wire); pass
|
|
87
|
+
`required_padding: { to: N }` or `{ buckets: [...] }` for full target
|
|
88
|
+
enforcement
|
|
89
|
+
|
|
90
|
+
Pass `required_padding: false` only when the application deliberately skips
|
|
91
|
+
length-hiding checks.
|
|
92
|
+
|
|
47
93
|
## Envelope digests
|
|
48
94
|
|
|
49
95
|
`Seal.digest` hashes the complete envelope, including the mutable recipient
|
|
@@ -65,21 +111,36 @@ unbounded staging or disk amplification before the final AEGIS tag fails.
|
|
|
65
111
|
|
|
66
112
|
Applications may raise the limits explicitly when larger objects are required.
|
|
67
113
|
|
|
68
|
-
## Staging
|
|
114
|
+
## Staging (RUP)
|
|
115
|
+
|
|
116
|
+
Large decryptions stage the **ciphertext** inner frame in a mode-0600 temporary
|
|
117
|
+
file. On Unix the file is unlinked immediately after opening. Intermediate AEGIS
|
|
118
|
+
streaming plaintext is wiped in memory and is not written to disk until the
|
|
119
|
+
final tag has verified; only then is verified plaintext materialised for
|
|
120
|
+
parsing and publication. Platforms that cannot unlink an open file, filesystem
|
|
121
|
+
snapshots, and storage-layer forensics may still retain remnants (including
|
|
122
|
+
ciphertext); use a suitably protected staging filesystem.
|
|
69
123
|
|
|
70
|
-
|
|
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.
|
|
124
|
+
## Wire suite pinning and draft risk
|
|
75
125
|
|
|
76
|
-
|
|
126
|
+
At-rest envelopes outlive draft documents. Suite identifiers freeze behaviour
|
|
127
|
+
independent of future RFC text:
|
|
77
128
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
`
|
|
82
|
-
|
|
129
|
+
| Component | Pin |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `wrap_suite_id = 1` | MLKEM768-X25519 / X-Wing **draft-10** compatible via `pq_crypto = 0.6.4` algorithm `:ml_kem_768_x25519_xwing` (PK 1216, CT 1120, SS 32) |
|
|
132
|
+
| `content_suite_id = 1` | AEGIS-256, 32-byte nonce/tag, inner-frame-v1, via vendored **libaegis 0.10.3** |
|
|
133
|
+
| AEGIS KATs | CFRG AEGIS draft-18 positive/negative vectors in `test/aegis_vectors_test.rb` |
|
|
134
|
+
| X-Wing KATs | draft-10 decapsulation vectors in `test/fixtures/xwing-draft-10-vector-*.json` (see `test/xwing_kat_test.rb`) |
|
|
135
|
+
|
|
136
|
+
Seal does **not** follow `HybridKEM::CANONICAL_ALGORITHM`; a future change of
|
|
137
|
+
that constant must not alter suite 1 wire bytes. Always generate keys with
|
|
83
138
|
`PQCrypto::HybridKEM.generate(PQCrypto::Seal::WRAP_KEM_ALGORITHM)`.
|
|
84
139
|
|
|
140
|
+
If a final RFC diverges from these pins, a new suite id (and migration) is
|
|
141
|
+
required; existing `PQCSEAL1` / suite-1 envelopes remain defined by this pin,
|
|
142
|
+
not by later drafts. Envelope golden vectors in `test/golden_vectors_test.rb`
|
|
143
|
+
are generated under a deterministic RNG to pin **Seal-controlled** layout and
|
|
144
|
+
labels; they are not a substitute for the external KEM/AEAD KATs above.
|
|
145
|
+
|
|
85
146
|
Report vulnerabilities privately to the repository owner.
|
data/VENDORING.md
CHANGED
|
@@ -8,7 +8,20 @@ libaegis 0.10.3.
|
|
|
8
8
|
- Tag commit: `0bb1639`
|
|
9
9
|
- Official tag archive SHA-256: `2f2682c1d08d9a5510caca1c82e3f8ea91f7085fef2ecbed0c398b2a921c79b1`
|
|
10
10
|
- License: MIT
|
|
11
|
+
- KATs exercised by this gem: CFRG AEGIS draft-18 vectors in
|
|
12
|
+
`test/aegis_vectors_test.rb` (content suite 1). Wire behaviour is pinned to
|
|
13
|
+
this snapshot, not to a future RFC that may diverge.
|
|
14
|
+
|
|
15
|
+
X-Wing / MLKEM768-X25519 wrap suite 1 is supplied by runtime dependency
|
|
16
|
+
`pq_crypto = 0.6.4` (not vendored here). External draft-10 decapsulation KATs
|
|
17
|
+
live in `test/fixtures/xwing-draft-10-vector-*.json`.
|
|
11
18
|
|
|
12
19
|
The complete source snapshot is retained, while the Ruby extension compiles only the AEGIS-256 and common implementation files it exposes. No system libaegis is
|
|
13
20
|
loaded at runtime. Update the version, archive checksum, source snapshot, KATs,
|
|
14
21
|
and cross-backend tests together.
|
|
22
|
+
|
|
23
|
+
The git tree keeps the full upstream snapshot so `script/vendor_libs.rb --check`
|
|
24
|
+
can prove byte-identity against the audited archive. The published gem package
|
|
25
|
+
ships only the AEGIS-256 family plus shared `common`/`include` sources required
|
|
26
|
+
to compile the extension (see `pq_crypto-seal.gemspec`); unused families and RAF
|
|
27
|
+
remain in the repository for provenance and are not loaded at runtime.
|
|
@@ -3,59 +3,68 @@
|
|
|
3
3
|
module PQCrypto
|
|
4
4
|
module Seal
|
|
5
5
|
module Binary
|
|
6
|
-
|
|
6
|
+
INTEGER_TYPES = {
|
|
7
|
+
u8: [1, "C", 0xff],
|
|
8
|
+
u16: [2, "n", 0xffff],
|
|
9
|
+
u32: [4, "N", 0xffff_ffff],
|
|
10
|
+
u64: [8, "Q>", 0xffff_ffff_ffff_ffff]
|
|
11
|
+
}.freeze
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
end
|
|
13
|
+
class Reader
|
|
14
|
+
attr_reader :offset
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
def initialize(bytes, offset = 0)
|
|
17
|
+
@bytes = String(bytes).b
|
|
18
|
+
@offset = Integer(offset)
|
|
19
|
+
end
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
INTEGER_TYPES.each do |name, (length, directive, _max)|
|
|
22
|
+
define_method(name) { bytes(length).unpack1(directive) }
|
|
23
|
+
end
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
def bytes(length)
|
|
26
|
+
length = Integer(length)
|
|
27
|
+
Binary.ensure_available!(@bytes, offset, length)
|
|
28
|
+
value = @bytes.byteslice(offset, length).b
|
|
29
|
+
@offset += length
|
|
30
|
+
value
|
|
31
|
+
end
|
|
23
32
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
33
|
+
def finished?
|
|
34
|
+
offset == @bytes.bytesize
|
|
35
|
+
end
|
|
27
36
|
end
|
|
28
37
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
class << self
|
|
39
|
+
INTEGER_TYPES.each do |name, (_length, directive, max)|
|
|
40
|
+
define_method(name) { |value| [integer!(value, max)].pack(directive) }
|
|
41
|
+
define_method("read_#{name}") { |bytes, offset| read_with(bytes, offset, &name) }
|
|
42
|
+
end
|
|
33
43
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
end
|
|
44
|
+
def read_bytes(bytes, offset, length)
|
|
45
|
+
read_with(bytes, offset) { |reader| reader.bytes(length) }
|
|
46
|
+
end
|
|
38
47
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
[bytes.byteslice(offset, 8).unpack1("Q>"), offset + 8]
|
|
42
|
-
end
|
|
48
|
+
def ensure_available!(bytes, offset, length)
|
|
49
|
+
offset, length = Integer(offset), Integer(length)
|
|
43
50
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
[bytes.byteslice(offset, length).b, offset + length]
|
|
48
|
-
end
|
|
51
|
+
raise FormatError, "invalid offset" if offset.negative? || length.negative?
|
|
52
|
+
raise FormatError, "truncated envelope" if offset > bytes.bytesize || length > bytes.bytesize - offset
|
|
53
|
+
end
|
|
49
54
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
def integer!(value, max)
|
|
56
|
+
number = Integer(value)
|
|
57
|
+
raise RangeError, "integer is out of range" if number.negative? || number > max
|
|
58
|
+
|
|
59
|
+
number
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
54
63
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
64
|
+
def read_with(bytes, offset)
|
|
65
|
+
reader = Reader.new(bytes, offset)
|
|
66
|
+
[yield(reader), reader.offset]
|
|
67
|
+
end
|
|
59
68
|
end
|
|
60
69
|
end
|
|
61
70
|
end
|