Ver código fonte

Add Locksmith wire-compat and restart-survival regression tests

Permanent tests in tests/TestMain.vala for the Locksmith-based
cryptography port (goal 44):

- Restart survival: providers rebuilt from the same raw key bytes
  (the web-config.json static-key path, generated through Locksmith
  KeyPair.generate rather than shelling out to statum-genkeys)
  verify frames and unseal private blobs authored before the
  restart; signer_id is identical across instances and signatures
  decode to 64 bytes.
- signer_id wire stability: Locksmith to_base64 and GLib Base64
  agree on the raw 32-byte public key, for both ephemeral and
  configured providers, so frames held by existing clients keep
  naming the same signer.
- Foreign and malformed verification keys: an unrelated third key
  verifies nothing and a wrong-length (31-byte) key returns false
  without crashing or emitting GLib criticals.
- with_keys pk-mismatch policy: signer_id is the pk derived from
  the secret, not the supplied one; signatures verify against the
  derived pk and fail against the wrong one.
- Error semantics: cross-namespace reads throw NAMESPACE_MISMATCH,
  garbage blobs throw DECRYPTION_FAILED, and a byte-flipped blob
  throws EncryptionError rather than crashing.

All existing tests keep passing unchanged (92 passed, 0 failed).
saddle-ClankerSaddle 1 dia atrás
pai
commit
ea14fbbb74
1 arquivos alterados com 226 adições e 0 exclusões
  1. 226 0
      tests/TestMain.vala

+ 226 - 0
tests/TestMain.vala

@@ -300,6 +300,202 @@ namespace Statum.Tests {
         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);
@@ -416,6 +612,36 @@ namespace Statum.Tests {
             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) {