decbackup 0.1.0__tar.gz → 0.2.0__tar.gz
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.
- decbackup-0.2.0/MANIFEST.in +5 -0
- {decbackup-0.1.0 → decbackup-0.2.0}/PKG-INFO +35 -10
- {decbackup-0.1.0 → decbackup-0.2.0}/README.md +29 -9
- decbackup-0.2.0/RECON.md +386 -0
- {decbackup-0.1.0 → decbackup-0.2.0}/decbackup/__init__.py +1 -1
- {decbackup-0.1.0 → decbackup-0.2.0}/decbackup/backup.py +117 -36
- decbackup-0.2.0/decbackup/cli.py +224 -0
- {decbackup-0.1.0 → decbackup-0.2.0}/decbackup/crypto.py +3 -1
- {decbackup-0.1.0 → decbackup-0.2.0}/decbackup/der.py +32 -2
- {decbackup-0.1.0 → decbackup-0.2.0}/decbackup/pkcs7.py +71 -20
- {decbackup-0.1.0 → decbackup-0.2.0}/decbackup/trust.py +95 -23
- {decbackup-0.1.0 → decbackup-0.2.0}/decbackup.egg-info/PKG-INFO +35 -10
- decbackup-0.2.0/decbackup.egg-info/SOURCES.txt +56 -0
- {decbackup-0.1.0 → decbackup-0.2.0}/pyproject.toml +5 -0
- decbackup-0.2.0/tests/test_backup_edges.py +281 -0
- decbackup-0.2.0/tests/test_cli_prompt.py +224 -0
- decbackup-0.2.0/tests/test_decbackup.py +620 -0
- decbackup-0.2.0/tests/test_der_units.py +289 -0
- decbackup-0.2.0/tests/test_trust_chain.py +378 -0
- decbackup-0.2.0/tests/vectors/README.md +107 -0
- decbackup-0.2.0/tests/vectors/algo2.bkup +0 -0
- decbackup-0.2.0/tests/vectors/big.bkup +0 -0
- decbackup-0.2.0/tests/vectors/big.tar +0 -0
- decbackup-0.2.0/tests/vectors/cert.bkup +0 -0
- decbackup-0.2.0/tests/vectors/ct.bkup +0 -0
- decbackup-0.2.0/tests/vectors/extdup.bkup +0 -0
- decbackup-0.2.0/tests/vectors/itovf.bkup +0 -0
- decbackup-0.2.0/tests/vectors/ivlen.bkup +0 -0
- decbackup-0.2.0/tests/vectors/lazyparse.bkup +0 -0
- decbackup-0.2.0/tests/vectors/mid.bkup +0 -0
- decbackup-0.2.0/tests/vectors/mid.tar +0 -0
- decbackup-0.2.0/tests/vectors/mv3.bkup +0 -0
- decbackup-0.2.0/tests/vectors/mvx.bkup +0 -0
- decbackup-0.2.0/tests/vectors/nopass.bkup +0 -0
- decbackup-0.2.0/tests/vectors/sig2bad.bkup +0 -0
- decbackup-0.2.0/tests/vectors/t2.tar +0 -0
- decbackup-0.2.0/tests/vectors/test.bkup +0 -0
- decbackup-0.2.0/tests/vectors/test.tar +0 -0
- decbackup-0.2.0/tests/vectors/trunc.bkup +0 -0
- decbackup-0.2.0/tests/vectors/v1badkey.bkup +0 -0
- decbackup-0.2.0/tests/vectors/v1probe.bkup +0 -0
- decbackup-0.2.0/tests/vectors/v9.bkup +0 -0
- decbackup-0.2.0/tests/vectors/v9s.bkup +0 -0
- decbackup-0.2.0/tests/vectors/xbig.bkup +0 -0
- decbackup-0.2.0/tests/vectors/xzero.bkup +0 -0
- decbackup-0.2.0/tests/vectors/ys.bkup +0 -0
- decbackup-0.2.0/tests/vectors/yt.bkup +0 -0
- decbackup-0.1.0/decbackup/cli.py +0 -110
- decbackup-0.1.0/decbackup.egg-info/SOURCES.txt +0 -22
- decbackup-0.1.0/tests/test_decbackup.py +0 -216
- {decbackup-0.1.0 → decbackup-0.2.0}/LICENSE +0 -0
- {decbackup-0.1.0 → decbackup-0.2.0}/decbackup/__main__.py +0 -0
- {decbackup-0.1.0 → decbackup-0.2.0}/decbackup/certs/netasq-firewall-ca.der +0 -0
- {decbackup-0.1.0 → decbackup-0.2.0}/decbackup/certs/stormshield-product-and-services-root-ca.der +0 -0
- {decbackup-0.1.0 → decbackup-0.2.0}/decbackup/certs/stormshield-products-root-ca.der +0 -0
- {decbackup-0.1.0 → decbackup-0.2.0}/decbackup/certs/stormshield-sns-enrollment-products-ca.der +0 -0
- {decbackup-0.1.0 → decbackup-0.2.0}/decbackup.egg-info/dependency_links.txt +0 -0
- {decbackup-0.1.0 → decbackup-0.2.0}/decbackup.egg-info/entry_points.txt +0 -0
- {decbackup-0.1.0 → decbackup-0.2.0}/decbackup.egg-info/requires.txt +0 -0
- {decbackup-0.1.0 → decbackup-0.2.0}/decbackup.egg-info/top_level.txt +0 -0
- {decbackup-0.1.0 → decbackup-0.2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: decbackup
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Python clone of Stormshield SNS's decbackup: decrypt and verify .bkup appliance backups without the device
|
|
5
5
|
Author: fl0l0u
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -14,6 +14,11 @@ Classifier: Environment :: Console
|
|
|
14
14
|
Classifier: Intended Audience :: System Administrators
|
|
15
15
|
Classifier: Operating System :: OS Independent
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
22
|
Classifier: Topic :: Security :: Cryptography
|
|
18
23
|
Classifier: Topic :: System :: Archiving
|
|
19
24
|
Requires-Python: >=3.9
|
|
@@ -52,6 +57,7 @@ Dynamic: license-file
|
|
|
52
57
|
<a href="#usage">Usage</a> •
|
|
53
58
|
<a href="#the-format">The Format</a> •
|
|
54
59
|
<a href="#development">Development</a> •
|
|
60
|
+
<a href="#releasing">Releasing</a> •
|
|
55
61
|
<a href="#license">License</a>
|
|
56
62
|
</p>
|
|
57
63
|
|
|
@@ -70,10 +76,14 @@ plus an empirical error-code matrix against a live device (see
|
|
|
70
76
|
|---------|-------------|
|
|
71
77
|
| **Byte-exact format support** | `[BACKUP]` header, detached PKCS#7 trailer, AES-128-CBC payload |
|
|
72
78
|
| **Both password schemes** | v2 (PBKDF2-HMAC-SHA256) and legacy v1 (salted-SHA1) key derivation |
|
|
73
|
-
| **Full signature verification** | RSA over PKCS#7 authAttrs, messageDigest over header+payload, chain to the four
|
|
79
|
+
| **Full signature verification** | RSA over PKCS#7 authAttrs, messageDigest over header+payload, chain to the four CA certificates embedded in libncrypto, policy 1.3.6.1.4.1.11256.2.3, dates ignored (as on the device) |
|
|
74
80
|
| **Original CLI parity** | same flags (`-i -o -p -P -d`), same messages, same numeric error codes (0/1/6/7/8/12/13/32) — validated against the real binary on device |
|
|
75
81
|
| **Tiny dependency set** | `cryptography` only; DER/PKCS#7 parsing is in-tree (`decbackup/der.py`, `pkcs7.py`) |
|
|
76
82
|
|
|
83
|
+
About the v1 row: no released SNS version emits the legacy v1 scheme (both
|
|
84
|
+
tested `encbackup` builds write v2), so the v1 path is validated only
|
|
85
|
+
through failure-path vectors, not a decryptable v1 backup.
|
|
86
|
+
|
|
77
87
|
## Installation
|
|
78
88
|
|
|
79
89
|
```bash
|
|
@@ -109,6 +119,10 @@ Success is silent (exit 0), exactly like the original; failure prints
|
|
|
109
119
|
`decrypt and/or verify backup failed (%d)` with the appliance's own error
|
|
110
120
|
code. `-d` prints the decoded header block after a successful run.
|
|
111
121
|
|
|
122
|
+
Output-file contract (same as the device): the `-o` file is created and
|
|
123
|
+
truncated early, and **deleted on every failure path** — a pre-existing
|
|
124
|
+
file at `-o` is destroyed even when the backup is rejected.
|
|
125
|
+
|
|
112
126
|
`--no-verify` has no counterpart on the device: it skips trailer
|
|
113
127
|
parsing and signature/chain checks to rescue backups whose signature
|
|
114
128
|
region is corrupted. It prints a warning and the output is
|
|
@@ -132,10 +146,14 @@ except BackupError as e:
|
|
|
132
146
|
0x000 0x400 [BACKUP] key=value header, NUL-terminated, zero-padded
|
|
133
147
|
0x400 4 be32 X — PKCS#7 trailer length
|
|
134
148
|
0x404 X detached PKCS#7 SignedData (device cert + CA intermediate)
|
|
135
|
-
0x404+X 4 be32 Y —
|
|
136
|
-
0x408+X Y AES-128-CBC ciphertext (PBKDF2-SHA256 key
|
|
149
|
+
0x404+X 4 be32 Y — ciphertext length
|
|
150
|
+
0x408+X Y AES-128-CBC ciphertext (PBKDF2-SHA256 key¹, PKCS#7 padding)
|
|
137
151
|
```
|
|
138
152
|
|
|
153
|
+
¹ v2 key derivation; the legacy v1 scheme instead uses a salted-SHA1
|
|
154
|
+
derivation (see [RECON.md](RECON.md)). Y counts ciphertext bytes including
|
|
155
|
+
the padding, so it exceeds the plaintext by a partial or full AES block.
|
|
156
|
+
|
|
139
157
|
The signature covers the **full 0x400-byte header block plus the decrypted
|
|
140
158
|
payload**; it chains device cert → *Stormshield Network Security Products
|
|
141
159
|
CA* → *Stormshield Products Root CA* (embedded). Expired device
|
|
@@ -148,13 +166,19 @@ readable forever. Full details in [RECON.md](RECON.md).
|
|
|
148
166
|
decbackup/ the package (cli, backup format, crypto, pkcs7, der, trust, certs/)
|
|
149
167
|
analysis/ RE tooling: disassembler, strings, symbol lists, the device
|
|
150
168
|
extraction script and the empirical error-code matrix
|
|
151
|
-
tests/vectors/
|
|
152
|
-
|
|
153
|
-
the stock
|
|
169
|
+
tests/vectors/ device-derived containers + expected payloads:
|
|
170
|
+
test/nopass/big/mid are stock encbackup output from an
|
|
171
|
+
SNS 5.1.4 device (cross-validated against the stock
|
|
172
|
+
decbackup); v1probe/v1badkey are patched v4.2.8
|
|
173
|
+
containers; extdup/lazyparse are pinned fuzz-crash
|
|
174
|
+
mutations; trunc/xzero/ct/cert/mv3/v9/yt/ys/mvx are
|
|
175
|
+
byte-edited probes
|
|
154
176
|
legacy/ the earlier single-file iteration (kept for reference)
|
|
155
177
|
```
|
|
156
178
|
|
|
157
179
|
```bash
|
|
180
|
+
pip install -e ".[dev]" # dev extras: ruff
|
|
181
|
+
ruff check . # lint (same as CI)
|
|
158
182
|
python tests/test_decbackup.py # exit code = number of failures
|
|
159
183
|
python analysis/probes.py # real-vs-clone error-code parity table
|
|
160
184
|
python analysis/fuzz.py 30000 # robustness contract fuzzer
|
|
@@ -163,9 +187,10 @@ python analysis/fuzz.py 30000 # robustness contract fuzzer
|
|
|
163
187
|
## Releasing
|
|
164
188
|
|
|
165
189
|
Tagged pushes (`v*`) build an sdist + wheel, smoke-test the wheel against
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
190
|
+
one bundled vector (`test.bkup`: clean decrypt, plus the wrong-password
|
|
191
|
+
failure contract), and publish to PyPI via trusted publishing
|
|
192
|
+
(see `.github/workflows/release.yml`). Pushes to `master`/`main` and pull
|
|
193
|
+
requests run lint + the error-code parity suite on Python 3.9–3.13
|
|
169
194
|
(`.github/workflows/ci.yml`).
|
|
170
195
|
|
|
171
196
|
```bash
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
<a href="#usage">Usage</a> •
|
|
27
27
|
<a href="#the-format">The Format</a> •
|
|
28
28
|
<a href="#development">Development</a> •
|
|
29
|
+
<a href="#releasing">Releasing</a> •
|
|
29
30
|
<a href="#license">License</a>
|
|
30
31
|
</p>
|
|
31
32
|
|
|
@@ -44,10 +45,14 @@ plus an empirical error-code matrix against a live device (see
|
|
|
44
45
|
|---------|-------------|
|
|
45
46
|
| **Byte-exact format support** | `[BACKUP]` header, detached PKCS#7 trailer, AES-128-CBC payload |
|
|
46
47
|
| **Both password schemes** | v2 (PBKDF2-HMAC-SHA256) and legacy v1 (salted-SHA1) key derivation |
|
|
47
|
-
| **Full signature verification** | RSA over PKCS#7 authAttrs, messageDigest over header+payload, chain to the four
|
|
48
|
+
| **Full signature verification** | RSA over PKCS#7 authAttrs, messageDigest over header+payload, chain to the four CA certificates embedded in libncrypto, policy 1.3.6.1.4.1.11256.2.3, dates ignored (as on the device) |
|
|
48
49
|
| **Original CLI parity** | same flags (`-i -o -p -P -d`), same messages, same numeric error codes (0/1/6/7/8/12/13/32) — validated against the real binary on device |
|
|
49
50
|
| **Tiny dependency set** | `cryptography` only; DER/PKCS#7 parsing is in-tree (`decbackup/der.py`, `pkcs7.py`) |
|
|
50
51
|
|
|
52
|
+
About the v1 row: no released SNS version emits the legacy v1 scheme (both
|
|
53
|
+
tested `encbackup` builds write v2), so the v1 path is validated only
|
|
54
|
+
through failure-path vectors, not a decryptable v1 backup.
|
|
55
|
+
|
|
51
56
|
## Installation
|
|
52
57
|
|
|
53
58
|
```bash
|
|
@@ -83,6 +88,10 @@ Success is silent (exit 0), exactly like the original; failure prints
|
|
|
83
88
|
`decrypt and/or verify backup failed (%d)` with the appliance's own error
|
|
84
89
|
code. `-d` prints the decoded header block after a successful run.
|
|
85
90
|
|
|
91
|
+
Output-file contract (same as the device): the `-o` file is created and
|
|
92
|
+
truncated early, and **deleted on every failure path** — a pre-existing
|
|
93
|
+
file at `-o` is destroyed even when the backup is rejected.
|
|
94
|
+
|
|
86
95
|
`--no-verify` has no counterpart on the device: it skips trailer
|
|
87
96
|
parsing and signature/chain checks to rescue backups whose signature
|
|
88
97
|
region is corrupted. It prints a warning and the output is
|
|
@@ -106,10 +115,14 @@ except BackupError as e:
|
|
|
106
115
|
0x000 0x400 [BACKUP] key=value header, NUL-terminated, zero-padded
|
|
107
116
|
0x400 4 be32 X — PKCS#7 trailer length
|
|
108
117
|
0x404 X detached PKCS#7 SignedData (device cert + CA intermediate)
|
|
109
|
-
0x404+X 4 be32 Y —
|
|
110
|
-
0x408+X Y AES-128-CBC ciphertext (PBKDF2-SHA256 key
|
|
118
|
+
0x404+X 4 be32 Y — ciphertext length
|
|
119
|
+
0x408+X Y AES-128-CBC ciphertext (PBKDF2-SHA256 key¹, PKCS#7 padding)
|
|
111
120
|
```
|
|
112
121
|
|
|
122
|
+
¹ v2 key derivation; the legacy v1 scheme instead uses a salted-SHA1
|
|
123
|
+
derivation (see [RECON.md](RECON.md)). Y counts ciphertext bytes including
|
|
124
|
+
the padding, so it exceeds the plaintext by a partial or full AES block.
|
|
125
|
+
|
|
113
126
|
The signature covers the **full 0x400-byte header block plus the decrypted
|
|
114
127
|
payload**; it chains device cert → *Stormshield Network Security Products
|
|
115
128
|
CA* → *Stormshield Products Root CA* (embedded). Expired device
|
|
@@ -122,13 +135,19 @@ readable forever. Full details in [RECON.md](RECON.md).
|
|
|
122
135
|
decbackup/ the package (cli, backup format, crypto, pkcs7, der, trust, certs/)
|
|
123
136
|
analysis/ RE tooling: disassembler, strings, symbol lists, the device
|
|
124
137
|
extraction script and the empirical error-code matrix
|
|
125
|
-
tests/vectors/
|
|
126
|
-
|
|
127
|
-
the stock
|
|
138
|
+
tests/vectors/ device-derived containers + expected payloads:
|
|
139
|
+
test/nopass/big/mid are stock encbackup output from an
|
|
140
|
+
SNS 5.1.4 device (cross-validated against the stock
|
|
141
|
+
decbackup); v1probe/v1badkey are patched v4.2.8
|
|
142
|
+
containers; extdup/lazyparse are pinned fuzz-crash
|
|
143
|
+
mutations; trunc/xzero/ct/cert/mv3/v9/yt/ys/mvx are
|
|
144
|
+
byte-edited probes
|
|
128
145
|
legacy/ the earlier single-file iteration (kept for reference)
|
|
129
146
|
```
|
|
130
147
|
|
|
131
148
|
```bash
|
|
149
|
+
pip install -e ".[dev]" # dev extras: ruff
|
|
150
|
+
ruff check . # lint (same as CI)
|
|
132
151
|
python tests/test_decbackup.py # exit code = number of failures
|
|
133
152
|
python analysis/probes.py # real-vs-clone error-code parity table
|
|
134
153
|
python analysis/fuzz.py 30000 # robustness contract fuzzer
|
|
@@ -137,9 +156,10 @@ python analysis/fuzz.py 30000 # robustness contract fuzzer
|
|
|
137
156
|
## Releasing
|
|
138
157
|
|
|
139
158
|
Tagged pushes (`v*`) build an sdist + wheel, smoke-test the wheel against
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
159
|
+
one bundled vector (`test.bkup`: clean decrypt, plus the wrong-password
|
|
160
|
+
failure contract), and publish to PyPI via trusted publishing
|
|
161
|
+
(see `.github/workflows/release.yml`). Pushes to `master`/`main` and pull
|
|
162
|
+
requests run lint + the error-code parity suite on Python 3.9–3.13
|
|
143
163
|
(`.github/workflows/ci.yml`).
|
|
144
164
|
|
|
145
165
|
```bash
|
decbackup-0.2.0/RECON.md
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# decbackup / Stormshield SNS backup Reverse-Engineering Recon
|
|
2
|
+
|
|
3
|
+
Target: `/usr/Firewall/sbin/decbackup` + `libncrypto.so` from **Stormshield
|
|
4
|
+
Network Security v5.1.4** (SNS-BSD 5.1.4, EVA1 XL-VM). All addresses below
|
|
5
|
+
are file offsets into the binaries extracted from the device; those
|
|
6
|
+
artifacts are **not in this repo** (the extraction workflow is §1), so
|
|
7
|
+
resolve an address only against your own extracted copy.
|
|
8
|
+
|
|
9
|
+
## 1. Environment & extraction workflow
|
|
10
|
+
|
|
11
|
+
* SNS **v5.1.4** lab VM; admin shell is a **restricted csh whitelist**:
|
|
12
|
+
no `cat`, `od`, `printf`, `for`, no `2>` redirection (csh: `2>&1` =
|
|
13
|
+
"Ambiguous output redirect"). `ls`, `stat`, `sed`, `dd`, `bsdtar`,
|
|
14
|
+
`openssl`, `md5`, `encbackup`, `decbackup` *are* available.
|
|
15
|
+
* SCP/SFTP is blocked. Files exfiltrate via
|
|
16
|
+
`ssh admin@vm 'openssl base64 -in /path/file' | base64 -d`
|
|
17
|
+
(`analysis/extract.sh`). Integrity confirmed with `md5` on both ends.
|
|
18
|
+
* Analysis tooling: `analysis/disasm.py` (capstone + pyelftools function
|
|
19
|
+
disassembler with PLT-resolved call targets and string annotations),
|
|
20
|
+
`analysis/strings.py`, `analysis/symbols.py`.
|
|
21
|
+
|
|
22
|
+
### Binaries that matter
|
|
23
|
+
|
|
24
|
+
| file | role |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `sbin/decbackup` | CLI front-end: getopt, password handling, `-d` dump |
|
|
27
|
+
| `sbin/encbackup` | creates backups (`-i archive -o bkup -t all -p pass`) |
|
|
28
|
+
| `sbin/backuprestore`, `sbin/backupinfo` | other consumers of the format |
|
|
29
|
+
| `lib/libncrypto.so` | the whole format: `fwcrypto_upd_*`, KDF, PKCS#7 |
|
|
30
|
+
| `lib/libnbase.so` | section/key-value parser (`str_get_int_ex` etc.) |
|
|
31
|
+
|
|
32
|
+
## 2. Container format (CONFIRMED, byte-exact)
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
offset size contents
|
|
36
|
+
0x000 0x400 [BACKUP] header: key=value lines, NUL-terminated,
|
|
37
|
+
zero-padded to 1 KiB
|
|
38
|
+
0x400 4 be32 X — length of the PKCS#7 trailer
|
|
39
|
+
0x404 X detached PKCS#7 SignedData (signature + certs)
|
|
40
|
+
0x404+X 4 be32 Y — ciphertext length (== filesize - 0x408 - X)
|
|
41
|
+
0x408+X Y AES-128-CBC ciphertext, PKCS#7 padding
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Y counts the **encrypted** span, PKCS#7 padding included (measured:
|
|
45
|
+
`test.bkup` Y=2064 over a 2048-byte plaintext). The parser does **not
|
|
46
|
+
trust** it — decryption is bounded by filesize (yt/ys probes, §6).
|
|
47
|
+
|
|
48
|
+
Header keys seen: `majversion=2`, `algo=1`, `date="…"`, `model`, `version`,
|
|
49
|
+
`serial`, `advanced`, `description`, `content`, `iv` (hex), `salt` (hex),
|
|
50
|
+
`pbkdf_iterations=10000`, `type=PASSWORD`, `validated`, `IPv6State`.
|
|
51
|
+
Values may be double-quoted; the text block ends at the first NUL.
|
|
52
|
+
|
|
53
|
+
Parsing path: `fwcrypto_upd_update` (libncrypto 0x12860, flags=2 backup
|
|
54
|
+
path). The file is read in **0x4000-byte chunks**; the signature trailer
|
|
55
|
+
must fit inside the first chunk (`X + 0x404 >= chunk_len → error 6`,
|
|
56
|
+
`fwcrypto_upd_get_signature failed`) — a fail-closed design, so whole-file
|
|
57
|
+
processing is behaviourally equivalent.
|
|
58
|
+
|
|
59
|
+
**Quirk (not reproduced):** if the filesize is an exact multiple of
|
|
60
|
+
0x4000, the streaming loop's EOF detection hits the zero-length-read path
|
|
61
|
+
and the tool fails. A bug; the clone simply reads the whole file.
|
|
62
|
+
|
|
63
|
+
## 3. Key derivation (CONFIRMED v2 and v1)
|
|
64
|
+
|
|
65
|
+
Selected by header `majversion` (`fwcrypto_get_backup_majversion`,
|
|
66
|
+
default 1 when the key is absent; 1 or 2 accepted, anything else → error 12
|
|
67
|
+
`Unknown backup majversion`). "Anything else" applies to *parsed* numbers
|
|
68
|
+
only: an unparsable value falls back to the default 1 exactly like an
|
|
69
|
+
absent key (mvx probe, §6).
|
|
70
|
+
|
|
71
|
+
* **v2** (`fwcrypto_derivekey`): `key = PBKDF2-HMAC-SHA256(password,
|
|
72
|
+
unhex(salt), pbkdf_iterations, 16)`; `iv = unhex(iv)`.
|
|
73
|
+
* **v1**: `key = SHA1(2807a5ef00620c4b62f32e0985e30e4e || password)[:16]`,
|
|
74
|
+
`iv = SHA1(463538798d48c8c0323df3e75109d221 || password)[:16]`. The two
|
|
75
|
+
salts are consecutive 16-byte windows of one 48-byte pool
|
|
76
|
+
(`463538798d…09d221` + `2807a5ef00…0e4e`); the getters' `lea` targets
|
|
77
|
+
resolve to pool+0 and pool+16 in *both* the 5.1.4 and 4.2.8 binaries.
|
|
78
|
+
An earlier reading (`212807a5…` / `0046353879…`) was a one-byte
|
|
79
|
+
vaddr→file-offset slip, corrected in §12 by direct experiment.
|
|
80
|
+
For sizes >20 the fill is
|
|
81
|
+
`buf[i] = (size-1 + (size-i)*0xEB) & 0xFF` — unreachable for AES-128.
|
|
82
|
+
5.1.4 `encbackup` cannot emit v1 (no version flag in getopt
|
|
83
|
+
`i:o:p:c:t:n:P`), but the **key derivation is now empirically confirmed**
|
|
84
|
+
(§12): a v1-header archive whose ciphertext was made with these
|
|
85
|
+
constants decrypts cleanly on both VMs (reaches the digest check → 7),
|
|
86
|
+
while the shifted-salt variant fails at padding (→ 13) on both.
|
|
87
|
+
|
|
88
|
+
Cipher: `algo=1` → AES-128-CBC (`EVP_aes_128_cbc`); anything else → error
|
|
89
|
+
32 `unsupported algo`.
|
|
90
|
+
|
|
91
|
+
## 4. Signature scheme (CONFIRMED end-to-end)
|
|
92
|
+
|
|
93
|
+
Detached PKCS#7 SignedData (OpenSSL 3 `PKCS7_*` via
|
|
94
|
+
`VerifyMajSignatureInit` 0x14a5? / `VerifySignatureFinal`; last nibble
|
|
95
|
+
unresolved from notes):
|
|
96
|
+
|
|
97
|
+
* **Signed content** = full 0x400-byte header block (text + NUL padding —
|
|
98
|
+
implemented as `strlen` + zero-pad loop) **followed by the decrypted
|
|
99
|
+
payload**. Proven: `messageDigest` attribute ==
|
|
100
|
+
`sha256(header_block + plaintext)` on every vector.
|
|
101
|
+
* The trailer's SignedData quirks:
|
|
102
|
+
* certificates sit under `[0]` as bare `SEQUENCE` TLVs (no SET wrapper);
|
|
103
|
+
* `signerInfos` is a plain `0x31` SET;
|
|
104
|
+
* authenticatedAttributes are stored bare under the `[0]` tag — the
|
|
105
|
+
signed DER is the tag-swapped form `0x31 + DER-length + value`
|
|
106
|
+
(Stormshield's encoder omits the SET, so the length must be re-encoded;
|
|
107
|
+
for the 105-byte value the short form `0x69` matches byte-exactly);
|
|
108
|
+
* signature AlgorithmIdentifier is `rsaEncryption` (1.2.840.113549.1.1.1,
|
|
109
|
+
hash from digestAlgorithm = sha256) — classic PKCS#7 DigestInfo.
|
|
110
|
+
* Attributes: contentType (1.9.3) = pkcs7-data, signingTime (1.9.5),
|
|
111
|
+
messageDigest (1.9.4).
|
|
112
|
+
* Signer: device certificate (leaf carries certificatePolicies
|
|
113
|
+
**1.3.6.1.4.1.11256.2.3**), SID = issuerAndSerialNumber.
|
|
114
|
+
|
|
115
|
+
### Trust store (`fw_libcrypto_get_trust_store`, backup type)
|
|
116
|
+
|
|
117
|
+
Four CA certs hard-coded in libncrypto .rodata, extracted to
|
|
118
|
+
`decbackup/certs/`:
|
|
119
|
+
|
|
120
|
+
| .rodata off | size | subject |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| 0x8890 | 0x6b0 | Stormshield Network Security Enrollment Products CA |
|
|
123
|
+
| 0x6b40 | 0x418 | NETASQ Firewall Certification Authority |
|
|
124
|
+
| 0x8f40 | 0x601 | Stormshield Products Root CA |
|
|
125
|
+
| 0x6f60 | 0x606 | Stormshield Product and Services Root CA |
|
|
126
|
+
|
|
127
|
+
Verify parameters: `X509_V_FLAG_NO_CHECK_TIME` (expired device certs still
|
|
128
|
+
verify — backups stay readable forever), explicit policy
|
|
129
|
+
1.3.6.1.4.1.11256.2.3. The callback `fwcrypto_verify_any_nodate_callback`
|
|
130
|
+
(0x11150) accepts errors {0, 9, 10} and 43 when
|
|
131
|
+
`fw_libcrypto_is_netasq_issued` (issuer DER memcmp vs the NETASQ root).
|
|
132
|
+
Net effect: signature-valid chain from leaf to an embedded root, dates
|
|
133
|
+
ignored. Chain in vectors: device ← Stormshield Network Security Products
|
|
134
|
+
CA (in the trailer) ← Stormshield Products Root CA (embedded).
|
|
135
|
+
|
|
136
|
+
## 5. CLI grammar (`decbackup` main)
|
|
137
|
+
|
|
138
|
+
getopt string `Pi:p:o:d`; usage (stderr, exit 1 when missing args/options):
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
Usage : () -i <backup> -o <output archive>
|
|
142
|
+
[-p <password>] [-d : dump backup header]
|
|
143
|
+
[-P : prompt for password]
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
(The empty parens are faithful: both `%s` args point at an empty string.)
|
|
147
|
+
|
|
148
|
+
* `-p` more than once → `-p can only be specified once`, exit 1.
|
|
149
|
+
* Password selection: `-p x` → x; `-P` → `t_getpass` prompt
|
|
150
|
+
`Enter backup password (<Enter> for none): ` which reads **/dev/tty**
|
|
151
|
+
(piped stdin is ignored: `-P` over a pipe with no tty → empty password →
|
|
152
|
+
13); no `-p`/`-P` on a tty → same prompt; otherwise empty password.
|
|
153
|
+
* `-d` → on success prints `Header :\n<text up to NUL>\n`. Success is
|
|
154
|
+
otherwise silent (exit 0); failure prints
|
|
155
|
+
`decrypt and/or verify backup failed (%d)` (stderr) and exits 1.
|
|
156
|
+
* Output file is created early (`fopen wb`) and **removed on every error
|
|
157
|
+
path**.
|
|
158
|
+
* Clone extension: `--no-verify` (long option only — the original
|
|
159
|
+
getopt string `Pi:p:o:d` is unchanged and cannot collide with it).
|
|
160
|
+
It skips trailer parsing and all verification; the X field is still
|
|
161
|
+
read to locate the payload, so the structural floor (8) and padding
|
|
162
|
+
errors (13) remain. A corrupted-but-in-range X can therefore yield
|
|
163
|
+
exit 0 with garbage content — that is the point of the switch.
|
|
164
|
+
A warning goes to stderr and exit 0 then means
|
|
165
|
+
"decrypted, not authenticated".
|
|
166
|
+
|
|
167
|
+
## 6. Error codes (empirical matrix, real device vs clone — parity)
|
|
168
|
+
|
|
169
|
+
`analysis/probes.py` renders this matrix as a static table, run locally
|
|
170
|
+
against `tests/vectors/` (no device needed; the `real` column holds codes
|
|
171
|
+
recorded on the device). The **maintained** parity check is
|
|
172
|
+
`tests/test_decbackup.py` (run in CI); keep the two in sync.
|
|
173
|
+
|
|
174
|
+
| code | meaning | proven by |
|
|
175
|
+
|---|---|---|
|
|
176
|
+
| 0 | success | all vectors |
|
|
177
|
+
| 1 | input/output open failure | code path |
|
|
178
|
+
| 6 | signature trailer unparsable / doesn't fit first chunk; crypto params missing | X=0 → 6; sed-shifted files → 6 |
|
|
179
|
+
| 7 | verification failed (content digest, attribute signature, chain) | 1-byte ciphertext tamper → 7; cert bytes zeroed → 7; header edit → 7 |
|
|
180
|
+
| 8 | file shorter than 0x400 | trunc.bkup → 8 |
|
|
181
|
+
| 11 | short write to the output file (residual default err; seen when `-o /dev/stdout` truncates the stream) — **not reproduced in clone**: v4.2.8 short-write artifact with no clone code path | 4.2.8 binary, v1 probe to /dev/stdout |
|
|
182
|
+
| 12 | unknown majversion (`Unknown backup majversion: %d`) or version gate (`Invalid archive`) | majversion=3 → 12; version=9 → 12 |
|
|
183
|
+
| 13 | decrypt failed — in practice wrong password (EVP_CipherFinal padding) | wrongpass → 13 |
|
|
184
|
+
| 32 | unsupported algo | algo=2 → 32 |
|
|
185
|
+
|
|
186
|
+
**Verification is enforced**: an early "tampering is accepted" conclusion
|
|
187
|
+
was an artifact of csh mangling `dd … 2>/dev/null` (all those tamper
|
|
188
|
+
commands were silent no-ops). With surgical `dd conv=notrunc` patches the
|
|
189
|
+
tool fails closed on every corrupted byte in the ciphertext, signature,
|
|
190
|
+
attributes, or certificate regions.
|
|
191
|
+
|
|
192
|
+
### Boundary probes (device, all derived from `test.bkup`)
|
|
193
|
+
|
|
194
|
+
| probe | edit | code | conclusion |
|
|
195
|
+
|---|---|---|---|
|
|
196
|
+
| yt.bkup | +16 zero bytes appended | 13 | decryption is bounded by **filesize**, not the be32 Y field — Y is informational; trailing garbage lands in the last CBC block → padding fails |
|
|
197
|
+
| ys.bkup | last 16 bytes removed | 13 | a file shorter than Y simply decrypts short (no separate Y error) |
|
|
198
|
+
| mvx.bkup | `majversion=x` (same length) | 13 | non-numeric majversion → parse fails → **defaults to 1** (v1 path) like an absent key; "anything else → 12" applies to parsed numbers only |
|
|
199
|
+
|
|
200
|
+
### CLI probes (device)
|
|
201
|
+
|
|
202
|
+
| invocation | stderr |
|
|
203
|
+
|---|---|
|
|
204
|
+
| `decbackup -z` | `decbackup: illegal option -- z` + usage |
|
|
205
|
+
| `-p a -p b …` | `-p can only be specified once` — **no usage dump** |
|
|
206
|
+
| missing `-i`/`-o` | usage only |
|
|
207
|
+
|
|
208
|
+
### The version gate (`fwcrypto_maj_get_version`, 0x11820)
|
|
209
|
+
|
|
210
|
+
`cmp ax,1 → error 12 ("Invalid archive")` at 0x13194 on the flags&2 backup
|
|
211
|
+
path. Observations (all same-length edits, X intact unless noted):
|
|
212
|
+
|
|
213
|
+
| header `version` | real code | interpretation |
|
|
214
|
+
|---|---|---|
|
|
215
|
+
| `5.1.4` (real) | pass | parse fails → default 1 |
|
|
216
|
+
| `1` | pass | parses to 1 |
|
|
217
|
+
| `9` (−4 shift) | 12 | parses to 9 → gate **before** the X check |
|
|
218
|
+
| `9 ` (padded) | 7 | trailing spaces → parse fails → default |
|
|
219
|
+
| `10000`/`90000` | 7 | 5-digit values → parse fails → default |
|
|
220
|
+
| `abcde`, `2.0.0` | 7 | parse fails → default |
|
|
221
|
+
|
|
222
|
+
Empirical rule implemented: value **parses** only when it is leading
|
|
223
|
+
whitespace + 1–4 digits + end (strict, no trailing chars); a parsed value
|
|
224
|
+
≠ 1 → error 12. The digit-count cap (why 5-digit strings fail) is inferred,
|
|
225
|
+
not read off `str_get_int_ex` conclusively — the 4-vs-3 boundary is
|
|
226
|
+
untested. Real backups always carry dotted versions → always pass.
|
|
227
|
+
|
|
228
|
+
## 7. Methodology gotchas (for future probes)
|
|
229
|
+
|
|
230
|
+
1. **csh eats `2>/dev/null`** — `dd`/redirects with `2>` become silent
|
|
231
|
+
no-ops ("Unknown operand"). Tamper commands *must* omit it.
|
|
232
|
+
2. **sed edits change file length** → everything after the header shifts →
|
|
233
|
+
the X field at 0x400 reads garbage → you measure the X check, not the
|
|
234
|
+
thing you probed. Only same-length substitutions, or patch with
|
|
235
|
+
`dd conv=notrunc`.
|
|
236
|
+
3. `cmp` is not whitelisted; csh reports command-not-found as exit 1,
|
|
237
|
+
which looks exactly like "files differ". Use `openssl dgst -sha256`.
|
|
238
|
+
4. Probe files: byte-identical local/VM copies verified by sha256
|
|
239
|
+
computed manually on both sides at extraction time (digests are not
|
|
240
|
+
stored in the repo), so device-observed codes apply to the vectors.
|
|
241
|
+
|
|
242
|
+
## 8. Robustness contract (fuzzing)
|
|
243
|
+
|
|
244
|
+
`analysis/fuzz.py` mutation-fuzzes container bytes and the PKCS#7/DER
|
|
245
|
+
parsers against the contract: **any** input must end in a `BackupError`
|
|
246
|
+
with a documented code, never a traceback, never a surviving output
|
|
247
|
+
file, never a hang (5 s guard). Seeds are the real vectors plus every
|
|
248
|
+
input that ever violated the contract (`$TMPDIR/opencode/fuzz_failures/`,
|
|
249
|
+
in practice `/tmp/opencode/fuzz_failures/` — a machine-local, volatile
|
|
250
|
+
scratch dir, not versioned; the pinned regression vectors are
|
|
251
|
+
`extdup.bkup` + `lazyparse.bkup`); mutations cover bit flips, splices,
|
|
252
|
+
truncations, X/Y field corruption and random files with plausible headers.
|
|
253
|
+
|
|
254
|
+
The first campaign (60k iterations, 2 seeds) surfaced one systemic
|
|
255
|
+
class: `cryptography` parses certificate fields **lazily**, so a
|
|
256
|
+
structurally-valid-but-tampered cert loads fine and then explodes on
|
|
257
|
+
`.subject` / `.public_key()` / `.extensions` — including
|
|
258
|
+
`DuplicateExtension`, which derives from bare `Exception`. Both verify
|
|
259
|
+
boundaries (`SignerInfo.verify`, `trust.verify_chain`) now convert any
|
|
260
|
+
unexpected parser exception into the fail-closed error 7. Two crash
|
|
261
|
+
inputs are pinned as vectors (`extdup.bkup`, `lazyparse.bkup`).
|
|
262
|
+
Re-runs: 90k iterations / 3 seeds clean; re-run before every release.
|
|
263
|
+
|
|
264
|
+
## 9. Clone deviations from the device (security hardening)
|
|
265
|
+
|
|
266
|
+
Deliberate behavior changes where the clone is *not* device-faithful:
|
|
267
|
+
|
|
268
|
+
* **Chain terminators**: a self-signed chain end must match an embedded
|
|
269
|
+
CA certificate **byte-exactly**. The device binds through an OpenSSL
|
|
270
|
+
store lookup to the genuine cert; the clone's earlier subject+serial
|
|
271
|
+
match was exploitable (forged chains accepted).
|
|
272
|
+
* **Chain signature algs**: explicit allow-list — unknown OIDs rejected,
|
|
273
|
+
`…1.1.5` → SHA-1, MD5 not accepted.
|
|
274
|
+
* **certificatePolicies**: an extension that fails to parse → reject
|
|
275
|
+
(previously fail-open).
|
|
276
|
+
* **Issuers**: intermediate/root issuers must carry `CA:TRUE` (matches
|
|
277
|
+
the device's INVALID_CA).
|
|
278
|
+
* **signerInfos**: **all** signerInfos are verified (previously only the
|
|
279
|
+
first).
|
|
280
|
+
* **Structure**: duplicate or missing `contentType` authAttrs, unknown
|
|
281
|
+
PKCS#7 signature algs, and malformed DER children (child extending
|
|
282
|
+
beyond its parent, oversized OID components) → reject.
|
|
283
|
+
* **pbkdf_iterations**: ≥ 2^31 → error 6; ASCII digits only. Device
|
|
284
|
+
analog: OpenSSL "iteration value is too great" (exact code unprobed).
|
|
285
|
+
* **iv**: wrong-length iv / whitespace-laced hex → error 6 at parse time
|
|
286
|
+
(inferred).
|
|
287
|
+
* **`-P`**: now actually prompts without echo on a tty (parity intent);
|
|
288
|
+
pipe-without-tty → empty password is unchanged (device-probed 13).
|
|
289
|
+
* **Passwords**: non-UTF-8 passwords remain a clone limitation (the
|
|
290
|
+
device uses raw argv bytes).
|
|
291
|
+
|
|
292
|
+
## 10. Open items / uncertainties
|
|
293
|
+
|
|
294
|
+
* v1 KDF **empirically confirmed** on both VMs (§12). The IV window is
|
|
295
|
+
static-confirmed (identical `lea`-pool layout in both binaries) but
|
|
296
|
+
RC-invisible in probes (CBC padding ignores the IV and the digest can
|
|
297
|
+
never match a copied trailer); it is the pool[0:16] window.
|
|
298
|
+
* Exact digit-count cap of the version gate parser (implemented ≤4).
|
|
299
|
+
* Error code when the *output* file can't be created: assumed 1.
|
|
300
|
+
* `pbkdf_iterations` default when the key is missing: assumed 1
|
|
301
|
+
(`mov esi, 1` near the getter); real files always carry the key.
|
|
302
|
+
* A crafted `pbkdf_iterations` of ~10^9 stalls verification for minutes
|
|
303
|
+
on the clone AND on the device (the value is trusted straight from the
|
|
304
|
+
header) — DoS-by-input is a shared property, not a clone deviation.
|
|
305
|
+
|
|
306
|
+
## 11. Test vectors (`tests/vectors/`)
|
|
307
|
+
|
|
308
|
+
Made with `encbackup -i <tar> -o <bkup> -t all -p <pass>` on the device,
|
|
309
|
+
fetched by base64-over-ssh, and cross-validated against the real
|
|
310
|
+
`decbackup` output. Error vectors are byte-identical to the device-side
|
|
311
|
+
probe files — verified by comparing digests manually on both sides at
|
|
312
|
+
extraction time; the digests themselves were never recorded in the repo.
|
|
313
|
+
|
|
314
|
+
Privacy note: the vectors are real device output and cannot be edited
|
|
315
|
+
without breaking the signature they exercise, so they carry the lab
|
|
316
|
+
device's certificate (public by design) and the default evaluation-VM
|
|
317
|
+
serial; the payload tars are synthetic and the passwords are the test
|
|
318
|
+
values shown below. No private key material is present in any vector.
|
|
319
|
+
|
|
320
|
+
| vector | payload | password | code |
|
|
321
|
+
|---|---|---|---|
|
|
322
|
+
| test.bkup | test.tar (2 KiB) | `testpass` | 0 |
|
|
323
|
+
| nopass.bkup | t2.tar | (empty) | 0 |
|
|
324
|
+
| big.bkup | big.tar | `BigPass789` | 0 |
|
|
325
|
+
| mid.bkup | mid.tar (23 KiB, multi-chunk) | `MidPass321` | 0 |
|
|
326
|
+
| trunc/xzero/ct/cert/mv3/v9 | — | `testpass` | 8/6/7/7/12/7 |
|
|
327
|
+
| v1probe.bkup / v1badkey.bkup | test.tar (§12, v1-encrypted) | `testpass` | 7 / 13 |
|
|
328
|
+
|
|
329
|
+
## 12. Second VM: SNS v4.2.8
|
|
330
|
+
|
|
331
|
+
Older box, same EVA1-VM hardware line, the same default evaluation-VM
|
|
332
|
+
serial as the 5.1.4 box, clock
|
|
333
|
+
pinned to 2020. All four binaries differ from 5.1.4
|
|
334
|
+
(`decbackup fca8df91…`, `libncrypto.so ddca97b1…`). Its `decbackup` usage
|
|
335
|
+
string lacks the `-P` line; `encbackup` still writes **majversion=2**
|
|
336
|
+
(PBKDF2, 10000 iterations) with the identical container layout — so no
|
|
337
|
+
release in this VM line emits v1.
|
|
338
|
+
|
|
339
|
+
* **Cross-version compatibility (CONFIRMED)**: the clone decrypts a
|
|
340
|
+
v4.2.8-produced backup byte-exactly, and the v4.2.8 device `decbackup`
|
|
341
|
+
reproduces the same sha256. Same PKI chain as 5.1.4
|
|
342
|
+
(device ← Products CA ← Products Root CA).
|
|
343
|
+
* **v1 path present and identical in structure**: `upd_update` (flags&2)
|
|
344
|
+
→ `fwcrypto_get_backup_majversion` → `==1` branch calls
|
|
345
|
+
`fwcrypto_keyfrompasswd(pass, 0x10)` + `fwcrypto_ivfrompasswd(pass, 0x10)`
|
|
346
|
+
(0xd692/0xd6ac), then the same streaming decrypt + `VerifySignatureFinal`
|
|
347
|
+
tail — v1 archives are signed containers too, and VSI failure is still
|
|
348
|
+
fail-closed (error 6). The version gate (`cmp ax,1` → 12) is the same.
|
|
349
|
+
* **v1 salt windows**: both binaries read the *same* windows of the shared
|
|
350
|
+
48-byte pool (key = pool+16, iv = pool+0). Resolved by `lea`
|
|
351
|
+
instruction-by-instruction in both (`sns` 0x16d8d→file 0x1b420,
|
|
352
|
+
0x16ac4→0x1b410; `sns428` 0x10d98→0x18030, 0x10aaf→0x18020), and
|
|
353
|
+
confirmed empirically with the probe matrix below.
|
|
354
|
+
* **v1 KDF probe matrix** (v1 header via same-length `majversion=2→1`
|
|
355
|
+
edit of a real v4.2.8 backup, copied valid trailer, content re-encrypted
|
|
356
|
+
under each salt×iv combination; padding validity is the key oracle, CBC
|
|
357
|
+
IV cannot affect it):
|
|
358
|
+
|
|
359
|
+
| ciphertext made with | old-VM (4.2.8) | 5.1.4 VM |
|
|
360
|
+
|---|---|---|
|
|
361
|
+
| pool+16 key (correct) | 7 (both IVs) | 7 |
|
|
362
|
+
| shifted key `212807a5…` | 13 | 13 |
|
|
363
|
+
|
|
364
|
+
`7` = decryption succeeded, digest check failed (expected: the trailer's
|
|
365
|
+
messageDigest covers the original v2 content). This proves the key
|
|
366
|
+
window on both binaries; the IV window follows from the identical
|
|
367
|
+
`lea`-pool layout (RC-invisible by construction).
|
|
368
|
+
* **4.2.8 embedded trust blobs** (libncrypto .data, six certs): the two
|
|
369
|
+
5.1.4 roots + Enrollment Products CA, **no NETASQ root**, plus three
|
|
370
|
+
service certs for the update path (`firmware.sns.local`,
|
|
371
|
+
`update.sns.local`, `signvmkit.sns.local`, issued by "Stormshield
|
|
372
|
+
Network Security CA"). The NETASQ-issuer special case in the verify
|
|
373
|
+
callback therefore postdates 4.2.8. 4.2.8 also exposes older API
|
|
374
|
+
surface (`fwcrypto_dec_maj_file`, `fwcrypto_check_maj`,
|
|
375
|
+
`fwcrypto_backup_read_algo`, 3DES/Blowfish/DES helpers).
|
|
376
|
+
* **Error 11** appears on the old binary: the stream loop sets
|
|
377
|
+
`[err]=11` as the default before each output `fwrite` and a failing
|
|
378
|
+
`EVP_CipherFinal`/short-write path leaves it in place. Observed when
|
|
379
|
+
pointing `-o` at `/dev/stdout` (pipe semantics + `remove()` of the
|
|
380
|
+
symlink); a regular output file behaves as on 5.1.4.
|
|
381
|
+
* **Ingestion channel** (SCP blocked, echo-chunks proved lossy — csh
|
|
382
|
+
`echo` corrupted some base64 payloads at identical length): pipe bytes
|
|
383
|
+
straight through ssh stdin into `dd of=/tmp/x bs=<len> count=1`, then
|
|
384
|
+
`openssl base64 -A -d -in /tmp/x -out /tmp/y`. The `-A` flag is
|
|
385
|
+
required on this box's OpenSSL 1.1.1k for single-line payloads;
|
|
386
|
+
verify with `md5` on both ends.
|