| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669 |
- using Invercargill;
- using Invercargill.DataStructures;
- using Invercargill.Mapping;
- using InvercargillJson;
- using Inversion;
- using Astralis;
- using Statum;
- namespace Statum.Tests {
- int failures = 0;
- int passes = 0;
- void check(bool condition, string label) {
- if (condition) {
- passes++;
- print("PASS %s\n", label);
- } else {
- failures++;
- print("FAIL %s\n", label);
- }
- }
- /**
- * Scalar model in the style of Percipio's login private data: the field
- * types that previously crashed (int64, bool) or mis-serialised (int) the
- * encryption path.
- */
- public class LoginPrivate : Object {
- public int64 user_id { get; set; }
- public bool is_admin { get; set; }
- public int attempts { get; set; }
- public double score { get; set; }
- public string username { get; set; }
- public long session_no { get; set; }
- }
- /** List-bearing model: a `string[]` plus a collection of a registered type. */
- public class ArrayModel : Object {
- public string[] tags { get; set; }
- public Series<Model.ActionDto> refs { get; set; }
- }
- /** Typed action WITHOUT a `private_type` override — demonstrates S-8 erosion. */
- public class ErodingAction : TypedStatumAction<LoginPrivate> {
- public Type exposed_private_type() {
- return private_type;
- }
- public LoginPrivate? exposed_request_private() {
- return request_private;
- }
- public void feed(Properties props) {
- request = new StatumRequest() { action_private = props };
- }
- public override async DirectiveBuilder handle() throws GLib.Error {
- return directives();
- }
- }
- /** Typed action WITH the `private_type` override — the supported pattern. */
- public class FixedAction : TypedStatumAction<LoginPrivate> {
- protected override Type private_type { get { return typeof(LoginPrivate); } }
- public Type exposed_private_type() {
- return private_type;
- }
- public LoginPrivate? exposed_request_private() {
- return request_private;
- }
- public void feed(Properties props) {
- request = new StatumRequest() { action_private = props };
- }
- public override async DirectiveBuilder handle() throws GLib.Error {
- return directives();
- }
- }
- LoginPrivate sample_private() {
- return new LoginPrivate() {
- user_id = (int64)9007199254740993,
- is_admin = true,
- attempts = 3,
- score = 0.5,
- username = "bbarrow",
- session_no = (long)123456789012345
- };
- }
- void assert_private_matches(LoginPrivate expected, LoginPrivate actual, string label) {
- check(actual.user_id == expected.user_id, label + ": user_id int64 round-trip");
- check(actual.is_admin == expected.is_admin, label + ": is_admin bool round-trip");
- check(actual.attempts == expected.attempts, label + ": attempts int round-trip");
- check(actual.score == expected.score, label + ": score double round-trip");
- check(actual.username == expected.username, label + ": username string round-trip");
- check(actual.session_no == expected.session_no, label + ": session_no long round-trip");
- }
- void test_scalar_public_path() throws GLib.Error {
- var model = sample_private();
- var props = GObjectMapping.to_properties(model);
- var wire = new JsonElement.from_properties(props).stringify();
- check(wire.contains("\"user_id\":9007199254740993"), "public path: int64 serialises as an unquoted JSON number");
- check(wire.contains("\"is_admin\":true"), "public path: bool serialises as an unquoted JSON boolean");
- check(wire.contains("\"attempts\":3"), "public path: int serialises as an unquoted JSON number, not a string");
- check(wire.contains("\"score\":0.5"), "public path: double serialises as an unquoted JSON number");
- var back = (LoginPrivate) GObjectMapping.from_properties(typeof(LoginPrivate),
- new JsonElement.from_string(wire).as<Properties>());
- assert_private_matches(model, back, "public path");
- }
- void test_scalar_encryption_path(Statum.Cryptography.EncryptionProvider encryption) throws GLib.Error {
- var model = sample_private();
- var blob = encryption.author_properties("statum-tests", GObjectMapping.to_properties(model));
- var props = encryption.read_properties("statum-tests", blob);
- var back = (LoginPrivate) GObjectMapping.from_properties(typeof(LoginPrivate), props);
- assert_private_matches(model, back, "encryption path");
- }
- void test_arrays(Statum.Cryptography.EncryptionProvider encryption) throws GLib.Error {
- var first = new Model.ActionDto() { uri = "/_statum/action/aaaa", method = "POST" };
- var second = new Model.ActionDto() { uri = "/_statum/action/bbbb", method = "DELETE" };
- var refs = new Series<Model.ActionDto>();
- refs.add(first);
- refs.add(second);
- var model = new ArrayModel() { tags = new string[] { "alpha", "beta", "gamma" }, refs = refs };
- var props = GObjectMapping.to_properties(model);
- var wire = new JsonElement.from_properties(props).stringify();
- check(wire.contains("\"tags\":[\"alpha\",\"beta\",\"gamma\"]"), "arrays: string[] serialises as a JSON array");
- check(wire.contains("\"uri\":\"/_statum/action/bbbb\"")
- && wire.contains("\"method\":\"DELETE\""), "arrays: registered-Object collection serialises as an array of nested objects");
- var parsed = new JsonElement.from_string(wire).as<Properties>();
- var array_model = (ArrayModel) GObjectMapping.from_properties(typeof(ArrayModel), parsed);
- check(array_model.tags.length == 3 && array_model.tags[0] == "alpha"
- && array_model.tags[1] == "beta" && array_model.tags[2] == "gamma", "arrays: string[] read-back from JSON array");
- var blob = encryption.author_properties("statum-tests", props);
- var decrypted = (ArrayModel) GObjectMapping.from_properties(typeof(ArrayModel),
- encryption.read_properties("statum-tests", blob));
- check(decrypted.tags.length == 3 && decrypted.tags[2] == "gamma", "arrays: string[] round-trips through the encryption path");
- }
- void test_typed_action_private(Inversion.Scope scope, Statum.Cryptography.EncryptionProvider encryption) throws GLib.Error {
- var eroding = (ErodingAction) scope.resolve_type(typeof(ErodingAction));
- check(eroding.exposed_private_type() == typeof(void), "S-8 repro: typeof(TPrivate) erodes to void for a reflectively-instantiated action");
- eroding.feed(new PropertyDictionary());
- check(eroding.exposed_request_private() == null, "S-8 repro: un-overridden request_private yields null without crashing");
- var fixed_action = (FixedAction) scope.resolve_type(typeof(FixedAction));
- check(fixed_action.exposed_private_type() == typeof(LoginPrivate), "S-8 fix: private_type override captures the GType from a foreign namespace");
- var model = sample_private();
- var blob = encryption.author_properties(fixed_action.get_type().name(), GObjectMapping.to_properties(model));
- fixed_action.feed(encryption.read_properties(fixed_action.get_type().name(), blob));
- var private_data = fixed_action.exposed_request_private();
- check(private_data != null, "S-8 fix: request_private materialises from a sealed blob");
- if (private_data != null) {
- assert_private_matches(model, (!)private_data, "S-8 fix");
- }
- }
- /**
- * SA-4: restore_slot must enforce the snapshot's invalid_after and must
- * not refresh last_touched, so replayed frames cannot pin slots forever
- * and the cleanup sweep still expires restored slots.
- */
- void test_restore_slot_expiry(StateService states, Statum.Cryptography.SigningProvider signing) throws GLib.Error {
- // A live frame authored now: restorable, keeps its own as_at as
- // last_touched, and still expirable by cleanup.
- var slot = states.new_slot(Scope.SESSION, new State() {
- type_name = "test-sa4",
- public_data = new PropertyDictionary(),
- private_data = new PropertyDictionary()
- });
- var live_frame = sign_snapshot(signing, slot.id, "test-sa4", new DateTime.now_utc().add_hours(6));
- var before_restore = new DateTime.now_utc();
- // Evict the server-side copy first so restore_slot has an empty cache
- // to restore into (a non-empty slot makes restore a no-op).
- states.clear_slot.begin(slot.id);
- check(states.get_slot(slot.id) == null, "SA-4: slot evicted before first restore");
- var restored = states.restore_slot(live_frame);
- check(restored, "SA-4: a frame inside its invalid_after window restores");
- var cached = states.get_slot(slot.id);
- check(cached != null, "SA-4: the restored slot is cached");
- if (cached != null) {
- var touched = ((!)cached).current_state.last_touched;
- check(touched.compare(before_restore) <= 0,
- "SA-4: restore does not refresh last_touched (the clock is not 'now')");
- }
- // The same slot re-restored after eviction must still expire through
- // cleanup, because last_touched came from the snapshot, not 'now'.
- states.clear_slot.begin(slot.id); // remove from cache (async push is a no-op)
- check(states.get_slot(slot.id) == null, "SA-4: slot evicted before replay");
- // Re-sign a frame with an as_at in the past but still valid.
- var old_touch = new DateTime.now_utc().add_days(-6);
- var stale_frame = sign_snapshot(signing, slot.id, "test-sa4-old", new DateTime.now_utc().add_hours(1), old_touch);
- check(states.restore_slot(stale_frame), "SA-4: stale-but-valid frame restores");
- states.cleanup();
- check(states.get_slot(slot.id) != null, "SA-4: restored slot within EXPIRY_SECONDS survives cleanup");
- // A frame whose as_at predates the 7-day expiry is gone after cleanup.
- var ancient_touch = new DateTime.now_utc().add_days(-9);
- var ancient_frame = sign_snapshot(signing, slot.id + "-ancient", "test-sa4-ancient",
- new DateTime.now_utc().add_hours(1), ancient_touch);
- check(states.restore_slot(ancient_frame), "SA-4: ancient-but-valid frame restores (once)");
- states.cleanup();
- check(states.get_slot(slot.id + "-ancient") == null,
- "SA-4: cleanup expires a restored slot (restore no longer resets last_touched to now)");
- // A frame past its invalid_after is rejected outright.
- var expired_frame = sign_snapshot(signing, slot.id + "-expired", "test-sa4-expired",
- new DateTime.now_utc().add_seconds(-5));
- bool threw = false;
- try {
- states.restore_slot(expired_frame);
- } catch (StateError e) {
- threw = true;
- } catch (GLib.Error e) {
- threw = false;
- }
- check(threw, "SA-4: restore_slot rejects a frame past invalid_after with FRAME_EXPIRED");
- check(states.get_slot(slot.id + "-expired") == null,
- "SA-4: a rejected frame leaves no slot cached");
- // SA-4 bonus: a rejected replay also drops any slot already cached
- // under the same key, so the stale entry cannot linger.
- var cached_key = slot.id + "-drop";
- check(states.restore_slot(sign_snapshot(signing, cached_key, "test-sa4-drop",
- new DateTime.now_utc().add_hours(1))), "SA-4: slot cached for the drop test");
- check(states.get_slot(cached_key) != null, "SA-4: slot present before expired replay");
- try {
- states.restore_slot(sign_snapshot(signing, cached_key, "test-sa4-drop",
- new DateTime.now_utc().add_seconds(-5)));
- } catch (StateError.FRAME_EXPIRED e) {
- // expected
- }
- check(states.get_slot(cached_key) == null,
- "SA-4: an expired replay of the same key invalidates the cached slot");
- }
- /** Signs a snapshot for `key` valid until `invalid_after`, authored `as_at`. */
- Model.FrameDto sign_snapshot(Statum.Cryptography.SigningProvider signing, string key, string type,
- DateTime invalid_after, DateTime? as_at = null) throws GLib.Error {
- var dto = new Model.SnapshotDto() {
- as_at = as_at ?? new DateTime.now_utc(),
- slot = new Model.SlotDto() {
- key = key,
- scope = Scope.SESSION,
- slot_type = type
- },
- transmit_after = new DateTime.now_utc().add_seconds(60),
- invalid_after = invalid_after,
- @public = new PropertyDictionary()
- };
- var json = new JsonElement.from_properties(Model.SnapshotDto.get_mapper().map_from(dto)).stringify();
- return signing.author_frame(json);
- }
- /** SA-10: verify_frame must fail closed on a mismatched signer claim. */
- void test_verify_frame_signer(Statum.Cryptography.SigningProvider signing,
- Statum.Cryptography.SigningProvider other) throws GLib.Error {
- var frame = sign_snapshot(signing, "sa10-key", "test-sa10", new DateTime.now_utc().add_hours(1));
- signing.verify_frame(frame);
- check(true, "SA-10: a frame signed and claimed by this provider verifies");
- // Same content and signature, forged signer claim.
- var forged = new Model.FrameDto() {
- content = frame.content,
- signer = other.signer_id,
- signature = frame.signature
- };
- bool threw = false;
- try {
- signing.verify_frame(forged);
- } catch (Statum.Cryptography.SigningError e) {
- threw = true;
- }
- check(threw, "SA-10: verify_frame rejects a frame whose signer != signer_id");
- // A frame actually signed by another provider is rejected too (its
- // signature does not verify against this key).
- var foreign = sign_snapshot(other, "sa10-key", "test-sa10", new DateTime.now_utc().add_hours(1));
- threw = false;
- try {
- signing.verify_frame(foreign);
- } catch (Statum.Cryptography.SigningError e) {
- threw = true;
- }
- check(threw, "SA-10: verify_frame rejects a frame signed by a different key");
- }
- // -----------------------------------------------------------------
- // Locksmith port (goal 44): wire compatibility and restart survival.
- // Key material is generated through Locksmith directly — mirroring
- // `statum-genkeys` without shelling out to it.
- // -----------------------------------------------------------------
- /** Raw key bytes in the shape `web-config.json` carries. */
- public class KeyMaterial : Object {
- public uint8[] signing_sk = new uint8[0];
- public uint8[] signing_pk = new uint8[0];
- public uint8[] enc_signing_sk = new uint8[0];
- public uint8[] enc_signing_pk = new uint8[0];
- public uint8[] sealing_sk = new uint8[0];
- public uint8[] sealing_pk = new uint8[0];
- }
- KeyMaterial generate_key_material() {
- var material = new KeyMaterial();
- var signing = Locksmith.Signing.KeyPair.generate();
- material.signing_sk = signing.to_secret_key().to_array();
- material.signing_pk = signing.public_key.key.to_array();
- var enc_signing = Locksmith.Signing.KeyPair.generate();
- material.enc_signing_sk = enc_signing.to_secret_key().to_array();
- material.enc_signing_pk = enc_signing.public_key.key.to_array();
- var sealing = Locksmith.Sealing.KeyPair.generate();
- material.sealing_sk = sealing.to_secret_key().to_array();
- material.sealing_pk = sealing.public_key.key.to_array();
- return material;
- }
- Statum.Cryptography.SigningProvider signing_from_keys(KeyMaterial material) throws Locksmith.Error {
- return new Statum.Cryptography.SigningProvider.with_keys(material.signing_sk, material.signing_pk);
- }
- Statum.Cryptography.EncryptionProvider encryption_from_keys(KeyMaterial material) throws Locksmith.Error {
- return new Statum.Cryptography.EncryptionProvider.with_keys(
- material.enc_signing_sk, material.enc_signing_pk,
- material.sealing_sk, material.sealing_pk);
- }
- /**
- * Restart survival: providers rebuilt from the same raw key bytes (the
- * `web-config.json` static-key path) verify frames and unseal private
- * blobs that were authored before the "restart".
- */
- void test_locksmith_restart_survival() throws GLib.Error {
- var material = generate_key_material();
- Statum.Cryptography.SigningProvider first_signing = null;
- Statum.Cryptography.EncryptionProvider first_encryption = null;
- try {
- first_signing = signing_from_keys(material);
- first_encryption = encryption_from_keys(material);
- } catch (Locksmith.Error e) {
- check(false, "restart: with_keys accepts freshly generated Locksmith key material (" + e.message + ")");
- return;
- }
- check(true, "restart: with_keys accepts freshly generated Locksmith key material");
- // Author a frame and a sealed private blob with the "before" pair.
- var frame = first_signing.author_frame("{\"slot\":\"restart-survival\"}");
- var model = sample_private();
- var blob = first_encryption.author_properties("statum-tests", GObjectMapping.to_properties(model));
- // Fresh provider instances from the same raw bytes model a restart.
- Statum.Cryptography.SigningProvider second_signing;
- Statum.Cryptography.EncryptionProvider second_encryption;
- try {
- second_signing = signing_from_keys(material);
- second_encryption = encryption_from_keys(material);
- } catch (Locksmith.Error e) {
- check(false, "restart: with_keys rebuilds providers from the same raw key bytes (" + e.message + ")");
- return;
- }
- check(true, "restart: with_keys rebuilds providers from the same raw key bytes");
- second_signing.verify_frame(frame);
- check(true, "restart: a frame authored before the restart verifies through a rebuilt provider");
- var back = (LoginPrivate) GObjectMapping.from_properties(typeof(LoginPrivate),
- second_encryption.read_properties("statum-tests", blob));
- assert_private_matches(model, back, "restart");
- check(first_signing.signer_id == second_signing.signer_id,
- "restart: signer_id is identical across provider instances");
- check(Base64.decode(frame.signature).length == 64,
- "restart: frame signatures are 64-byte detached Ed25519 signatures");
- }
- /**
- * Wire stability of `signer_id`: Locksmith's `to_base64` and GLib's
- * `Base64.encode` agree on the raw 32-byte public key, so frames already
- * held by clients (localStorage) keep verifying and keep naming the same
- * signer after the Locksmith port.
- */
- void test_locksmith_signer_id_wire_stability() throws GLib.Error {
- var ephemeral = new Statum.Cryptography.SigningProvider();
- var raw = ephemeral.get_public_key();
- check(raw.length == 32, "wire: get_public_key returns a raw 32-byte Ed25519 public key");
- check(ephemeral.signer_id == Base64.encode(raw),
- "wire: signer_id is GLib Base64.encode of the raw public key (ephemeral provider)");
- var material = generate_key_material();
- try {
- var configured = signing_from_keys(material);
- check(configured.signer_id == Base64.encode(configured.get_public_key()),
- "wire: signer_id is GLib Base64.encode of the raw public key (configured provider)");
- check(configured.signer_id == Base64.encode(material.signing_pk),
- "wire: a configured provider's signer_id matches its configured public key bytes");
- } catch (Locksmith.Error e) {
- check(false, "wire: configured provider constructs (" + e.message + ")");
- }
- }
- /**
- * Foreign and malformed verification keys verify nothing and crash
- * nothing (no GLib criticals).
- */
- void test_locksmith_foreign_and_malformed_keys() throws GLib.Error {
- var material = generate_key_material();
- var provider = signing_from_keys(material);
- var frame = provider.author_frame("{\"slot\":\"foreign\"}");
- check(provider.verify_with_key(frame.content, frame.signature, material.signing_pk),
- "foreign: the provider's own public key verifies its signature");
- var foreign = Locksmith.Signing.KeyPair.generate();
- check(!provider.verify_with_key(frame.content, frame.signature, foreign.public_key.key.to_array()),
- "foreign: an unrelated third key's public key verifies nothing");
- var malformed = new uint8[31];
- check(!provider.verify_with_key(frame.content, frame.signature, malformed),
- "malformed: a wrong-length (31-byte) verification key verifies nothing");
- }
- /**
- * `with_keys` pk-mismatch policy: the public key DERIVED from the secret
- * wins. A supplied config pk that disagrees with the secret is a config
- * error (loud warning at construction — not captured here); the derived
- * key is the one that signs, names the signer and verifies.
- */
- void test_locksmith_pk_mismatch_policy() throws GLib.Error {
- var material = generate_key_material();
- var wrong_pk = Locksmith.Signing.KeyPair.generate().public_key.key.to_array();
- var provider = new Statum.Cryptography.SigningProvider.with_keys(material.signing_sk, wrong_pk);
- check(provider.signer_id == Base64.encode(material.signing_pk),
- "mismatch: signer_id is the pk derived from the secret, not the supplied one");
- check(provider.signer_id != Base64.encode(wrong_pk),
- "mismatch: the supplied wrong public key is not the signer_id");
- var frame = provider.author_frame("{\"slot\":\"mismatch\"}");
- check(provider.verify_with_key(frame.content, frame.signature, material.signing_pk),
- "mismatch: a signature by this provider verifies against the derived pk");
- check(!provider.verify_with_key(frame.content, frame.signature, wrong_pk),
- "mismatch: the same signature fails against the supplied wrong pk");
- }
- /**
- * Error semantics survived the Locksmith port: namespace binding rejects
- * cross-namespace reads, garbage fails to decrypt, and a tampered blob
- * never reads back — all as `EncryptionError`, never a crash.
- */
- void test_locksmith_error_semantics() throws GLib.Error {
- var encryption = new Statum.Cryptography.EncryptionProvider();
- var blob = encryption.author_string("statum-a", "{\"n\":1}");
- bool mismatched = false;
- try {
- encryption.read_string("statum-b", blob);
- } catch (Statum.Cryptography.EncryptionError.NAMESPACE_MISMATCH e) {
- mismatched = true;
- }
- check(mismatched, "errors: a blob authored under namespace \"a\" and read under \"b\" throws NAMESPACE_MISMATCH");
- var garbage = Base64.encode(new uint8[] { 1, 2, 3, 4, 5 });
- bool undecryptable = false;
- try {
- encryption.read_string("statum-a", garbage);
- } catch (Statum.Cryptography.EncryptionError.DECRYPTION_FAILED e) {
- undecryptable = true;
- }
- check(undecryptable, "errors: base64-encoded garbage throws DECRYPTION_FAILED");
- var bytes = Base64.decode(blob);
- bytes[bytes.length - 1] = bytes[bytes.length - 1] ^ 0xff;
- bool tampering_threw = false;
- string detail = "read back without throwing";
- try {
- encryption.read_string("statum-a", Base64.encode(bytes));
- } catch (Statum.Cryptography.EncryptionError e) {
- tampering_threw = true;
- detail = e.message;
- }
- check(tampering_threw, "errors: a byte-flipped blob throws EncryptionError, not a crash (" + detail + ")");
- }
- /** SA-15: global and per-IP channel connection caps. */
- void test_connection_caps() {
- var tracker = new ConnectionTracker(4, 2);
- check(tracker.can_accept("10.0.0.1"), "SA-15: first connection from an IP is accepted");
- tracker.acquired("10.0.0.1");
- check(tracker.can_accept("10.0.0.1"), "SA-15: second connection from the same IP is accepted");
- tracker.acquired("10.0.0.1");
- check(!tracker.can_accept("10.0.0.1"), "SA-15: third connection from the same IP is refused (per-IP cap)");
- check(tracker.can_accept("10.0.0.2"), "SA-15: another IP is unaffected by the per-IP cap");
- tracker.released("10.0.0.1");
- check(tracker.can_accept("10.0.0.1"), "SA-15: releasing a connection frees its per-IP slot");
- tracker.acquired("10.0.0.2");
- tracker.acquired("10.0.0.3");
- // total: 1 + 1 + 1 = 3 of 4 — the next is the last.
- check(tracker.can_accept("10.0.0.4"), "SA-15: connection accepted up to the global cap");
- tracker.acquired("10.0.0.4");
- check(!tracker.can_accept("10.0.0.4"), "SA-15: connections beyond the global cap are refused");
- check(!tracker.can_accept("192.168.0.1"), "SA-15: the global cap refuses new IPs too");
- check(tracker.open_total() == 4, "SA-15: the global count tracks acquisitions");
- tracker.released("10.0.0.2");
- check(tracker.can_accept("192.168.0.1"), "SA-15: a release frees the global slot");
- // Unknown clients count globally but cannot trip the per-IP cap.
- var anon = new ConnectionTracker(1, 2);
- check(anon.can_accept(null), "SA-15: unknown client is accepted when under the global cap");
- anon.acquired(null);
- check(!anon.can_accept(null), "SA-15: unknown clients still respect the global cap");
- check(anon.open_for(null) == 0, "SA-15: unknown client is not counted per-IP");
- }
- /** SA-15: subscriptions per channel are bounded. */
- void test_subscription_cap(ChannelEndpoint channel) {
- // subscribe() only acts on channels with an open connection; without
- // one it must simply no-op (returning false) rather than crash.
- bool recorded = channel.subscribe("nonexistent-channel", "some-key");
- check(!recorded, "SA-15: subscribing to a channel with no connection is a no-op");
- }
- /** SA-6: the authorize hooks default open and honour installed guards. */
- void test_authorization_policy() {
- var policy = new AuthorizationPolicy();
- check(policy.authorize_entrypoint(null) == null, "SA-6: entrypoint is open when no guard is installed");
- check(policy.authorize_channel(null) == null, "SA-6: channel is open when no guard is installed");
- check(policy.authorize_channel_subscriptions(null) == null, "SA-6: channel subscriptions are open when no guard is installed");
- var denied = new HttpStringResult("login required", StatusCode.UNAUTHORIZED);
- policy.guard_channel(ctx => denied);
- var decision = policy.authorize_channel(null);
- check(decision != null && ((!)decision).status == StatusCode.UNAUTHORIZED,
- "SA-6: an installed channel guard short-circuits with its result");
- check(policy.authorize_entrypoint(null) == null, "SA-6: the channel guard does not leak into the entrypoint");
- policy.guard_entrypoint(ctx => null);
- check(policy.authorize_entrypoint(null) == null, "SA-6: a guard returning null allows the request");
- }
- void main() {
- GObjectMapping.register_mapper<Model.ActionDto>(Model.ActionDto.get_mapper());
- var container = new Container();
- container.register_singleton<Statum.Cryptography.SigningProvider>();
- container.register_singleton<Statum.Cryptography.EncryptionProvider>();
- container.register_singleton<StateService>();
- container.register_singleton<HeldSlotResolver>();
- container.register_singleton<ChannelEndpoint>().as<ChannelService>();
- container.register_singleton<ActionRegistry>();
- container.register_singleton<TopicRegistry>();
- container.register_scoped<ErodingAction>();
- container.register_scoped<FixedAction>();
- var scope = container.create_transient_scope();
- var encryption = (Statum.Cryptography.EncryptionProvider) scope.resolve_type(typeof(Statum.Cryptography.EncryptionProvider));
- var signing = (Statum.Cryptography.SigningProvider) scope.resolve_type(typeof(Statum.Cryptography.SigningProvider));
- var states = (StateService) scope.resolve_type(typeof(StateService));
- var channel = (ChannelEndpoint) scope.resolve_type(typeof(ChannelEndpoint));
- try {
- test_scalar_public_path();
- } catch (GLib.Error e) {
- failures++;
- print("FAIL scalar public path threw: %s\n", e.message);
- }
- try {
- test_scalar_encryption_path(encryption);
- } catch (GLib.Error e) {
- failures++;
- print("FAIL scalar encryption path threw: %s\n", e.message);
- }
- try {
- test_arrays(encryption);
- } catch (GLib.Error e) {
- failures++;
- print("FAIL arrays threw: %s\n", e.message);
- }
- try {
- test_typed_action_private(scope, encryption);
- } catch (GLib.Error e) {
- failures++;
- print("FAIL typed action private threw: %s\n", e.message);
- }
- try {
- test_restore_slot_expiry(states, signing);
- } catch (GLib.Error e) {
- failures++;
- print("FAIL restore_slot expiry threw: %s\n", e.message);
- }
- try {
- test_verify_frame_signer(signing, new Statum.Cryptography.SigningProvider());
- } catch (GLib.Error e) {
- failures++;
- print("FAIL verify_frame signer threw: %s\n", e.message);
- }
- try {
- test_locksmith_restart_survival();
- } catch (GLib.Error e) {
- failures++;
- print("FAIL locksmith restart survival threw: %s\n", e.message);
- }
- try {
- test_locksmith_signer_id_wire_stability();
- } catch (GLib.Error e) {
- failures++;
- print("FAIL locksmith signer_id wire stability threw: %s\n", e.message);
- }
- try {
- test_locksmith_foreign_and_malformed_keys();
- } catch (GLib.Error e) {
- failures++;
- print("FAIL locksmith foreign/malformed keys threw: %s\n", e.message);
- }
- try {
- test_locksmith_pk_mismatch_policy();
- } catch (GLib.Error e) {
- failures++;
- print("FAIL locksmith pk-mismatch policy threw: %s\n", e.message);
- }
- try {
- test_locksmith_error_semantics();
- } catch (GLib.Error e) {
- failures++;
- print("FAIL locksmith error semantics threw: %s\n", e.message);
- }
- try {
- test_connection_caps();
- } catch (GLib.Error e) {
- failures++;
- print("FAIL connection caps threw: %s\n", e.message);
- }
- try {
- test_subscription_cap(channel);
- } catch (GLib.Error e) {
- failures++;
- print("FAIL subscription cap threw: %s\n", e.message);
- }
- try {
- test_authorization_policy();
- } catch (GLib.Error e) {
- failures++;
- print("FAIL authorization policy threw: %s\n", e.message);
- }
- print("---- %d passed, %d failed ----\n", passes, failures);
- if (failures > 0) {
- Process.exit(1);
- }
- }
- }
|