|
|
@@ -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) {
|