capsium 0.2.0 → 0.3.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 +4 -4
- data/.rubocop.yml +12 -0
- data/CHANGELOG.md +125 -0
- data/README.adoc +332 -5
- data/capsium.gemspec +1 -0
- data/lib/capsium/cli/formatting.rb +27 -0
- data/lib/capsium/cli/package.rb +68 -6
- data/lib/capsium/cli/reactor.rb +9 -1
- data/lib/capsium/cli.rb +1 -0
- data/lib/capsium/package/authentication.rb +38 -0
- data/lib/capsium/package/authentication_config.rb +78 -0
- data/lib/capsium/package/cipher.rb +197 -0
- data/lib/capsium/package/composition.rb +77 -0
- data/lib/capsium/package/dependency_resolver.rb +61 -0
- data/lib/capsium/package/encryption_config.rb +38 -0
- data/lib/capsium/package/merged_view.rb +168 -0
- data/lib/capsium/package/preparation.rb +81 -0
- data/lib/capsium/package/routes_config.rb +60 -6
- data/lib/capsium/package/security.rb +21 -4
- data/lib/capsium/package/signer.rb +201 -0
- data/lib/capsium/package/storage_config.rb +29 -1
- data/lib/capsium/package/store.rb +109 -0
- data/lib/capsium/package/testing/config_test.rb +70 -0
- data/lib/capsium/package/testing/context.rb +11 -0
- data/lib/capsium/package/testing/data_validation_test.rb +63 -0
- data/lib/capsium/package/testing/file_test.rb +45 -0
- data/lib/capsium/package/testing/report.rb +33 -0
- data/lib/capsium/package/testing/route_test.rb +64 -0
- data/lib/capsium/package/testing/test_case.rb +60 -0
- data/lib/capsium/package/testing/test_suite.rb +86 -0
- data/lib/capsium/package/testing.rb +20 -0
- data/lib/capsium/package/validator.rb +14 -3
- data/lib/capsium/package/verification.rb +32 -0
- data/lib/capsium/package/version.rb +204 -0
- data/lib/capsium/package.rb +59 -75
- data/lib/capsium/packager.rb +34 -1
- data/lib/capsium/reactor/authenticator.rb +180 -0
- data/lib/capsium/reactor/deploy.rb +71 -0
- data/lib/capsium/reactor/htpasswd.rb +154 -0
- data/lib/capsium/reactor/introspection.rb +4 -1
- data/lib/capsium/reactor/oauth2.rb +106 -0
- data/lib/capsium/reactor/serving.rb +75 -0
- data/lib/capsium/reactor/session.rb +82 -0
- data/lib/capsium/reactor.rb +43 -41
- data/lib/capsium/thor_ext.rb +1 -1
- data/lib/capsium/version.rb +1 -1
- data/sig/capsium/package/authentication.rbs +23 -0
- data/sig/capsium/package/authentication_config.rbs +71 -0
- data/sig/capsium/package/cipher.rbs +51 -0
- data/sig/capsium/package/composition.rbs +20 -0
- data/sig/capsium/package/dependency_resolver.rbs +49 -0
- data/sig/capsium/package/encryption_config.rbs +35 -0
- data/sig/capsium/package/merged_view.rbs +46 -0
- data/sig/capsium/package/preparation.rbs +23 -0
- data/sig/capsium/package/routes_config.rbs +36 -3
- data/sig/capsium/package/security.rbs +9 -2
- data/sig/capsium/package/signer.rbs +59 -0
- data/sig/capsium/package/storage_config.rbs +26 -2
- data/sig/capsium/package/store.rbs +37 -0
- data/sig/capsium/package/testing/config_test.rbs +21 -0
- data/sig/capsium/package/testing/context.rbs +14 -0
- data/sig/capsium/package/testing/data_validation_test.rbs +22 -0
- data/sig/capsium/package/testing/file_test.rbs +17 -0
- data/sig/capsium/package/testing/report.rbs +20 -0
- data/sig/capsium/package/testing/route_test.rbs +22 -0
- data/sig/capsium/package/testing/test_case.rbs +41 -0
- data/sig/capsium/package/testing/test_suite.rbs +20 -0
- data/sig/capsium/package/testing.rbs +7 -0
- data/sig/capsium/package/verification.rbs +24 -0
- data/sig/capsium/package/version.rbs +39 -0
- data/sig/capsium/package.rbs +25 -17
- data/sig/capsium/packager.rbs +9 -0
- data/sig/capsium/reactor/authenticator.rbs +40 -0
- data/sig/capsium/reactor/deploy.rbs +34 -0
- data/sig/capsium/reactor/htpasswd.rbs +36 -0
- data/sig/capsium/reactor/oauth2.rbs +31 -0
- data/sig/capsium/reactor/serving.rbs +22 -0
- data/sig/capsium/reactor/session.rbs +36 -0
- data/sig/capsium/reactor.rbs +6 -1
- metadata +70 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 188f04acf4c1f13b7a4515798e1183f59473ffba9200d375ea31b7ec2ceb705f
|
|
4
|
+
data.tar.gz: '068cb0fbbfa415b6f07bb299e233c2f27f5161fe82b932c1379883fcba79a36f'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ed6d82167bc38448608ced953d1c906980e2fbbbc1297e1c79debe3aaf67508a9aa14237741c42be113b4daffe4e8c8d3e7a8c710f899acebeece1ce9b4dfde8
|
|
7
|
+
data.tar.gz: c1294b0ec91effddc6cf7da04cb4c128db2a31f2f82625b2a12b92c303be6bcfc566f4bf4a8c6658a187f623e8ba433cab24d275a0d880b82c3717775aec3aa5
|
data/.rubocop.yml
CHANGED
|
@@ -26,6 +26,18 @@ Metrics/BlockLength:
|
|
|
26
26
|
- 'spec/**/*'
|
|
27
27
|
- 'capsium.gemspec'
|
|
28
28
|
|
|
29
|
+
# Spec helpers take the parameters the fixtures need.
|
|
30
|
+
Metrics/ModuleLength:
|
|
31
|
+
Exclude:
|
|
32
|
+
- 'spec/**/*'
|
|
33
|
+
|
|
34
|
+
# Spec helpers take the parameters the fixtures need; the reactor
|
|
35
|
+
# constructor takes one keyword per deploy-time concern.
|
|
36
|
+
Metrics/ParameterLists:
|
|
37
|
+
Max: 6
|
|
38
|
+
Exclude:
|
|
39
|
+
- 'spec/**/*'
|
|
40
|
+
|
|
29
41
|
Metrics/ClassLength:
|
|
30
42
|
Max: 120
|
|
31
43
|
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,130 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Digital signatures (RSA-SHA256, X.509) per the packaging and security
|
|
8
|
+
standard clauses: `Capsium::Package::Signer` signs a package directory in
|
|
9
|
+
place (`signature.sig` plus the embedded public key PEM, recorded as
|
|
10
|
+
`security.digitalSignatures {publicKey, signatureFile}` in
|
|
11
|
+
`security.json`) and verifies declared signatures. The signed payload is
|
|
12
|
+
the concatenation, in sorted package-relative path order, of the raw
|
|
13
|
+
bytes of every checksum-covered file (everything except `security.json`
|
|
14
|
+
and `signature.sig`) — verifiable independently with
|
|
15
|
+
`openssl dgst -sha256 -verify`.
|
|
16
|
+
- Signed packages are verified automatically on load:
|
|
17
|
+
`Capsium::Package#verify_signature`/`#verify_signature!`/`#signed?`;
|
|
18
|
+
a mismatch raises `Capsium::Package::Signer::SignatureMismatchError`,
|
|
19
|
+
so the reactor refuses to serve tampered signed packages.
|
|
20
|
+
- `capsium package sign PATH --key KEY.pem [--cert CERT.pem]` and
|
|
21
|
+
`capsium package verify-signature PATH [--cert CERT.pem]` (exit status 1
|
|
22
|
+
on unsigned packages and verification failures). Both accept a package
|
|
23
|
+
directory or a `.cap` file.
|
|
24
|
+
- Whole-package encryption (AES-256-GCM, RSA-OAEP-SHA256 wrapped DEK) per
|
|
25
|
+
the packaging standard's encryption clause: `Capsium::Package::Cipher`
|
|
26
|
+
encrypts a package directory or `.cap` into the standard encrypted
|
|
27
|
+
layout (`metadata.json` cleartext, `signature.json` encryption envelope,
|
|
28
|
+
`package.enc` AES-256-GCM of the inner `.cap` zip) and decrypts it back.
|
|
29
|
+
The OCB/OpenPGP alternatives mentioned by the standard are intentionally
|
|
30
|
+
not implemented.
|
|
31
|
+
- `capsium package encrypt PATH --public-key PUB.pem -o OUT.cap` and
|
|
32
|
+
`capsium package decrypt PATH --private-key PRIV.pem [-o OUT.cap]`.
|
|
33
|
+
- `Capsium::Package.new(path, decryption_key:)` transparently decrypts an
|
|
34
|
+
encrypted `.cap` (or uncompressed encrypted directory) on load; without
|
|
35
|
+
a key it raises `Capsium::Package::Cipher::KeyRequiredError` (the
|
|
36
|
+
reactor refuses to serve), and a wrong key or tampered ciphertext raises
|
|
37
|
+
`Capsium::Package::Cipher::DecryptionError` (GCM authentication).
|
|
38
|
+
- `Capsium::Packager#transform_cap`/`#with_unpacked_cap` helpers for
|
|
39
|
+
in-place `.cap` transformations.
|
|
40
|
+
- The Capsium package testing YAML DSL (05x-testing): packages declare
|
|
41
|
+
tests in `tests/*.yaml` files (top-level `tests` list), and
|
|
42
|
+
`capsium package test PATH` runs them with per-test `PASS`/`FAIL`
|
|
43
|
+
output, a summary line and proper exit codes. Supported test types:
|
|
44
|
+
`route` (expected status, optional `response_contains` and
|
|
45
|
+
`expected_content_type` against a reactor started for the run),
|
|
46
|
+
`file` (existence, optional `contains`), `data_validation` (dataset
|
|
47
|
+
rows validated against a JSON schema, JSON and YAML formats) and
|
|
48
|
+
`config` (configuration files parsed and the known package configs
|
|
49
|
+
validated against their canonical models). Implemented as an
|
|
50
|
+
open/closed registry of `Capsium::Package::Testing::TestCase`
|
|
51
|
+
subclasses run by `Capsium::Package::Testing::TestSuite`; invalid
|
|
52
|
+
test definitions are reported as failures instead of aborting the run.
|
|
53
|
+
- Layered storage (ARCHITECTURE.md section 5a): `storage.layers` stacks
|
|
54
|
+
overlay directories over `content/` (bottom -> top in declaration
|
|
55
|
+
order), each mirroring the `content/` tree.
|
|
56
|
+
`Capsium::Package::MergedView` — the merged-view home shared by the
|
|
57
|
+
package (validation) and the reactor (serving) — resolves content
|
|
58
|
+
top-first, honors `.capsium-tombstones` deletions (a tombstoned path
|
|
59
|
+
resolves 404 even when a lower layer holds the file, and a tombstone
|
|
60
|
+
also suppresses dependency content below it), and hides
|
|
61
|
+
`visibility: private` layers and non-exported resources in the
|
|
62
|
+
exported-only view dependents get. Packages without a `layers` config
|
|
63
|
+
behave exactly as before (single implicit `content/` layer).
|
|
64
|
+
- Composite packages (ARCHITECTURE.md section 4a):
|
|
65
|
+
`metadata.dependencies` (GUID -> semver range) resolve against a
|
|
66
|
+
package store (`CAPSIUM_STORE` or `--store`) of
|
|
67
|
+
`<name>-<version>.cap` files plus an optional `index.json` (GUID ->
|
|
68
|
+
file); the newest satisfying version wins via the built-in semver
|
|
69
|
+
matcher `Capsium::Package::Version`/`VersionRange` (`>=`, `^`, `~`,
|
|
70
|
+
exact, `*`, `x`-wildcards/partials, comma/space conjunctions — no new
|
|
71
|
+
runtime dependency). Resolution is recursive with circularity
|
|
72
|
+
detection and raises typed errors:
|
|
73
|
+
`Capsium::Package::DependencyNotFoundError`,
|
|
74
|
+
`UnsatisfiableDependencyError`, `CircularDependencyError`,
|
|
75
|
+
`DependencyVisibilityError`.
|
|
76
|
+
- Resolved dependencies become read-only layers below all of the
|
|
77
|
+
dependent's own layers, and only `exported` resources are visible;
|
|
78
|
+
routes may address dependency content explicitly as
|
|
79
|
+
`"<dependency-guid>/<path>"` and declare the route-inheritance
|
|
80
|
+
attributes of 05x-routing: `remap` (replaces the serving path),
|
|
81
|
+
`responseRewrite` (`body`, `headers`), `responseHeaders` (merged over
|
|
82
|
+
served headers) and `requestHeaders` (parsed and exposed for
|
|
83
|
+
forwarding reactors; a documented no-op for this static reactor).
|
|
84
|
+
Referencing a dependency's private or missing resource is a load-time
|
|
85
|
+
error. `capsium package info` prints the resolved dependency tree;
|
|
86
|
+
`capsium reactor serve` accepts `--store`.
|
|
87
|
+
- Authentication (05x-authentication, ARCHITECTURE.md section 4b):
|
|
88
|
+
`authentication.json` declares `basicAuth {enabled, passwdFile,
|
|
89
|
+
realm}` and `oauth2 {enabled, provider, clientId, authorizationUrl,
|
|
90
|
+
tokenUrl, userinfoUrl, redirectPath, scopes}`; the reactor enforces
|
|
91
|
+
it via `Capsium::Reactor::Authenticator`. Basic authentication
|
|
92
|
+
challenges (401 + `WWW-Authenticate`) and verifies against the
|
|
93
|
+
htpasswd file — bcrypt via the new `bcrypt` runtime dependency, plus
|
|
94
|
+
pure-Ruby md5-crypt (`$apr1$`/`$1$` as deployed by
|
|
95
|
+
htpasswd/OpenSSL/glibc), `{SHA}` and a `crypt(3)` fallback.
|
|
96
|
+
OAuth2 runs the authorization-code flow over net/http: `/auth/login`
|
|
97
|
+
redirects with HMAC-signed state, the callback exchanges the code at
|
|
98
|
+
`tokenUrl`, fetches the userinfo claims and establishes an
|
|
99
|
+
HMAC-SHA256 signed session cookie (`Capsium::Reactor::Session`).
|
|
100
|
+
Secrets never come from the package: `clientSecret`/`sessionSecret`
|
|
101
|
+
and role assignments live in `deploy.json` (`--deploy` or
|
|
102
|
+
`CAPSIUM_DEPLOY`); without a configured session secret the reactor
|
|
103
|
+
generates one and persists it (mode 0600) outside the package.
|
|
104
|
+
Dataset-route `accessControl {roles, authenticationRequired}` is
|
|
105
|
+
enforced after authentication: 401 unauthenticated, 403 unauthorized.
|
|
106
|
+
- The `content-validity` introspection entry now reports `signed` and
|
|
107
|
+
`encrypted` status, plus `signatureValid` when the package declares a
|
|
108
|
+
signature; `Capsium::Package#encrypted?` reports whether the package
|
|
109
|
+
was loaded from an encrypted source.
|
|
110
|
+
|
|
111
|
+
### Changed
|
|
112
|
+
|
|
113
|
+
- Integrity checksums now exclude `signature.sig` in addition to
|
|
114
|
+
`security.json` (the signature signs the checksum-covered payload, so it
|
|
115
|
+
cannot be part of it).
|
|
116
|
+
- `capsium package pack` drops signing artifacts (`signature.sig`,
|
|
117
|
+
embedded public key) when repacking: signing is a post-pack step.
|
|
118
|
+
- The reactor serves resources through `Package#merged_view` (the
|
|
119
|
+
overlay-aware resolution path); behavior is unchanged for packages
|
|
120
|
+
without layers.
|
|
121
|
+
- Loading a package with declared `metadata.dependencies` now resolves
|
|
122
|
+
them eagerly against the package store (`store:` or `CAPSIUM_STORE`);
|
|
123
|
+
a package whose dependencies cannot resolve (including normalized
|
|
124
|
+
pre-0.2 dependency declarations) fails to load with a typed error.
|
|
125
|
+
- `Metrics/ParameterLists` rubocop budget raised to 6 for the reactor
|
|
126
|
+
constructor's per-concern keywords.
|
|
127
|
+
|
|
3
128
|
## 0.2.0
|
|
4
129
|
|
|
5
130
|
### Changed
|
data/README.adoc
CHANGED
|
@@ -55,6 +55,7 @@ manifest.json # auto-generated at load/pack time when absent
|
|
|
55
55
|
routes.json # auto-generated when absent
|
|
56
56
|
storage.json # optional (only when datasets exist)
|
|
57
57
|
security.json # generated at pack time (SHA-256 checksums)
|
|
58
|
+
authentication.json # optional (user authentication)
|
|
58
59
|
content/ # served content root (URL space mirrors paths below it)
|
|
59
60
|
data/ # datasets (optional)
|
|
60
61
|
----
|
|
@@ -73,9 +74,15 @@ Paths inside the configuration files are package-relative POSIX paths.
|
|
|
73
74
|
full filename); the index HTML additionally gets `/`.
|
|
74
75
|
* **storage.json**: dataset definitions under `storage.dataSets`, either
|
|
75
76
|
schema-backed files (`source`, `schemaFile`, `schemaType: json-schema`)
|
|
76
|
-
or SQLite (`databaseFile`, `table`).
|
|
77
|
+
or SQLite (`databaseFile`, `table`). `storage.layers` declares overlay
|
|
78
|
+
layers (see "Layered storage" below).
|
|
77
79
|
* **security.json**: `security.integrityChecks.checksums` holds a SHA-256 hex
|
|
78
|
-
digest for every file in the package except `security.json` itself
|
|
80
|
+
digest for every file in the package except `security.json` itself and
|
|
81
|
+
`signature.sig`. When the package is signed, `security.digitalSignatures`
|
|
82
|
+
records `{ "publicKey", "signatureFile" }`.
|
|
83
|
+
* **authentication.json**: optional user authentication (see
|
|
84
|
+
"Authentication" below): `authentication.basicAuth` and/or
|
|
85
|
+
`authentication.oauth2`.
|
|
79
86
|
|
|
80
87
|
Legacy (pre-0.2) configuration files are still accepted on read and
|
|
81
88
|
normalized to the canonical forms; writers emit only the canonical forms.
|
|
@@ -151,20 +158,186 @@ Runs a per-check report and exits with status 1 on any failure:
|
|
|
151
158
|
|
|
152
159
|
[source,bash]
|
|
153
160
|
----
|
|
154
|
-
capsium package info|manifest|routes|metadata|storage path-to-package
|
|
161
|
+
capsium package info|manifest|routes|metadata|storage path-to-package [--store DIR]
|
|
155
162
|
----
|
|
156
163
|
|
|
164
|
+
`info` additionally prints the resolved dependency tree for composite
|
|
165
|
+
packages (declared GUID and range, resolved version, store `.cap`), see
|
|
166
|
+
"Composite packages" below.
|
|
167
|
+
|
|
168
|
+
=== Signing a package
|
|
169
|
+
|
|
170
|
+
[source,bash]
|
|
171
|
+
----
|
|
172
|
+
capsium package sign path-to-package-or-cap --key private.pem [--cert cert.pem]
|
|
173
|
+
----
|
|
174
|
+
|
|
175
|
+
Signs the package with an RSA private key (minimum 2048 bits) using
|
|
176
|
+
RSA-SHA256, per the packaging standard's digital signature clause. Signing
|
|
177
|
+
is a post-pack step: `pack` drops signing artifacts, so sign the packed
|
|
178
|
+
directory or the `.cap` file itself.
|
|
179
|
+
|
|
180
|
+
The signed payload is the concatenation, in sorted package-relative path
|
|
181
|
+
order, of the raw bytes of every file covered by the `security.json`
|
|
182
|
+
integrity checksums — i.e. every package file except `security.json` and
|
|
183
|
+
`signature.sig`. The signature over that payload is stored as raw bytes in
|
|
184
|
+
`signature.sig`, the signer's public key PEM is embedded as
|
|
185
|
+
`signature.pub.pem`, and `security.json` records:
|
|
186
|
+
|
|
187
|
+
[source,json]
|
|
188
|
+
----
|
|
189
|
+
{
|
|
190
|
+
"security": {
|
|
191
|
+
"digitalSignatures": {
|
|
192
|
+
"publicKey": "signature.pub.pem",
|
|
193
|
+
"signatureFile": "signature.sig"
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
----
|
|
198
|
+
|
|
199
|
+
When `--cert` is given, the X.509 certificate must match the private key,
|
|
200
|
+
and the certificate's public key is embedded instead.
|
|
201
|
+
|
|
202
|
+
The payload construction is openssl-compatible; to verify a signed package
|
|
203
|
+
independently, concatenate the checksum-covered files in sorted order and
|
|
204
|
+
run:
|
|
205
|
+
|
|
206
|
+
[source,bash]
|
|
207
|
+
----
|
|
208
|
+
openssl dgst -sha256 -verify signature.pub.pem -signature signature.sig payload.bin
|
|
209
|
+
----
|
|
210
|
+
|
|
211
|
+
=== Verifying a package signature
|
|
212
|
+
|
|
213
|
+
[source,bash]
|
|
214
|
+
----
|
|
215
|
+
capsium package verify-signature path-to-package-or-cap [--cert cert-or-public-key.pem]
|
|
216
|
+
----
|
|
217
|
+
|
|
218
|
+
Verifies the declared signature against the package contents (using the
|
|
219
|
+
embedded public key, or the given certificate/public key) and exits with
|
|
220
|
+
status 1 when the package is unsigned or the signature does not match.
|
|
221
|
+
Packages that declare a signature are also verified automatically on load —
|
|
222
|
+
a mismatch raises `Capsium::Package::Signer::SignatureMismatchError`, so a
|
|
223
|
+
tampered signed package cannot be served by the reactor.
|
|
224
|
+
|
|
225
|
+
=== Encrypting a package
|
|
226
|
+
|
|
227
|
+
[source,bash]
|
|
228
|
+
----
|
|
229
|
+
capsium package encrypt path-to-package-or-cap --public-key public.pem -o encrypted.cap
|
|
230
|
+
----
|
|
231
|
+
|
|
232
|
+
Encrypts the whole package for the recipient's RSA public key (or X.509
|
|
233
|
+
certificate), per the packaging standard's encryption clause: the
|
|
234
|
+
encrypted `.cap` is a zip containing `metadata.json` (cleartext, so
|
|
235
|
+
name/version stay readable), `signature.json` (the encryption envelope)
|
|
236
|
+
and `package.enc` — the AES-256-GCM ciphertext of the inner plaintext
|
|
237
|
+
`.cap` zip (content, configuration files, data). A random 256-bit data
|
|
238
|
+
encryption key (DEK) encrypts the inner zip and is wrapped with the
|
|
239
|
+
recipient's public key using RSA-OAEP with SHA-256:
|
|
240
|
+
|
|
241
|
+
[source,json]
|
|
242
|
+
----
|
|
243
|
+
{
|
|
244
|
+
"encryption": {
|
|
245
|
+
"algorithm": "AES-256-GCM",
|
|
246
|
+
"keyManagement": "RSA-OAEP-SHA256",
|
|
247
|
+
"encryptedDek": "<base64>",
|
|
248
|
+
"iv": "<base64>",
|
|
249
|
+
"authTag": "<base64>"
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
----
|
|
253
|
+
|
|
254
|
+
The OCB/OpenPGP alternatives mentioned by the standard are intentionally
|
|
255
|
+
not implemented.
|
|
256
|
+
|
|
257
|
+
=== Decrypting a package
|
|
258
|
+
|
|
259
|
+
[source,bash]
|
|
260
|
+
----
|
|
261
|
+
capsium package decrypt encrypted.cap --private-key private.pem [-o decrypted.cap]
|
|
262
|
+
----
|
|
263
|
+
|
|
264
|
+
Decrypts an encrypted package with the recipient's RSA private key and
|
|
265
|
+
writes the plaintext `.cap` (default output: `<name>-decrypted.cap`).
|
|
266
|
+
Decryption fails with a typed error when the key does not match or the
|
|
267
|
+
ciphertext was tampered with (GCM authentication). Loading an encrypted
|
|
268
|
+
package without a key raises `Capsium::Package::Cipher::KeyRequiredError`
|
|
269
|
+
— the reactor refuses to serve it.
|
|
270
|
+
|
|
271
|
+
=== Testing a package
|
|
272
|
+
|
|
273
|
+
[source,bash]
|
|
274
|
+
----
|
|
275
|
+
capsium package test path-to-package-or-cap
|
|
276
|
+
----
|
|
277
|
+
|
|
278
|
+
Runs the package's test suite declared in the Capsium testing YAML DSL
|
|
279
|
+
(05x-testing): all `tests/*.yaml` files in the package, each holding a
|
|
280
|
+
top-level `tests` list. Every test has a `name` and a `type`; the four
|
|
281
|
+
supported types:
|
|
282
|
+
|
|
283
|
+
* `route` — requests the URL path from a reactor started for the run and
|
|
284
|
+
checks `expected_status`, plus optional `response_contains` (body
|
|
285
|
+
substring) and `expected_content_type`. Absolute URLs are accepted;
|
|
286
|
+
only their path (and query) is used.
|
|
287
|
+
* `file` — the file at `path` must exist in the package; optional
|
|
288
|
+
`contains` checks a content substring.
|
|
289
|
+
* `data_validation` — the rows of `data_file` (format `json` or `yaml`)
|
|
290
|
+
must validate against the JSON schema at `schema_file`. Array data
|
|
291
|
+
validates row by row.
|
|
292
|
+
* `config` — `config_file` (format `json` or `yaml`) must exist and
|
|
293
|
+
parse; the known package configs (`metadata.json`, `manifest.json`,
|
|
294
|
+
`routes.json`, `storage.json`, `security.json`) are additionally
|
|
295
|
+
validated against their canonical models.
|
|
296
|
+
|
|
297
|
+
Example:
|
|
298
|
+
|
|
299
|
+
[source,yaml]
|
|
300
|
+
----
|
|
301
|
+
tests:
|
|
302
|
+
- name: Home Route Test
|
|
303
|
+
type: route
|
|
304
|
+
url: "/home"
|
|
305
|
+
expected_status: 200
|
|
306
|
+
response_contains: "Welcome"
|
|
307
|
+
|
|
308
|
+
- name: Config File Exists
|
|
309
|
+
type: file
|
|
310
|
+
path: "metadata.json"
|
|
311
|
+
|
|
312
|
+
- name: JSON Data Validation
|
|
313
|
+
type: data_validation
|
|
314
|
+
format: json
|
|
315
|
+
data_file: "data/animals.json"
|
|
316
|
+
schema_file: "data/animals.schema.json"
|
|
317
|
+
|
|
318
|
+
- name: Metadata Config Validation
|
|
319
|
+
type: config
|
|
320
|
+
format: json
|
|
321
|
+
config_file: "metadata.json"
|
|
322
|
+
----
|
|
323
|
+
|
|
324
|
+
Each test is reported as `PASS`/`FAIL` with messages; the command exits
|
|
325
|
+
with status 1 when any test fails (or a test definition is invalid).
|
|
326
|
+
|
|
157
327
|
== CLI: Reactor
|
|
158
328
|
|
|
159
329
|
=== Starting a reactor on your package
|
|
160
330
|
|
|
161
331
|
[source,bash]
|
|
162
332
|
----
|
|
163
|
-
capsium reactor serve my_package.cap [--port 8864]
|
|
333
|
+
capsium reactor serve my_package.cap [--port 8864] [--store DIR] [--deploy deploy.json]
|
|
164
334
|
----
|
|
165
335
|
|
|
166
336
|
The reactor takes a local package directory or `.cap` file; serving
|
|
167
|
-
directly from a URL is not supported.
|
|
337
|
+
directly from a URL is not supported. `--store` (or `CAPSIUM_STORE`)
|
|
338
|
+
resolves composite-package dependencies; `--deploy` (or
|
|
339
|
+
`CAPSIUM_DEPLOY`) points at the reactor-side `deploy.json` with
|
|
340
|
+
authentication secrets (see "Authentication" below).
|
|
168
341
|
|
|
169
342
|
=== Reactor introspection API
|
|
170
343
|
|
|
@@ -189,6 +362,116 @@ content checksums — the same data `security.json` integrityChecks carry.
|
|
|
189
362
|
`content-validity` re-verifies the package against `security.json` on
|
|
190
363
|
every request and reports the outcome with a UTC `lastChecked`
|
|
191
364
|
timestamp; `reason` lists the integrity errors when `valid` is false.
|
|
365
|
+
The entry also reports `signed` and `encrypted` status, plus
|
|
366
|
+
`signatureValid` when the package declares a signature.
|
|
367
|
+
|
|
368
|
+
== Layered storage
|
|
369
|
+
|
|
370
|
+
`storage.layers` stacks overlay directories over `content/` (bottom ->
|
|
371
|
+
top in declaration order), each mirroring the `content/` tree:
|
|
372
|
+
|
|
373
|
+
[source,json]
|
|
374
|
+
----
|
|
375
|
+
{ "storage": { "layers": [
|
|
376
|
+
{ "path": "base", "writable": false, "visibility": "exported" },
|
|
377
|
+
{ "path": "updates", "writable": true, "visibility": "private" }
|
|
378
|
+
] } }
|
|
379
|
+
----
|
|
380
|
+
|
|
381
|
+
A request resolves against layers from the TOP down; the first hit
|
|
382
|
+
wins. Deletions are recorded as tombstones: a `.capsium-tombstones`
|
|
383
|
+
JSON file (an array of `content/`-relative paths) in a writable layer;
|
|
384
|
+
a tombstoned path resolves 404 even when a lower layer holds the file,
|
|
385
|
+
while a file reappearing above the tombstone is served again. Packages
|
|
386
|
+
without a `layers` config behave exactly as before (single implicit
|
|
387
|
+
`content/` layer). `visibility: private` layers are hidden from
|
|
388
|
+
dependent packages (see below), as are resources whose manifest
|
|
389
|
+
visibility is `private`.
|
|
390
|
+
|
|
391
|
+
== Composite packages
|
|
392
|
+
|
|
393
|
+
`metadata.dependencies` maps a dependency GUID to a semver range
|
|
394
|
+
(`>=1.0.0`, `^1.2.3`, `~1.2.3`, exact, `*`, `1.x`/`1.2.x`, partials,
|
|
395
|
+
and comma/space conjunctions like `>=1.0.0, <2.0.0`). Dependencies
|
|
396
|
+
resolve against a *package store* — a directory of
|
|
397
|
+
`<name>-<version>.cap` files plus an optional `index.json` (GUID ->
|
|
398
|
+
file) — given via `CAPSIUM_STORE` or `--store`; the newest satisfying
|
|
399
|
+
version wins.
|
|
400
|
+
|
|
401
|
+
On load, each resolved dependency's exported content becomes read-only
|
|
402
|
+
layers below all of the dependent's own layers. Routes may address
|
|
403
|
+
dependency content explicitly (`"resource": "<dependency-guid>/content/app.js"`)
|
|
404
|
+
or declare route-inheritance attributes per 05x-routing: `remap`
|
|
405
|
+
(replaces the serving path), `responseRewrite` (`body`, `headers`),
|
|
406
|
+
`responseHeaders` (merged over served headers) and `requestHeaders`
|
|
407
|
+
(parsed and exposed for forwarding reactors; this reactor serves
|
|
408
|
+
statically, so they do not alter its responses). Referencing a
|
|
409
|
+
dependency's private or missing resource is a load-time error, as are
|
|
410
|
+
circular, missing or unsatisfiable dependencies.
|
|
411
|
+
|
|
412
|
+
[source,bash]
|
|
413
|
+
----
|
|
414
|
+
capsium package info my-composite-package --store ./store
|
|
415
|
+
capsium reactor serve my-composite-package --store ./store
|
|
416
|
+
----
|
|
417
|
+
|
|
418
|
+
== Authentication
|
|
419
|
+
|
|
420
|
+
`authentication.json` enables user authentication (05x-authentication):
|
|
421
|
+
|
|
422
|
+
[source,json]
|
|
423
|
+
----
|
|
424
|
+
{ "authentication": {
|
|
425
|
+
"basicAuth": { "enabled": true, "passwdFile": "auth/.htpasswd", "realm": "capsium" },
|
|
426
|
+
"oauth2": { "enabled": true, "provider": "google", "clientId": "...",
|
|
427
|
+
"authorizationUrl": "...", "tokenUrl": "...", "userinfoUrl": "...",
|
|
428
|
+
"redirectPath": "/auth/callback", "scopes": ["openid", "email"] }
|
|
429
|
+
} }
|
|
430
|
+
----
|
|
431
|
+
|
|
432
|
+
When enabled, the reactor challenges every unauthenticated request
|
|
433
|
+
(`401`, with `WWW-Authenticate: Basic realm="..."` when basicAuth is
|
|
434
|
+
enabled) and verifies credentials against the htpasswd file. Supported
|
|
435
|
+
hashes: bcrypt (`htpasswd -B`, via the `bcrypt` gem), Apache apr1 MD5
|
|
436
|
+
and md5-crypt (`$apr1$`/`$1$`, pure Ruby), unsalted SHA-1 (`{SHA}`),
|
|
437
|
+
and a platform `crypt(3)` fallback for the rest.
|
|
438
|
+
|
|
439
|
+
With OAuth2, `/auth/login` redirects to the provider with HMAC-signed
|
|
440
|
+
state; the callback exchanges the code at `tokenUrl`, fetches the
|
|
441
|
+
userinfo claims and establishes an HMAC-SHA256 signed session cookie
|
|
442
|
+
(`capsium_session`, `HttpOnly; SameSite=Lax`). Provider errors during
|
|
443
|
+
the exchange answer `502`; a tampered state answers `401`.
|
|
444
|
+
|
|
445
|
+
Route-level `accessControl` on dataset routes is enforced after
|
|
446
|
+
authentication — `401` unauthenticated, `403` when the identity lacks a
|
|
447
|
+
required role:
|
|
448
|
+
|
|
449
|
+
[source,json]
|
|
450
|
+
----
|
|
451
|
+
{ "path": "/api/v1/data/animals", "dataset": "animals",
|
|
452
|
+
"accessControl": { "roles": ["admin"], "authenticationRequired": true } }
|
|
453
|
+
----
|
|
454
|
+
|
|
455
|
+
Secrets NEVER come from the package. They live in `deploy.json`
|
|
456
|
+
(`--deploy` or `CAPSIUM_DEPLOY`):
|
|
457
|
+
|
|
458
|
+
[source,json]
|
|
459
|
+
----
|
|
460
|
+
{
|
|
461
|
+
"baseUrl": "http://localhost:8864",
|
|
462
|
+
"authentication": {
|
|
463
|
+
"basicAuth": { "passwdFile": "/secure/outside/package/.htpasswd" },
|
|
464
|
+
"oauth2": { "clientSecret": "..." },
|
|
465
|
+
"sessionSecret": "...",
|
|
466
|
+
"roles": { "alice": ["admin"] }
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
----
|
|
470
|
+
|
|
471
|
+
`roles` assigns roles by identity name (basic-auth username, OAuth2
|
|
472
|
+
email or subject); userinfo `roles` claims are honored too. Without a
|
|
473
|
+
configured `sessionSecret`, the reactor generates one and persists it
|
|
474
|
+
(mode `0600`) outside the package.
|
|
192
475
|
|
|
193
476
|
== Programmatically managing packages
|
|
194
477
|
|
|
@@ -203,6 +486,11 @@ require 'capsium'
|
|
|
203
486
|
|
|
204
487
|
# Read a package directory or a .cap file
|
|
205
488
|
package = Capsium::Package.new(path)
|
|
489
|
+
|
|
490
|
+
# Read an encrypted .cap (raises Capsium::Package::Cipher::KeyRequiredError
|
|
491
|
+
# without a key, Capsium::Package::Cipher::DecryptionError for a wrong key
|
|
492
|
+
# or tampered ciphertext)
|
|
493
|
+
package = Capsium::Package.new('encrypted.cap', decryption_key: 'private.pem')
|
|
206
494
|
----
|
|
207
495
|
|
|
208
496
|
When `security.json` is present, the package is verified on load and
|
|
@@ -231,6 +519,45 @@ puts animals.data.inspect
|
|
|
231
519
|
|
|
232
520
|
# Verifying integrity (returns a list of typed errors, empty when valid)
|
|
233
521
|
errors = package.verify_integrity
|
|
522
|
+
|
|
523
|
+
# Digital signatures (RSA-SHA256)
|
|
524
|
+
package.signed? # => security.json declares digitalSignatures
|
|
525
|
+
package.verify_signature # => true/false
|
|
526
|
+
|
|
527
|
+
signer = Capsium::Package::Signer.new(package_dir)
|
|
528
|
+
signer.sign('private.pem') # or sign('private.pem', 'cert.pem')
|
|
529
|
+
signer.verify # embedded public key
|
|
530
|
+
signer.verify('cert-or-public-key.pem') # explicit key
|
|
531
|
+
signer.verify! # raises SignatureMismatchError
|
|
532
|
+
|
|
533
|
+
# Whole-package encryption (AES-256-GCM, RSA-OAEP-SHA256 wrapped DEK)
|
|
534
|
+
cipher = Capsium::Package::Cipher.new
|
|
535
|
+
cipher.encrypt('pkg-1.0.0.cap', 'public.pem', 'encrypted.cap')
|
|
536
|
+
cipher.decrypt('encrypted.cap', 'private.pem', 'decrypted.cap')
|
|
537
|
+
Capsium::Package::Cipher.encrypted?('encrypted.cap') # => true
|
|
538
|
+
|
|
539
|
+
# Running the package's tests/*.yaml suite (05x-testing DSL)
|
|
540
|
+
report = Capsium::Package::Testing::TestSuite.new(package).run
|
|
541
|
+
report.ok? # => true/false
|
|
542
|
+
report.failures # => failed TestCase::Result list
|
|
543
|
+
report.summary # => "7 tests, 0 failures"
|
|
544
|
+
|
|
545
|
+
# Layered storage (5a): the merged overlay view shared with the reactor
|
|
546
|
+
view = package.merged_view
|
|
547
|
+
view.resolve('content/app.js') # => absolute path of the topmost hit, or nil
|
|
548
|
+
|
|
549
|
+
# Composite packages (4a): resolve metadata.dependencies against a store
|
|
550
|
+
package = Capsium::Package.new(dir, store: './store') # or CAPSIUM_STORE
|
|
551
|
+
package.resolved_dependencies.each do |dep|
|
|
552
|
+
puts "#{dep.guid} (#{dep.range}) => #{dep.version} [#{dep.path}]"
|
|
553
|
+
end
|
|
554
|
+
view = package.merged_view # own layers over dependency layers
|
|
555
|
+
view = package.merged_view(exported_only: true) # what dependents may see
|
|
556
|
+
|
|
557
|
+
# Serving with authentication and a store
|
|
558
|
+
reactor = Capsium::Reactor.new(package: dir, store: './store',
|
|
559
|
+
deploy: 'deploy.json')
|
|
560
|
+
reactor.serve
|
|
234
561
|
----
|
|
235
562
|
|
|
236
563
|
=== Packing and unpacking programmatically
|
data/capsium.gemspec
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Capsium
|
|
4
|
+
class Cli < Thor
|
|
5
|
+
# Output formatting helpers for CLI subcommands.
|
|
6
|
+
module Formatting
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def format_result(result)
|
|
10
|
+
line = "#{result.ok? ? 'PASS' : 'FAIL'} #{result.name}"
|
|
11
|
+
return line if result.messages.empty?
|
|
12
|
+
|
|
13
|
+
"#{line}: #{result.messages.join('; ')}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# The resolved dependency tree (ARCHITECTURE.md section 4a): one
|
|
17
|
+
# line per dependency showing the declared GUID and range, the
|
|
18
|
+
# resolved version and the store .cap it resolved to, nested.
|
|
19
|
+
def print_dependency_tree(package, indent = "")
|
|
20
|
+
package.resolved_dependencies.each do |dep|
|
|
21
|
+
puts "#{indent}- #{dep.guid} (#{dep.range}) => #{dep.version} [#{dep.path}]"
|
|
22
|
+
print_dependency_tree(dep.package, "#{indent} ")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/capsium/cli/package.rb
CHANGED
|
@@ -6,14 +6,17 @@ module Capsium
|
|
|
6
6
|
class Cli
|
|
7
7
|
class Package < Thor
|
|
8
8
|
extend ThorExt::Start
|
|
9
|
+
include Formatting
|
|
9
10
|
|
|
10
11
|
desc "info PACKAGE_PATH", "Display information about the package"
|
|
12
|
+
option :store, type: :string, desc: "Package store directory (default: CAPSIUM_STORE)"
|
|
11
13
|
|
|
12
14
|
def info(path_to_package)
|
|
13
|
-
package = Capsium::Package.new(path_to_package)
|
|
15
|
+
package = Capsium::Package.new(path_to_package, store: options[:store])
|
|
14
16
|
puts "Package Path: #{package.path}"
|
|
15
17
|
puts "Routes: #{package.routes.to_hash}"
|
|
16
18
|
puts "Manifest: #{package.manifest.to_hash}"
|
|
19
|
+
print_dependency_tree(package)
|
|
17
20
|
end
|
|
18
21
|
|
|
19
22
|
desc "manifest PATH_TO_PACKAGE",
|
|
@@ -73,13 +76,72 @@ module Capsium
|
|
|
73
76
|
raise Thor::Error, "Package validation failed"
|
|
74
77
|
end
|
|
75
78
|
|
|
76
|
-
|
|
79
|
+
desc "sign PACKAGE_PATH --key KEY.pem [--cert CERT.pem]",
|
|
80
|
+
"Sign the package (RSA-SHA256) and record the signature in security.json"
|
|
81
|
+
option :key, type: :string, required: true,
|
|
82
|
+
desc: "Path to the RSA private key PEM (min 2048 bits)"
|
|
83
|
+
option :cert, type: :string,
|
|
84
|
+
desc: "Path to the X.509 certificate PEM (must match the key)"
|
|
85
|
+
|
|
86
|
+
def sign(path_to_package)
|
|
87
|
+
Capsium::Package::Signer.sign_package(path_to_package, options[:key], options[:cert])
|
|
88
|
+
puts "Package signed: #{path_to_package}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
desc "verify-signature PACKAGE_PATH [--cert CERT.pem]",
|
|
92
|
+
"Verify the package digital signature declared in security.json"
|
|
93
|
+
option :cert, type: :string,
|
|
94
|
+
desc: "Path to the X.509 certificate or public key PEM " \
|
|
95
|
+
"(defaults to the key embedded in the package)"
|
|
96
|
+
|
|
97
|
+
def verify_signature(path_to_package)
|
|
98
|
+
unless Capsium::Package::Signer.verify_package(path_to_package, options[:cert])
|
|
99
|
+
raise Thor::Error, "Signature verification failed: #{path_to_package}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
puts "Signature valid: #{path_to_package}"
|
|
103
|
+
rescue Capsium::Package::Signer::SignatureError => e
|
|
104
|
+
raise Thor::Error, e.message
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
desc "encrypt PACKAGE_PATH --public-key PUB.pem -o OUT.cap",
|
|
108
|
+
"Encrypt a package (AES-256-GCM, RSA-OAEP-SHA256 wrapped DEK)"
|
|
109
|
+
option :public_key, type: :string, required: true,
|
|
110
|
+
desc: "Path to the recipient RSA public key or X.509 certificate PEM"
|
|
111
|
+
option :output, type: :string, required: true, aliases: "-o",
|
|
112
|
+
desc: "Output path for the encrypted .cap"
|
|
113
|
+
|
|
114
|
+
def encrypt(path_to_package)
|
|
115
|
+
Capsium::Package::Cipher.new.encrypt(
|
|
116
|
+
path_to_package, options[:public_key], options[:output]
|
|
117
|
+
)
|
|
118
|
+
puts "Package encrypted: #{options[:output]}"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
desc "decrypt PACKAGE_PATH --private-key PRIV.pem [-o OUT.cap]",
|
|
122
|
+
"Decrypt an encrypted package"
|
|
123
|
+
option :private_key, type: :string, required: true,
|
|
124
|
+
desc: "Path to the recipient RSA private key PEM"
|
|
125
|
+
option :output, type: :string, aliases: "-o",
|
|
126
|
+
desc: "Output path (default: <name>-decrypted.cap)"
|
|
127
|
+
|
|
128
|
+
def decrypt(path_to_package)
|
|
129
|
+
output = options[:output] || "#{File.basename(path_to_package, '.cap')}-decrypted.cap"
|
|
130
|
+
Capsium::Package::Cipher.new.decrypt(path_to_package, options[:private_key], output)
|
|
131
|
+
puts "Package decrypted: #{output}"
|
|
132
|
+
end
|
|
77
133
|
|
|
78
|
-
|
|
79
|
-
line = "#{result.ok? ? 'PASS' : 'FAIL'} #{result.name}"
|
|
80
|
-
return line if result.messages.empty?
|
|
134
|
+
desc "test PACKAGE_PATH", "Run the package's tests/*.yaml suite (05x-testing DSL)"
|
|
81
135
|
|
|
82
|
-
|
|
136
|
+
def test(path_to_package)
|
|
137
|
+
package = Capsium::Package.new(path_to_package)
|
|
138
|
+
report = Capsium::Package::Testing::TestSuite.new(package).run
|
|
139
|
+
report.results.each { |result| puts format_result(result) }
|
|
140
|
+
if report.results.empty?
|
|
141
|
+
puts "No tests found (#{Capsium::Package::Testing::TestSuite::TESTS_DIR}/*.yaml)"
|
|
142
|
+
end
|
|
143
|
+
puts report.summary
|
|
144
|
+
raise Thor::Error, "Package tests failed" unless report.ok?
|
|
83
145
|
end
|
|
84
146
|
end
|
|
85
147
|
end
|