diff --git a/backward-compatibility/data/0_11_0/kms/kms_fhe_key_handles.bcode b/backward-compatibility/data/0_11_0/kms/kms_fhe_key_handles.bcode index 5d41e2eafc..c1ba2d0934 100644 --- a/backward-compatibility/data/0_11_0/kms/kms_fhe_key_handles.bcode +++ b/backward-compatibility/data/0_11_0/kms/kms_fhe_key_handles.bcode @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8d74ed16cbbb34018f8cc03b234818f7d2fd0b6255b47fb09db0d9af8845343 +oid sha256:8db11b1373de6d643aea6dc9117eb41ddb3f24671290b58e7a38368ec6d202bf size 11428 diff --git a/backward-compatibility/data/0_11_1/kms/kms_fhe_key_handles.bcode b/backward-compatibility/data/0_11_1/kms/kms_fhe_key_handles.bcode index 2859334bb1..e30c1ff159 100644 --- a/backward-compatibility/data/0_11_1/kms/kms_fhe_key_handles.bcode +++ b/backward-compatibility/data/0_11_1/kms/kms_fhe_key_handles.bcode @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78dcf61f269be47abe065d2f0b249ad076adf613284f90b9f1163f075133b6fb +oid sha256:5ab1ea78f19d679845dbea16d39dc5d02311736a66efc7dc86284eddfec94180 size 11199 diff --git a/backward-compatibility/data/0_13_0/kms/auxiliary_key_gen_metadata/legacy_key_gen_metadata.bcode b/backward-compatibility/data/0_13_0/kms/auxiliary_key_gen_metadata/legacy_key_gen_metadata.bcode index d761bf957d..3e915b7200 100644 --- a/backward-compatibility/data/0_13_0/kms/auxiliary_key_gen_metadata/legacy_key_gen_metadata.bcode +++ b/backward-compatibility/data/0_13_0/kms/auxiliary_key_gen_metadata/legacy_key_gen_metadata.bcode @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28e48be8b487d1008bb0c3231e1c04b7632516cefb43209050f354d08e557531 +oid sha256:dcaa5ec92d837668257440f038409da4cfbab18d2f2f700f553386ae7b1802e5 size 468 diff --git a/backward-compatibility/data/0_13_0/kms/kms_fhe_key_handles.bcode b/backward-compatibility/data/0_13_0/kms/kms_fhe_key_handles.bcode index 01f8f8a85c..501fb29423 100644 --- a/backward-compatibility/data/0_13_0/kms/kms_fhe_key_handles.bcode +++ b/backward-compatibility/data/0_13_0/kms/kms_fhe_key_handles.bcode @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2190f3e6afd35d3c1da4fe9e058d90c4b6e25febe03de181343db0170465d2b0 +oid sha256:56a6f47fe169b963fdc1b33bc3a74245d66832b22f62261679bee85a0dc5013e size 11208 diff --git a/backward-compatibility/data/0_13_10/kms/auxiliary_key_gen_metadata/legacy_key_gen_metadata.bcode b/backward-compatibility/data/0_13_10/kms/auxiliary_key_gen_metadata/legacy_key_gen_metadata.bcode index 3e915b7200..d761bf957d 100644 --- a/backward-compatibility/data/0_13_10/kms/auxiliary_key_gen_metadata/legacy_key_gen_metadata.bcode +++ b/backward-compatibility/data/0_13_10/kms/auxiliary_key_gen_metadata/legacy_key_gen_metadata.bcode @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dcaa5ec92d837668257440f038409da4cfbab18d2f2f700f553386ae7b1802e5 +oid sha256:28e48be8b487d1008bb0c3231e1c04b7632516cefb43209050f354d08e557531 size 468 diff --git a/backward-compatibility/data/0_13_10/kms/key_gen_metadata.bcode b/backward-compatibility/data/0_13_10/kms/key_gen_metadata.bcode index c08bd87f6f..aff78c52fe 100644 --- a/backward-compatibility/data/0_13_10/kms/key_gen_metadata.bcode +++ b/backward-compatibility/data/0_13_10/kms/key_gen_metadata.bcode @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62f8b355d53250052ce835b22a04b5911c2b196013e00fb446baf87927584134 +oid sha256:d11d0c555157f483446fd64c71c694019f4ffff92c8604ace43b8a828d952a43 size 245 diff --git a/backward-compatibility/data/0_13_10/kms/kms_fhe_key_handles.bcode b/backward-compatibility/data/0_13_10/kms/kms_fhe_key_handles.bcode index 501fb29423..01f8f8a85c 100644 --- a/backward-compatibility/data/0_13_10/kms/kms_fhe_key_handles.bcode +++ b/backward-compatibility/data/0_13_10/kms/kms_fhe_key_handles.bcode @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56a6f47fe169b963fdc1b33bc3a74245d66832b22f62261679bee85a0dc5013e +oid sha256:2190f3e6afd35d3c1da4fe9e058d90c4b6e25febe03de181343db0170465d2b0 size 11208 diff --git a/backward-compatibility/data/0_13_20/kms/auxiliary_key_gen_metadata/legacy_key_gen_metadata.bcode b/backward-compatibility/data/0_13_20/kms/auxiliary_key_gen_metadata/legacy_key_gen_metadata.bcode index 3e915b7200..d761bf957d 100644 --- a/backward-compatibility/data/0_13_20/kms/auxiliary_key_gen_metadata/legacy_key_gen_metadata.bcode +++ b/backward-compatibility/data/0_13_20/kms/auxiliary_key_gen_metadata/legacy_key_gen_metadata.bcode @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dcaa5ec92d837668257440f038409da4cfbab18d2f2f700f553386ae7b1802e5 +oid sha256:28e48be8b487d1008bb0c3231e1c04b7632516cefb43209050f354d08e557531 size 468 diff --git a/backward-compatibility/data/0_13_20/kms/internal_custodian_setup_message.bcode b/backward-compatibility/data/0_13_20/kms/internal_custodian_setup_message.bcode index ac04027395..9a484fe4ed 100644 --- a/backward-compatibility/data/0_13_20/kms/internal_custodian_setup_message.bcode +++ b/backward-compatibility/data/0_13_20/kms/internal_custodian_setup_message.bcode @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0bd33249da40b616bbbc1f43cfb777c37b33ba755ad0c7906c29149ad18b8507 +oid sha256:be7677719379ec60b91fab6c66c273907af82f09f032b8eb7b8938615f6db97f size 992 diff --git a/backward-compatibility/data/0_13_20/kms/key_gen_metadata_with_extra_data.bcode b/backward-compatibility/data/0_13_20/kms/key_gen_metadata_with_extra_data.bcode index 0a44396d4c..a35ae27c47 100644 --- a/backward-compatibility/data/0_13_20/kms/key_gen_metadata_with_extra_data.bcode +++ b/backward-compatibility/data/0_13_20/kms/key_gen_metadata_with_extra_data.bcode @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac0ded3ade3242ce472ecb8ff691834667b04d6c648f0535ff1028e627449ee2 +oid sha256:11575ae3f8530dde09f753f69c754bde1a07d7185290bf0725545675e877ea8f size 258 diff --git a/backward-compatibility/data/0_13_20/kms/kms_fhe_key_handles.bcode b/backward-compatibility/data/0_13_20/kms/kms_fhe_key_handles.bcode index a509ba13bc..397b91584f 100644 --- a/backward-compatibility/data/0_13_20/kms/kms_fhe_key_handles.bcode +++ b/backward-compatibility/data/0_13_20/kms/kms_fhe_key_handles.bcode @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66109cc83c96100d35c06229016feaa517d71f30a2764d03fc1cd95c5abe7866 +oid sha256:556168b309d817cbcdd0a10554916546a09b59847b250d1d4b56c52a882db7c4 size 11318 diff --git a/backward-compatibility/data/0_14_0/kms/auxiliary_key_gen_metadata/legacy_key_gen_metadata.bcode b/backward-compatibility/data/0_14_0/kms/auxiliary_key_gen_metadata/legacy_key_gen_metadata.bcode index 3e915b7200..d761bf957d 100644 --- a/backward-compatibility/data/0_14_0/kms/auxiliary_key_gen_metadata/legacy_key_gen_metadata.bcode +++ b/backward-compatibility/data/0_14_0/kms/auxiliary_key_gen_metadata/legacy_key_gen_metadata.bcode @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dcaa5ec92d837668257440f038409da4cfbab18d2f2f700f553386ae7b1802e5 +oid sha256:28e48be8b487d1008bb0c3231e1c04b7632516cefb43209050f354d08e557531 size 468 diff --git a/backward-compatibility/data/0_14_0/kms/internal_custodian_recovery_output.bcode b/backward-compatibility/data/0_14_0/kms/internal_custodian_recovery_output.bcode index 38cdf921d8..836c2129e3 100644 --- a/backward-compatibility/data/0_14_0/kms/internal_custodian_recovery_output.bcode +++ b/backward-compatibility/data/0_14_0/kms/internal_custodian_recovery_output.bcode @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5a79b59689143dadc62d606abd3fe5921a14fc7971d620dd246645d8368374f -size 221 +oid sha256:b1e0264ec788ad7273ce2698c2016806d90fa49a45578ac17bcdc6183243f283 +size 144 diff --git a/backward-compatibility/data/0_14_0/kms/internal_custodian_setup_message.bcode b/backward-compatibility/data/0_14_0/kms/internal_custodian_setup_message.bcode index a100f85525..80db2e7280 100644 --- a/backward-compatibility/data/0_14_0/kms/internal_custodian_setup_message.bcode +++ b/backward-compatibility/data/0_14_0/kms/internal_custodian_setup_message.bcode @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8a85b0ec533efe1a86a3ded7fe86eae2534e9da826fd28640fa516053355513 +oid sha256:82dab1647344d54f7a36f6973db7a51dd9f25626693d4559f3c8cfc6444636c1 size 992 diff --git a/backward-compatibility/data/0_14_0/kms/internal_recovery_request.bcode b/backward-compatibility/data/0_14_0/kms/internal_recovery_request.bcode index d3e042756e..a974d5707c 100644 --- a/backward-compatibility/data/0_14_0/kms/internal_recovery_request.bcode +++ b/backward-compatibility/data/0_14_0/kms/internal_recovery_request.bcode @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:131b45495e950de30f57d751f78f662d939fb50fa91265d5aaca4365b92b9bef -size 1665 +oid sha256:0a490c3e5d9bf53913d77bb73800955a5b145d422be8e6fc24def7f6585a93c0 +size 1588 diff --git a/backward-compatibility/data/0_14_0/kms/key_gen_metadata.bcode b/backward-compatibility/data/0_14_0/kms/key_gen_metadata.bcode index 5d440827de..8b5c1b78a1 100644 --- a/backward-compatibility/data/0_14_0/kms/key_gen_metadata.bcode +++ b/backward-compatibility/data/0_14_0/kms/key_gen_metadata.bcode @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66e3bc33dbc047674bf0163868f3d73ad86962c6fb07c4bf1db7c704ea4722b0 +oid sha256:216516c2d9475a4e2bb098b47a7b0c00242bfcc7b0f6b9b48ffc031441fd6817 size 246 diff --git a/backward-compatibility/data/0_14_0/kms/key_gen_metadata_with_extra_data.bcode b/backward-compatibility/data/0_14_0/kms/key_gen_metadata_with_extra_data.bcode index 0a44396d4c..a35ae27c47 100644 --- a/backward-compatibility/data/0_14_0/kms/key_gen_metadata_with_extra_data.bcode +++ b/backward-compatibility/data/0_14_0/kms/key_gen_metadata_with_extra_data.bcode @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac0ded3ade3242ce472ecb8ff691834667b04d6c648f0535ff1028e627449ee2 +oid sha256:11575ae3f8530dde09f753f69c754bde1a07d7185290bf0725545675e877ea8f size 258 diff --git a/backward-compatibility/data/0_14_0/kms/operator_backup_output.bcode b/backward-compatibility/data/0_14_0/kms/operator_backup_output.bcode index 7c24373275..acf33d1721 100644 --- a/backward-compatibility/data/0_14_0/kms/operator_backup_output.bcode +++ b/backward-compatibility/data/0_14_0/kms/operator_backup_output.bcode @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b9622e8a05c3c04d5f8d74a5a9f9460b6be883f061ef3e9e49252553f2e50b7b -size 1260 +oid sha256:e6bcc99dc0d9dbfa8e2a36acfcc8c61c602ff5efb4c6f1e9212c2a73a5e82390 +size 1292 diff --git a/backward-compatibility/data/0_14_0/kms/recovery_material.bcode b/backward-compatibility/data/0_14_0/kms/recovery_material.bcode index 6365f46725..e0992faa43 100644 --- a/backward-compatibility/data/0_14_0/kms/recovery_material.bcode +++ b/backward-compatibility/data/0_14_0/kms/recovery_material.bcode @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c3a25b30266ea83f46f55e42aa155f7216e069f9cf79613c4a06163770c1b24 +oid sha256:dab6df5aab012710bf4a270a23739db534d74aa51f402c47e66a738a153a2ea9 size 6482 diff --git a/backward-compatibility/generate-v0.14.0/Cargo.lock b/backward-compatibility/generate-v0.14.0/Cargo.lock index 4d70323621..8e82b3ac3b 100644 --- a/backward-compatibility/generate-v0.14.0/Cargo.lock +++ b/backward-compatibility/generate-v0.14.0/Cargo.lock @@ -1682,7 +1682,7 @@ checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bc2wrap" version = "2.0.1" -source = "git+https://github.com/zama-ai/kms.git?rev=3eb3862#3eb3862e81502efc020ce718d81ddd90b2601320" +source = "git+https://github.com/zama-ai/kms.git?rev=865e4091da5d663dec82c62cca3f2a0aad83939a#865e4091da5d663dec82c62cca3f2a0aad83939a" dependencies = [ "bincode 2.0.1", "serde", @@ -2781,8 +2781,8 @@ dependencies = [ [[package]] name = "error-utils" -version = "0.13.20-0" -source = "git+https://github.com/zama-ai/kms.git?rev=3eb3862#3eb3862e81502efc020ce718d81ddd90b2601320" +version = "0.14.0-0" +source = "git+https://github.com/zama-ai/kms.git?rev=865e4091da5d663dec82c62cca3f2a0aad83939a#865e4091da5d663dec82c62cca3f2a0aad83939a" dependencies = [ "anyhow", "tracing", @@ -3805,8 +3805,8 @@ dependencies = [ [[package]] name = "kms" -version = "0.13.20-0" -source = "git+https://github.com/zama-ai/kms.git?rev=3eb3862#3eb3862e81502efc020ce718d81ddd90b2601320" +version = "0.14.0-0" +source = "git+https://github.com/zama-ai/kms.git?rev=865e4091da5d663dec82c62cca3f2a0aad83939a#865e4091da5d663dec82c62cca3f2a0aad83939a" dependencies = [ "aes", "aes-gcm", @@ -3892,8 +3892,8 @@ dependencies = [ [[package]] name = "kms-grpc" -version = "0.13.20-0" -source = "git+https://github.com/zama-ai/kms.git?rev=3eb3862#3eb3862e81502efc020ce718d81ddd90b2601320" +version = "0.14.0-0" +source = "git+https://github.com/zama-ai/kms.git?rev=865e4091da5d663dec82c62cca3f2a0aad83939a#865e4091da5d663dec82c62cca3f2a0aad83939a" dependencies = [ "alloy-dyn-abi", "alloy-primitives", @@ -4337,8 +4337,8 @@ dependencies = [ [[package]] name = "observability" -version = "0.13.20-0" -source = "git+https://github.com/zama-ai/kms.git?rev=3eb3862#3eb3862e81502efc020ce718d81ddd90b2601320" +version = "0.14.0-0" +source = "git+https://github.com/zama-ai/kms.git?rev=865e4091da5d663dec82c62cca3f2a0aad83939a#865e4091da5d663dec82c62cca3f2a0aad83939a" dependencies = [ "anyhow", "axum", @@ -6480,8 +6480,8 @@ checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "test-utils" -version = "0.13.20-0" -source = "git+https://github.com/zama-ai/kms.git?rev=3eb3862#3eb3862e81502efc020ce718d81ddd90b2601320" +version = "0.14.0-0" +source = "git+https://github.com/zama-ai/kms.git?rev=865e4091da5d663dec82c62cca3f2a0aad83939a#865e4091da5d663dec82c62cca3f2a0aad83939a" dependencies = [ "anyhow", "bc2wrap", @@ -6655,8 +6655,8 @@ dependencies = [ [[package]] name = "thread-handles" -version = "0.13.20-0" -source = "git+https://github.com/zama-ai/kms.git?rev=3eb3862#3eb3862e81502efc020ce718d81ddd90b2601320" +version = "0.14.0-0" +source = "git+https://github.com/zama-ai/kms.git?rev=865e4091da5d663dec82c62cca3f2a0aad83939a#865e4091da5d663dec82c62cca3f2a0aad83939a" dependencies = [ "anyhow", "error-utils", @@ -6686,8 +6686,8 @@ dependencies = [ [[package]] name = "threshold-algebra" -version = "0.13.20-0" -source = "git+https://github.com/zama-ai/kms.git?rev=3eb3862#3eb3862e81502efc020ce718d81ddd90b2601320" +version = "0.14.0-0" +source = "git+https://github.com/zama-ai/kms.git?rev=865e4091da5d663dec82c62cca3f2a0aad83939a#865e4091da5d663dec82c62cca3f2a0aad83939a" dependencies = [ "anyhow", "error-utils", @@ -6708,8 +6708,8 @@ dependencies = [ [[package]] name = "threshold-execution" -version = "0.13.20-0" -source = "git+https://github.com/zama-ai/kms.git?rev=3eb3862#3eb3862e81502efc020ce718d81ddd90b2601320" +version = "0.14.0-0" +source = "git+https://github.com/zama-ai/kms.git?rev=865e4091da5d663dec82c62cca3f2a0aad83939a#865e4091da5d663dec82c62cca3f2a0aad83939a" dependencies = [ "aes", "aes-prng", @@ -6754,8 +6754,8 @@ dependencies = [ [[package]] name = "threshold-hashing" -version = "0.13.20-0" -source = "git+https://github.com/zama-ai/kms.git?rev=3eb3862#3eb3862e81502efc020ce718d81ddd90b2601320" +version = "0.14.0-0" +source = "git+https://github.com/zama-ai/kms.git?rev=865e4091da5d663dec82c62cca3f2a0aad83939a#865e4091da5d663dec82c62cca3f2a0aad83939a" dependencies = [ "anyhow", "bc2wrap", @@ -6765,8 +6765,8 @@ dependencies = [ [[package]] name = "threshold-networking" -version = "0.13.20-0" -source = "git+https://github.com/zama-ai/kms.git?rev=3eb3862#3eb3862e81502efc020ce718d81ddd90b2601320" +version = "0.14.0-0" +source = "git+https://github.com/zama-ai/kms.git?rev=865e4091da5d663dec82c62cca3f2a0aad83939a#865e4091da5d663dec82c62cca3f2a0aad83939a" dependencies = [ "anyhow", "async-trait", @@ -6800,8 +6800,8 @@ dependencies = [ [[package]] name = "threshold-types" -version = "0.13.20-0" -source = "git+https://github.com/zama-ai/kms.git?rev=3eb3862#3eb3862e81502efc020ce718d81ddd90b2601320" +version = "0.14.0-0" +source = "git+https://github.com/zama-ai/kms.git?rev=865e4091da5d663dec82c62cca3f2a0aad83939a#865e4091da5d663dec82c62cca3f2a0aad83939a" dependencies = [ "anyhow", "async-trait", diff --git a/backward-compatibility/generate-v0.14.0/Cargo.toml b/backward-compatibility/generate-v0.14.0/Cargo.toml index f5ffe73dd1..0fe501056f 100644 --- a/backward-compatibility/generate-v0.14.0/Cargo.toml +++ b/backward-compatibility/generate-v0.14.0/Cargo.toml @@ -9,19 +9,19 @@ license = "BSD-3-Clause-Clear" [dependencies] # Import the base backward-compatibility crate for shared types and utilities backward-compatibility = { path = ".." } -bc2wrap = { git = "https://github.com/zama-ai/kms.git", package = "bc2wrap", rev = "3eb3862" } +bc2wrap = { git = "https://github.com/zama-ai/kms.git", package = "bc2wrap", rev = "865e4091da5d663dec82c62cca3f2a0aad83939a" } # NOTE: Some dependencies below are duplicated from ../Cargo.toml # This is intentional - these crates are excluded from the workspace for isolation. # Each generator may need different versions to match its target KMS version. # All dependencies below target tag v0.14.0-0 (for now just using a commit from main until tag is made). -kms_0_14_0 = { git = "https://github.com/zama-ai/kms.git", package = "kms", rev = "3eb3862"} -kms_grpc_0_14_0 = { git = "https://github.com/zama-ai/kms.git", package = "kms-grpc", rev = "3eb3862"} -algebra_0_14_0 = { git = "https://github.com/zama-ai/kms.git", package = "threshold-algebra", default-features = false, rev = "3eb3862" } -threshold_execution_0_14_0 = { git = "https://github.com/zama-ai/kms.git", package = "threshold-execution", default-features = false, rev = "3eb3862", features = ["testing"] } -threshold_networking_0_14_0 = { git = "https://github.com/zama-ai/kms.git", package = "threshold-networking", default-features = false, rev = "3eb3862" } -threshold_types_0_14_0 = { git = "https://github.com/zama-ai/kms.git", package = "threshold-types", default-features = false, rev = "3eb3862" } +kms_0_14_0 = { git = "https://github.com/zama-ai/kms.git", package = "kms", rev = "865e4091da5d663dec82c62cca3f2a0aad83939a"} +kms_grpc_0_14_0 = { git = "https://github.com/zama-ai/kms.git", package = "kms-grpc", rev = "865e4091da5d663dec82c62cca3f2a0aad83939a"} +algebra_0_14_0 = { git = "https://github.com/zama-ai/kms.git", package = "threshold-algebra", default-features = false, rev = "865e4091da5d663dec82c62cca3f2a0aad83939a" } +threshold_execution_0_14_0 = { git = "https://github.com/zama-ai/kms.git", package = "threshold-execution", default-features = false, rev = "865e4091da5d663dec82c62cca3f2a0aad83939a", features = ["testing"] } +threshold_networking_0_14_0 = { git = "https://github.com/zama-ai/kms.git", package = "threshold-networking", default-features = false, rev = "865e4091da5d663dec82c62cca3f2a0aad83939a" } +threshold_types_0_14_0 = { git = "https://github.com/zama-ai/kms.git", package = "threshold-types", default-features = false, rev = "865e4091da5d663dec82c62cca3f2a0aad83939a" } #error-utils_0_14_0 = { path = "./core/error-utils", default-features = false } #thread-handles_0_14_0 = { path = "./core/thread-handles" } diff --git a/backward-compatibility/generate-v0.14.0/src/data_0_14.rs b/backward-compatibility/generate-v0.14.0/src/data_0_14.rs index b9199e3157..a15f439b12 100644 --- a/backward-compatibility/generate-v0.14.0/src/data_0_14.rs +++ b/backward-compatibility/generate-v0.14.0/src/data_0_14.rs @@ -1033,6 +1033,7 @@ impl KmsV0_14_0 { let (custodian_pk, _) = gen_sig_keys(&mut rng); let backup_material = BackupMaterial { backup_id, + mpc_context_id: kms_grpc_0_14_0::ContextId::from_bytes([9u8; 32]), custodian_pk, custodian_role: cus_role, operator_pk: operator_pk.clone(), @@ -1109,10 +1110,8 @@ impl KmsV0_14_0 { fn gen_internal_recovery_request(dir: &PathBuf) -> TestMetadataKMS { let mut rng = AesRng::seed_from_u64(INTERNAL_RECOVERY_REQUEST_TEST.state); - let backup_id: RequestId = RequestId::new_random(&mut rng); let mut encryption = Encryption::new(PkeSchemeType::MlKem512, &mut rng); let (_dec_key, enc_key) = encryption.keygen().unwrap(); - let (verf_key, _) = gen_sig_keys(&mut rng); let mut cts = BTreeMap::new(); for role_j in 1..=INTERNAL_RECOVERY_REQUEST_TEST.amount { let cur_role = Role::indexed_from_one(role_j as usize); @@ -1125,8 +1124,7 @@ impl KmsV0_14_0 { }; cts.insert(cur_role, InnerOperatorBackupOutput { signcryption }); } - let recovery_material = - InternalRecoveryRequest::new(enc_key, cts, backup_id, verf_key).unwrap(); + let recovery_material = InternalRecoveryRequest::new(enc_key, cts).unwrap(); store_versioned_test!( &recovery_material, dir, @@ -1380,7 +1378,6 @@ impl KmsV0_14_0 { fn gen_internal_cus_rec_out(dir: &PathBuf) -> TestMetadataKMS { let mut rng = AesRng::seed_from_u64(INTERNAL_CUS_REC_OUT_TEST.state); - let (operator_verification_key, _) = gen_sig_keys(&mut rng); let mut buf = [0u8; 100]; rng.fill_bytes(&mut buf); let signcryption = UnifiedSigncryption { @@ -1391,8 +1388,6 @@ impl KmsV0_14_0 { let icro = InternalCustodianRecoveryOutput { signcryption, custodian_role: Role::indexed_from_one(2), - operator_verification_key, - mpc_context_id: RequestId::from_bytes(INTERNAL_CUS_REC_OUT_TEST.mpc_context_id), }; store_versioned_test!(&icro, dir, &INTERNAL_CUS_REC_OUT_TEST.test_filename); TestMetadataKMS::InternalCustodianRecoveryOutput(INTERNAL_CUS_REC_OUT_TEST) @@ -1439,6 +1434,7 @@ impl KmsV0_14_0 { &mut rng, &OPERATOR_BACKUP_OUTPUT_TEST.plaintext, RequestId::from_bytes(OPERATOR_BACKUP_OUTPUT_TEST.backup_id), + kms_grpc_0_14_0::ContextId::from_bytes([9u8; 32]), ) .unwrap() .ct_shares[&Role::indexed_from_one(1)]; diff --git a/core-client/src/backup.rs b/core-client/src/backup.rs index c1b59fbe21..45f814bbd6 100644 --- a/core-client/src/backup.rs +++ b/core-client/src/backup.rs @@ -10,17 +10,14 @@ use kms_grpc::{ }, kms_service::v1::core_service_endpoint_client::CoreServiceEndpointClient, }; -use kms_lib::{ - backup::{ - custodian::{InternalCustodianRecoveryOutput, InternalCustodianSetupMessage}, - operator::InternalRecoveryRequest, - }, - cryptography::internal_crypto_types::LegacySerialization, +use kms_lib::backup::{ + custodian::{InternalCustodianRecoveryOutput, InternalCustodianSetupMessage}, + operator::InternalRecoveryRequest, }; use tokio::task::JoinSet; use tonic::transport::Channel; -use crate::{CoreClientConfig, CoreConf, s3_operations::fetch_kms_verification_keys}; +use crate::CoreConf; pub(crate) async fn do_get_operator_pub_keys( core_endpoints: &HashMap>, @@ -131,54 +128,36 @@ pub(crate) async fn do_custodian_recovery_init( Ok(res.into_iter().map(|(_, v)| v).collect()) } +/// Send every custodian recovery output to every operator. pub(crate) async fn do_custodian_backup_recovery( core_endpoints: &HashMap>, - sim_conf: &CoreClientConfig, custodian_context_id: RequestId, custodian_recovery_outputs: Vec, ) -> anyhow::Result<()> { - let pivot_mpc_context_id = custodian_recovery_outputs - .first() - .ok_or_else(|| anyhow::anyhow!("At least one custodian recovery output is required"))? - .mpc_context_id; - if custodian_recovery_outputs - .iter() - .any(|output| output.mpc_context_id != pivot_mpc_context_id) - { - anyhow::bail!("All custodian recovery outputs must have the same MPC context ID"); + if custodian_recovery_outputs.is_empty() { + anyhow::bail!("At least one custodian recovery output is required"); } - // fetch the public keys of operators - let verf_keys = fetch_kms_verification_keys(sim_conf).await?; + let proto_outputs: Vec = custodian_recovery_outputs + .into_iter() + .map(|out| CustodianRecoveryOutput { + backup_output: Some(OperatorBackupOutput { + signcryption: out.signcryption.payload, + pke_type: out.signcryption.pke_type as i32, + signing_type: out.signcryption.signing_type as i32, + }), + custodian_role: out.custodian_role.one_based() as u64, + }) + .collect(); + let mut req_tasks = JoinSet::new(); - // we should change the key in the [core_endpoints] hashmap to be the verification key - for (core_conf, ce) in core_endpoints.iter() { + for ce in core_endpoints.values() { let mut cur_client = ce.clone(); - // We assume the core client endpoints are ordered by the server identity - let mut cur_recoveries = Vec::new(); - for cur_recover in custodian_recovery_outputs.iter() { - // Find the recoveries designated for the correct server - let verf_key = &verf_keys[&core_conf.party_id]; - - if &cur_recover.operator_verification_key == verf_key { - cur_recoveries.push(CustodianRecoveryOutput { - backup_output: Some(OperatorBackupOutput { - signcryption: cur_recover.signcryption.payload.clone(), - pke_type: cur_recover.signcryption.pke_type as i32, - signing_type: cur_recover.signcryption.signing_type as i32, - }), - custodian_role: cur_recover.custodian_role.one_based() as u64, - operator_verification_key: cur_recover - .operator_verification_key - .to_legacy_bytes()?, - mpc_context_id: Some(pivot_mpc_context_id.into()), - }); - } - } + let outputs = proto_outputs.clone(); req_tasks.spawn(async move { cur_client .custodian_backup_recovery(tonic::Request::new(CustodianRecoveryRequest { custodian_context_id: Some(custodian_context_id.into()), - custodian_recovery_outputs: cur_recoveries, + custodian_recovery_outputs: outputs, })) .await }); diff --git a/core-client/src/lib.rs b/core-client/src/lib.rs index cc0e25fe20..ed37d4e41c 100644 --- a/core-client/src/lib.rs +++ b/core-client/src/lib.rs @@ -2195,20 +2195,13 @@ pub async fn execute_cmd( do_custodian_recovery_init(&core_endpoints_req, *overwrite_ephemeral_key).await?; assert_eq!(res.len(), operator_recovery_resp_paths.len()); - let backup_id = res[0].backup_id(); - // no ordering of results and paths here for (cur_res, cur_path) in res.into_iter().zip(operator_recovery_resp_paths) { - assert_eq!( - backup_id, - cur_res.backup_id(), - "All recovery responses must belong to the same backup ID" - ); safe_write_element_versioned(cur_path, &cur_res).await?; } vec![( - Some(backup_id), + None, "custodian recovery init queried and recovery request stored".to_string(), )] } @@ -2225,7 +2218,6 @@ pub async fn execute_cmd( } do_custodian_backup_recovery( &core_endpoints_req, - &cc_conf, *custodian_context_id, custodian_outputs, ) diff --git a/core-client/src/s3_operations.rs b/core-client/src/s3_operations.rs index 18dcac48a9..0dc9624e15 100644 --- a/core-client/src/s3_operations.rs +++ b/core-client/src/s3_operations.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "testing")] use std::collections::HashMap; use std::{collections::HashSet, path::Path}; @@ -8,11 +9,15 @@ use kms_grpc::rpc_types::PrivDataType; use kms_grpc::rpc_types::PubDataType; #[cfg(feature = "testing")] use kms_lib::cryptography::signatures::PrivateSigKey; +#[cfg(feature = "testing")] use kms_lib::vault::storage::file::FileStorage; +#[cfg(feature = "testing")] use kms_lib::vault::storage::s3::{ S3Storage, build_anonymous_s3_client, find_region_from_s3_url, split_url, }; +#[cfg(feature = "testing")] use kms_lib::vault::storage::{StorageReader, StorageType}; +#[cfg(feature = "testing")] use kms_lib::{consts::SIGNING_KEY_ID, cryptography::signatures::PublicSigKey}; /// Fetch all remote elements and store them locally for the core client @@ -83,6 +88,7 @@ pub async fn fetch_public_elements( } /// This fetches the KMS public verification key from S3 for all the cores. +#[cfg(feature = "testing")] pub(crate) async fn fetch_kms_verification_keys( sim_conf: &CoreClientConfig, ) -> anyhow::Result> { diff --git a/core-client/tests/integration/integration_test.rs b/core-client/tests/integration/integration_test.rs index c7513d7f4d..e6f31ece5d 100644 --- a/core-client/tests/integration/integration_test.rs +++ b/core-client/tests/integration/integration_test.rs @@ -1829,12 +1829,12 @@ fn extract_seed_phrase(out: Output) -> String { .to_string() } -/// Native implementation: Initialize custodian backup using isolated config +/// Native implementation: Initialize custodian backup using isolated config. async fn custodian_backup_init( config_path: &Path, test_path: &Path, operator_recovery_resp_paths: Vec, -) -> String { +) { let config = cmd_config( config_path, CCCommand::CustodianRecoveryInit(RecoveryInitParameters { @@ -1843,10 +1843,10 @@ async fn custodian_backup_init( }), 200, ); - run_cmd(&config, test_path, "backup init") + let results = execute_cmd(&config, test_path) .await - .unwrap() - .to_string() + .expect("backup init: execute_cmd failed"); + assert_eq!(results.len(), 1, "backup init: expected 1 result"); } /// Native implementation: Re-encrypt custodian backups using kms-custodian binary directly @@ -2199,20 +2199,19 @@ async fn test_centralized_custodian_backup() -> Result<()> { create_dir_all(operator_recovery_resp_path.parent().unwrap())?; // Initialize custodian backup - let init_backup_id = custodian_backup_init( + custodian_backup_init( &config_path, temp_path, vec![operator_recovery_resp_path.clone()], ) .await; - assert_eq!(cus_backup_id, init_backup_id); // Re-encrypt with custodian keys let recovery_output_paths = custodian_reencrypt( temp_path, 1, amount_custodians, - init_backup_id.parse()?, + RequestId::from_str(&cus_backup_id)?, *DEFAULT_MPC_CONTEXT, &seeds, &[operator_recovery_resp_path], @@ -2596,20 +2595,19 @@ async fn test_threshold_custodian_backup() -> Result<()> { } // Initialize custodian backup - let init_backup_id = custodian_backup_init( + custodian_backup_init( &config_path, temp_path, operator_recovery_resp_paths.clone(), ) .await; - assert_eq!(cus_backup_id, init_backup_id); // Re-encrypt with custodian keys let recovery_output_paths = custodian_reencrypt( temp_path, amount_operators, amount_custodians, - init_backup_id.parse()?, + RequestId::from_str(&cus_backup_id)?, *DEFAULT_MPC_CONTEXT, &seeds, &operator_recovery_resp_paths, diff --git a/core/grpc/proto/kms.v1.proto b/core/grpc/proto/kms.v1.proto index bb1336cc68..b8520fc582 100644 --- a/core/grpc/proto/kms.v1.proto +++ b/core/grpc/proto/kms.v1.proto @@ -659,13 +659,11 @@ message CustodianRecoveryInitRequest { bool overwrite_ephemeral_key = 1; } -// The data constructed by a single custodian to help a single operator recover their private master decryption key +// The data constructed by a single custodian to help a single operator recover their private master decryption key. message CustodianRecoveryOutput { OperatorBackupOutput backup_output = 1; - // The 1-indexed role of the custodian + // The 1-indexed role of the custodian (lookup hint; authenticated copy lives in the signcrypted BackupMaterial) uint64 custodian_role = 2; - bytes operator_verification_key = 3; - RequestId mpc_context_id = 4; } // The recovery request which contains the needed recovery data from all the custodians, @@ -697,10 +695,6 @@ message RecoveryRequest { bytes ephem_op_enc_key = 1; /// The ciphertexts that are the backup. Indexed by the custodian role. map cts = 2; - /// The request ID under which the backup was created. - RequestId backup_id = 3; - /// Verification key of the operator. - bytes operator_verification_key = 4; } // Response containing available key material information diff --git a/core/service/src/backup/custodian.rs b/core/service/src/backup/custodian.rs index 8ca2b026c7..e9cda9f2d9 100644 --- a/core/service/src/backup/custodian.rs +++ b/core/service/src/backup/custodian.rs @@ -37,13 +37,13 @@ pub enum InternalCustodianRecoveryOutputVersioned { } /// This is the message that a custodian sends to an operator after starting recovery. +/// +/// The payload of the signcryption is a `BackupMaterial` that contains the decrypted backup share for an operator. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Versionize)] #[versionize(InternalCustodianRecoveryOutputVersioned)] pub struct InternalCustodianRecoveryOutput { pub signcryption: UnifiedSigncryption, pub custodian_role: Role, - pub operator_verification_key: PublicSigKey, - pub mpc_context_id: RequestId, } impl Named for InternalCustodianRecoveryOutput { @@ -59,22 +59,9 @@ impl TryFrom for InternalCustodianRecoveryOutput { "Invalid custodian role in CustodianRecoveryOutput" )); } - // TODO(zama-ai/kms-internal/issues/2836) - // we may change how the verification key is serialized - let verification_key: PublicSigKey = - bc2wrap::deserialize_safe(&value.operator_verification_key).map_err(|e| { - anyhow::anyhow!("Failed to deserialize operator verification key: {}", e) - })?; let backup_output = &value.backup_output.ok_or_else(|| { anyhow::anyhow!("backup output not part of the custodian recovery output") })?; - let mpc_context_id = value - .mpc_context_id - .ok_or_else(|| { - anyhow::anyhow!("MPC context ID not part of the custodian recovery output") - })? - .try_into() - .map_err(|e| anyhow::anyhow!("Failed to parse MPC context ID: {}", e))?; Ok(InternalCustodianRecoveryOutput { signcryption: UnifiedSigncryption::new( backup_output.signcryption.clone(), @@ -82,8 +69,6 @@ impl TryFrom for InternalCustodianRecoveryOutput { backup_output.signing_type().into(), ), custodian_role: Role::indexed_from_one(value.custodian_role as usize), - operator_verification_key: verification_key, - mpc_context_id, }) } } @@ -92,7 +77,6 @@ impl TryFrom for CustodianRecoveryOutput { type Error = anyhow::Error; fn try_from(value: InternalCustodianRecoveryOutput) -> Result { - let verification_key_buf = bc2wrap::serialize(&value.operator_verification_key)?; Ok(CustodianRecoveryOutput { backup_output: Some(OperatorBackupOutput { signcryption: value.signcryption.payload, @@ -100,8 +84,6 @@ impl TryFrom for CustodianRecoveryOutput { signing_type: value.signcryption.signing_type as i32, }), custodian_role: value.custodian_role.one_based() as u64, - operator_verification_key: verification_key_buf, - mpc_context_id: Some(value.mpc_context_id.into()), }) } } @@ -313,10 +295,8 @@ impl Custodian { &self, rng: &mut R, backup: &InnerOperatorBackupOutput, - mpc_context_id: RequestId, operator_verification_key: &PublicSigKey, operator_ephem_enc_key: &UnifiedPublicEncKey, - backup_id: RequestId, ) -> Result { tracing::debug!( "Verifying and re-encrypting backup for operator: {}", @@ -333,7 +313,7 @@ impl Custodian { .unsigncrypt(&DSEP_BACKUP_CUSTODIAN, &backup.signcryption) .map_err(|e| { tracing::warn!( - "Unsigncryption failed for backup {backup_id} for operator {}: {e}", + "Unsigncryption failed for operator {}: {e}", operator_verification_key.address(), ); BackupError::CustodianRecoveryError @@ -342,15 +322,30 @@ impl Custodian { "Decrypted ciphertext for operator: {}", operator_verification_key.address() ); + if !backup_material.backup_id.is_valid() { + tracing::error!( + "Invalid backup_id {} in the decrypted backup material for operator with address: {}", + backup_material.backup_id, + operator_verification_key.address() + ); + return Err(BackupError::CustodianRecoveryError); + } + if !backup_material.mpc_context_id.is_valid() { + tracing::error!( + "Invalid MPC context ID {} in the decrypted backup material for operator with address: {}", + backup_material.mpc_context_id, + operator_verification_key.address() + ); + return Err(BackupError::CustodianRecoveryError); + } // check the decrypted result - if !backup_material.matches_expected_metadata( - backup_id, + if let Err(e) = backup_material.check_expected_metadata( &self.signing_key.verf_key(), self.role, &operator_verification_key.verf_key_id(), ) { tracing::error!( - "Backup material did not match expected metadata for operator: {}", + "Backup material did not match expected metadata ({e:?}) for operator: {}", operator_verification_key.address() ); return Err(BackupError::CustodianRecoveryError); @@ -371,8 +366,6 @@ impl Custodian { Ok(InternalCustodianRecoveryOutput { signcryption, custodian_role: self.role, - operator_verification_key: operator_verification_key.clone(), - mpc_context_id, }) } diff --git a/core/service/src/backup/error.rs b/core/service/src/backup/error.rs index 65bc49df52..b9b2c46ac1 100644 --- a/core/service/src/backup/error.rs +++ b/core/service/src/backup/error.rs @@ -12,15 +12,29 @@ pub enum SetupSkipReason { } /// Why a single custodian recovery output was skipped during filtering. +/// +/// `InvalidSigncryption` covers both tampered ciphertexts and outputs addressed to a different +/// operator +/// +/// The `*InPayload` / `*Malformed` / `*Mismatch` / `CommitmentMismatch` variants fire only after a +/// successful unsigncrypt has produced a `BackupMaterial`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RecoverySkipReason { - WrongOperator, InvalidRole, MissingVerificationKey, MissingSigncryption, InvalidSigncryption, ParseError, DuplicateRole, + BackupIdMalformed, + MpcContextIdMalformed, + BackupIdMismatch, + MpcContextIdMismatch, + CustodianRoleMismatchInPayload, + CustodianKeyMismatchInPayload, + OperatorMismatchInPayload, + MissingCommitment, + CommitmentMismatch, } #[derive(Error, Debug)] diff --git a/core/service/src/backup/operator.rs b/core/service/src/backup/operator.rs index 5c64ab44d0..670bacecea 100644 --- a/core/service/src/backup/operator.rs +++ b/core/service/src/backup/operator.rs @@ -3,6 +3,10 @@ use super::{ error::{BackupError, SetupSkipReason}, secretsharing, }; +use crate::backup::{ + custodian::{InternalCustodianContext, InternalCustodianRecoveryOutput}, + error::RecoverySkipReason, +}; use crate::{ anyhow_error_and_log, consts::SAFE_SER_SIZE_LIMIT, @@ -12,16 +16,12 @@ use crate::{ Signcrypt, UnifiedSigncryption, UnifiedSigncryptionKey, UnifiedUnsigncryptionKey, Unsigncrypt, }, - engine::{base::safe_serialize_hash_element_versioned, validation::RequestIdParsingErr}, + engine::base::safe_serialize_hash_element_versioned, }; use crate::{ backup::custodian::DSEP_BACKUP_CUSTODIAN, cryptography::signatures::{internal_sign, internal_verify_sig}, }; -use crate::{ - backup::custodian::{InternalCustodianContext, InternalCustodianRecoveryOutput}, - engine::validation::parse_optional_grpc_request_id, -}; use algebra::{ galois_rings::degree_4::ResiduePolyF4Z64, sharing::{shamir::ShamirSharings, share::Share}, @@ -65,8 +65,6 @@ impl Named for InternalRecoveryRequest { pub struct InternalRecoveryRequest { ephem_op_enc_key: UnifiedPublicEncKey, cts: BTreeMap, - backup_id: RequestId, - operator_verification_key: PublicSigKey, } impl InternalRecoveryRequest { @@ -74,20 +72,11 @@ impl InternalRecoveryRequest { pub fn new( ephem_op_enc_key: UnifiedPublicEncKey, cts: BTreeMap, - backup_id: RequestId, - operator_verification_key: PublicSigKey, ) -> anyhow::Result { let res = InternalRecoveryRequest { ephem_op_enc_key, cts, - backup_id, - operator_verification_key, }; - if !backup_id.is_valid() { - return Err(anyhow_error_and_log( - "InternalRecoveryRequest has an invalid backup ID", - )); - } Ok(res) } @@ -97,10 +86,6 @@ impl InternalRecoveryRequest { custodian_role: Role, unsigncrypt_key: &UnifiedUnsigncryptionKey, ) -> anyhow::Result { - if !self.backup_id.is_valid() { - tracing::warn!("InternalRecoveryRequest has an invalid backup ID"); - return Ok(false); - } let output = match self.cts.get(&custodian_role) { Some(output) => output, None => { @@ -129,24 +114,11 @@ impl InternalRecoveryRequest { pub fn signcryptions(&self) -> HashMap { self.cts.iter().map(|(role, ct)| (*role, ct)).collect() } - - pub fn backup_id(&self) -> RequestId { - self.backup_id - } - - pub fn operator_verification_key(&self) -> &PublicSigKey { - &self.operator_verification_key - } } impl Display for InternalRecoveryRequest { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "InternalRecoveryRequest with:\n backup id: {}\n operator : {}", - self.backup_id, - self.operator_verification_key.address(), - ) + write!(f, "InternalRecoveryRequest",) } } @@ -165,15 +137,9 @@ impl TryFrom for InternalRecoveryRequest { let inner_ct: InnerOperatorBackupOutput = cur_backup_out.try_into()?; cts.insert(role, inner_ct); } - let backup_id: RequestId = - parse_optional_grpc_request_id(&value.backup_id, RequestIdParsingErr::BackupRecovery)?; - let operator_verification_key: PublicSigKey = - bc2wrap::deserialize_safe(&value.operator_verification_key)?; Ok(Self { ephem_op_enc_key, cts, - backup_id, - operator_verification_key, }) } } @@ -351,6 +317,10 @@ impl RecoveryValidationMaterial { &self.payload.custodian_context } + pub fn mpc_context(&self) -> ContextId { + self.payload.mpc_context + } + /// Validated the signature on the recovery validation material. /// This is useful after deserializing from untrusted storage such as public storage pub fn validate(&self, verf_key: &PublicSigKey) -> bool { @@ -405,46 +375,6 @@ impl Named for RecoveryValidationMaterialPayload { const NAME: &'static str = "backup::RecoveryValidationMaterialPayload"; } -#[allow(clippy::too_many_arguments)] -fn checked_decryption_deserialize( - unsign_key: &UnifiedUnsigncryptionKey, - signcryption: &UnifiedSigncryption, - commitment: &[u8], - backup_id: RequestId, - custodian_role: Role, -) -> Result>, BackupError> { - let backup_material: BackupMaterial = unsign_key - .unsigncrypt(&DSEP_BACKUP_RECOVERY, signcryption) - .map_err(|e| { - BackupError::OperatorError(format!( - "Failed to unsigncrypt backup share for custodian role {custodian_role}: {e}", - )) - })?; - // check metadata - if !backup_material.matches_expected_metadata( - backup_id, - unsign_key.sender_verf_key, - custodian_role, - unsign_key.receiver_id, - ) { - return Err(BackupError::OperatorError( - "backup metadata check failure".to_string(), - )); - } - - // check commitment - let actual_commitment = - safe_serialize_hash_element_versioned(&DSEP_BACKUP_COMMITMENT, &backup_material) - .map_err(|e| BackupError::OperatorError(e.to_string()))?; - if actual_commitment != commitment { - return Err(BackupError::OperatorError( - "backup commitment check failure".to_string(), - )); - } - - Ok(backup_material.shares) -} - #[derive(Clone, Serialize, Deserialize, VersionsDispatch)] pub enum BackupMaterialVersioned { V0(BackupMaterial), @@ -453,7 +383,10 @@ pub enum BackupMaterialVersioned { #[derive(Clone, Debug, Serialize, Deserialize, Versionize)] #[versionize(BackupMaterialVersioned)] pub struct BackupMaterial { + /// The custodian context this backup is associated with. pub backup_id: RequestId, + /// The MPC context this backup was produced under. + pub mpc_context_id: ContextId, // receiver pub custodian_pk: PublicSigKey, pub custodian_role: Role, @@ -463,38 +396,31 @@ pub struct BackupMaterial { } impl BackupMaterial { - pub fn matches_expected_metadata( + /// Verify the operator-/custodian-bound metadata fields of a freshly unsigncrypted + /// `BackupMaterial` against the expected routing parameters. + pub fn check_expected_metadata( &self, - backup_id: RequestId, custodian_verf_key: &PublicSigKey, custodian_role: Role, operator_pk_id: &[u8], - ) -> bool { - if self.backup_id != backup_id { - tracing::error!( - "backup_id mismatch: expected {} but got {}", - self.backup_id, - backup_id - ); - return false; - } + ) -> Result<(), RecoverySkipReason> { if self.custodian_role != custodian_role { tracing::error!( "custodian_role mismatch: expected {} but got {}", self.custodian_role, custodian_role ); - return false; + return Err(RecoverySkipReason::CustodianRoleMismatchInPayload); } if &self.custodian_pk != custodian_verf_key { tracing::error!("custodian_pk mismatch"); - return false; + return Err(RecoverySkipReason::CustodianKeyMismatchInPayload); } if self.operator_pk.verf_key_id() != operator_pk_id { tracing::error!("operator_pk_id mismatch"); - return false; + return Err(RecoverySkipReason::OperatorMismatchInPayload); } - true + Ok(()) } } @@ -567,6 +493,7 @@ impl Operator { rng: &mut R, secret: &[u8], backup_id: RequestId, + mpc_context_id: ContextId, ) -> Result { let sk = match &self.signing_key { None => { @@ -639,6 +566,7 @@ impl Operator { }; let backup_material = BackupMaterial { backup_id, + mpc_context_id, custodian_pk: custodian_verf_key.clone(), custodian_role: role_j, operator_pk: self.verification_key.clone(), @@ -683,61 +611,176 @@ impl Operator { }) } - /// Operators that does the recovery collects all the materials - /// used during the backup protocol such as shares, keys and signcryptions, - /// and then uses them to verify whether the shares are correct before - /// doing the reconstruction. + /// Validate a single signcrypted custodian recovery output. /// - /// Commitments do not come from the same location as the custodian message - /// so the are a separate input. + /// Runs every check the operator-side recovery path requires: unsigncryption (which also + /// enforces the signcryption's receiver-id binding), `backup_id` / `mpc_context_id` validity + + /// equality against the operator-signed `RecoveryValidationMaterial`, custodian / operator key + /// equality inside the decrypted payload, and the commitment match. Returns the precise + /// `RecoverySkipReason` on the first failure. + pub(crate) fn validate_one_recovery_output( + &self, + output: &InternalCustodianRecoveryOutput, + recovery_material: &RecoveryValidationMaterial, + ephm_dec_key: &UnifiedPrivateEncKey, + ephm_enc_key: &UnifiedPublicEncKey, + ) -> Result { + let (_, custodian_verf_key) = self.custodian_keys.get(&output.custodian_role).ok_or({ + tracing::warn!("missing custodian key for role {}", output.custodian_role); + RecoverySkipReason::MissingVerificationKey + })?; + let operator_id = self.verification_key.verf_key_id(); + let unsign_key = UnifiedUnsigncryptionKey::new( + ephm_dec_key, + ephm_enc_key, + custodian_verf_key, + &operator_id, + ); + let backup_material: BackupMaterial = unsign_key + .unsigncrypt(&DSEP_BACKUP_RECOVERY, &output.signcryption) + .map_err(|e| { + tracing::warn!( + "Could not unsigncrypt backup share for custodian role {} (wrong operator or tampered): {e}", + output.custodian_role + ); + RecoverySkipReason::InvalidSigncryption + })?; + let expected_backup_id: RequestId = recovery_material.custodian_context().context_id; + let expected_mpc_context_id = recovery_material.mpc_context(); + if !backup_material.backup_id.is_valid() { + tracing::warn!( + "BackupMaterial.backup_id {} is malformed for custodian role {}", + backup_material.backup_id, + output.custodian_role + ); + return Err(RecoverySkipReason::BackupIdMalformed); + } + if !backup_material.mpc_context_id.is_valid() { + tracing::warn!( + "BackupMaterial.mpc_context_id {} is malformed for custodian role {}", + backup_material.mpc_context_id, + output.custodian_role + ); + return Err(RecoverySkipReason::MpcContextIdMalformed); + } + if backup_material.backup_id != expected_backup_id { + tracing::warn!( + "BackupMaterial.backup_id mismatch for custodian role {}: expected {} got {}", + output.custodian_role, + expected_backup_id, + backup_material.backup_id + ); + return Err(RecoverySkipReason::BackupIdMismatch); + } + if backup_material.mpc_context_id != expected_mpc_context_id { + tracing::warn!( + "BackupMaterial.mpc_context_id mismatch for custodian role {}: expected {} got {}", + output.custodian_role, + expected_mpc_context_id, + backup_material.mpc_context_id + ); + return Err(RecoverySkipReason::MpcContextIdMismatch); + } + if let Err(e) = backup_material.check_expected_metadata( + custodian_verf_key, + output.custodian_role, + &operator_id, + ) { + tracing::warn!( + "Metadata check ({e:?}) failed for custodian role {}", + output.custodian_role + ); + return Err(e); + } + let actual_commitment = + safe_serialize_hash_element_versioned(&DSEP_BACKUP_COMMITMENT, &backup_material) + .map_err(|e| { + tracing::warn!( + "Could not hash BackupMaterial for commitment check (role {}): {e}", + output.custodian_role + ); + RecoverySkipReason::ParseError + })?; + let expected_commitment = recovery_material.get(&output.custodian_role).map_err(|_| { + tracing::warn!( + "No stored commitment for custodian role {}", + output.custodian_role + ); + RecoverySkipReason::MissingCommitment + })?; + if actual_commitment.as_slice() != expected_commitment { + tracing::warn!( + "Commitment mismatch for custodian role {}: BackupMaterial hash does not match the operator-signed commitment", + output.custodian_role + ); + return Err(RecoverySkipReason::CommitmentMismatch); + } + Ok(backup_material) + } + + /// Validate every signcrypted custodian recovery output and reconstruct the operator's secret. pub fn verify_and_recover( &self, custodian_recovery_output: &[InternalCustodianRecoveryOutput], recovery_material: &RecoveryValidationMaterial, - backup_id: RequestId, - ephm_dec_key: &UnifiedPrivateEncKey, // Note that this is the ephemeral decryption key, NOT the actual backup decryption key + ephm_dec_key: &UnifiedPrivateEncKey, ephm_enc_key: &UnifiedPublicEncKey, ) -> Result, BackupError> { - // the output is ordered by custodian ID, from 0 to n-1 - // first check the signature and decrypt - // decrypted_buf[j][i] where j = jth custodian, i = ith block - let decrypted_buf = custodian_recovery_output - .iter() - .map(|custodian_output| { - let (_, custodian_verf_key) = self - .custodian_keys - .get(&custodian_output.custodian_role) - .ok_or_else(|| { - BackupError::OperatorError(format!( - "missing custodian key for {}", - custodian_output.custodian_role - )) - })?; - let commitment = recovery_material - .get(&custodian_output.custodian_role) - .map_err(|_| BackupError::OperatorError("missing commitment".to_string()))?; - let operator_id = &self.verification_key.verf_key_id(); - let unsign_key = UnifiedUnsigncryptionKey::new( - ephm_dec_key, - ephm_enc_key, - custodian_verf_key, - operator_id, - ); - checked_decryption_deserialize( - &unsign_key, - &custodian_output.signcryption, - commitment, - backup_id, - custodian_output.custodian_role, - ) - }) - .collect::, _>>()?; + let mut validated: HashMap = HashMap::new(); + let mut skip_reasons: Vec = Vec::new(); + for output in custodian_recovery_output { + match self.validate_one_recovery_output( + output, + recovery_material, + ephm_dec_key, + ephm_enc_key, + ) { + Ok(bm) => match validated.entry(output.custodian_role) { + std::collections::hash_map::Entry::Occupied(_) => { + tracing::warn!( + "Received multiple recovery outputs for custodian role {}. Only the first one will be used.", + output.custodian_role + ); + skip_reasons.push(RecoverySkipReason::DuplicateRole); + } + std::collections::hash_map::Entry::Vacant(v) => { + v.insert(bm); + } + }, + Err(reason) => skip_reasons.push(reason), + } + } + let threshold = recovery_material.custodian_context().threshold as usize; + let required_min = threshold + 1; + if validated.len() < required_min { + let received = validated.len(); + tracing::error!( + received, + threshold, + ?skip_reasons, + "Cannot recover the backup decryption key: not enough valid recovery outputs" + ); + return Err(BackupError::RecoveryThresholdNotMet { + required_min, + received, + threshold, + skipped: skip_reasons, + }); + } + self.recover_from_validated(&validated) + } + + /// Reconstruct the operator's secret from already-validated per-role `BackupMaterial`s. + pub fn recover_from_validated( + &self, + validated: &HashMap, + ) -> Result, BackupError> { + let decrypted_buf: Vec<&Vec>> = + validated.values().map(|bm| &bm.shares).collect(); let num_blocks = if let Some(x) = decrypted_buf.iter().map(|v| v.len()).min() { x } else { - // This is normally impossible to happen because if it did - // then it would mean the validation on expected_shares above failed return Err(BackupError::NoBlocksError); }; @@ -745,7 +788,6 @@ impl Operator { for b in 0..num_blocks { let mut shamir_sharing = ShamirSharings::new(); for blocks in decrypted_buf.iter() { - // we should be able to safely add shares since it checks whether the role is repeated shamir_sharing.add_share(blocks[b]); } all_sharings.push(shamir_sharing); @@ -1206,6 +1248,66 @@ mod tests { ); } + #[test] + fn check_expected_metadata_sunshine_and_mismatches() { + let mut rng = AesRng::seed_from_u64(101); + let (custodian_verf_key, _) = gen_sig_keys(&mut rng); + let (operator_verf_key, _) = gen_sig_keys(&mut rng); + let (other_custodian_verf_key, _) = gen_sig_keys(&mut rng); + let (other_operator_verf_key, _) = gen_sig_keys(&mut rng); + + let custodian_role = Role::indexed_from_one(2); + let backup_id = derive_request_id("check_expected_metadata").unwrap(); + + let material = BackupMaterial { + backup_id, + mpc_context_id: *DEFAULT_MPC_CONTEXT, + custodian_pk: custodian_verf_key.clone(), + custodian_role, + operator_pk: operator_verf_key.clone(), + shares: Vec::new(), + }; + + // Sunshine: every metadata field matches the expected routing parameters. + material + .check_expected_metadata( + &custodian_verf_key, + custodian_role, + &operator_verf_key.verf_key_id(), + ) + .expect("metadata that matches the routing parameters should validate"); + + // Custodian role mismatch. + assert_eq!( + material.check_expected_metadata( + &custodian_verf_key, + Role::indexed_from_one(3), + &operator_verf_key.verf_key_id(), + ), + Err(RecoverySkipReason::CustodianRoleMismatchInPayload), + ); + + // Custodian verification key mismatch. + assert_eq!( + material.check_expected_metadata( + &other_custodian_verf_key, + custodian_role, + &operator_verf_key.verf_key_id(), + ), + Err(RecoverySkipReason::CustodianKeyMismatchInPayload), + ); + + // Operator (recipient) id mismatch — defends against a share routed to the wrong operator. + assert_eq!( + material.check_expected_metadata( + &custodian_verf_key, + custodian_role, + &other_operator_verf_key.verf_key_id(), + ), + Err(RecoverySkipReason::OperatorMismatchInPayload), + ); + } + #[test] fn operator_new_fails_with_not_enough() { let mut rng = AesRng::seed_from_u64(8); diff --git a/core/service/src/backup/tests.rs b/core/service/src/backup/tests.rs index 05595ab1a2..1ecd7f356d 100644 --- a/core/service/src/backup/tests.rs +++ b/core/service/src/backup/tests.rs @@ -1,4 +1,8 @@ -use super::{custodian, error::BackupError, operator::Operator}; +use super::{ + custodian, + error::{BackupError, RecoverySkipReason}, + operator::Operator, +}; use crate::{ backup::{ custodian::{ @@ -19,7 +23,7 @@ use crate::{ }; use aes_prng::AesRng; use itertools::Itertools; -use kms_grpc::{RequestId, kms::v1::CustodianContext}; +use kms_grpc::{ContextId, RequestId, kms::v1::CustodianContext}; use proptest::prelude::*; use rand::{SeedableRng, rngs::OsRng}; use std::collections::BTreeMap; @@ -94,6 +98,7 @@ fn custodian_reencrypt() { let operator_count = 4usize; let secret_len = 32usize; let backup_id = RequestId::from_bytes([8u8; crate::consts::ID_LENGTH]); + let mpc_context_id = *DEFAULT_MPC_CONTEXT; let mut rng = OsRng; @@ -142,13 +147,13 @@ fn custodian_reencrypt() { .zip_eq(&secrets) .map(|(operator, secret)| { operator - .secret_share_and_signcrypt(&mut rng, secret, backup_id) + .secret_share_and_signcrypt(&mut rng, secret, backup_id, mpc_context_id) .unwrap() }) .collect::>(); let verification_key = operators[0].verification_key(); - let mpc_context_id = RequestId::from_bytes([7u8; crate::consts::ID_LENGTH]); + let mut enc = Encryption::new(PkeSchemeType::MlKem512, &mut rng); let (_ephemeral_dec_key, ephemeral_enc_key) = enc.keygen().unwrap(); @@ -164,10 +169,8 @@ fn custodian_reencrypt() { .verify_reencrypt( &mut rng, bad_results[0].ct_shares.get(&operator_role).unwrap(), - mpc_context_id, verification_key, &ephemeral_enc_key, - backup_id, ) .unwrap_err(); assert!(matches!(err, BackupError::CustodianRecoveryError)); @@ -185,27 +188,8 @@ fn custodian_reencrypt() { .verify_reencrypt( &mut rng, bad_results[0].ct_shares.get(&operator_role).unwrap(), - mpc_context_id, verification_key, &ephemeral_enc_key, - backup_id, - ) - .unwrap_err(); - assert!(matches!(err, BackupError::CustodianRecoveryError)); - } - - // use the wrong backup_id - { - let operator_role = Role::indexed_from_zero(0); - let bad_backup_id = RequestId::from_bytes([7u8; crate::consts::ID_LENGTH]); - let err = custodians[0] - .verify_reencrypt( - &mut rng, - signcrypt_results[0].ct_shares.get(&operator_role).unwrap(), - mpc_context_id, - verification_key, - &ephemeral_enc_key, - bad_backup_id, ) .unwrap_err(); assert!(matches!(err, BackupError::CustodianRecoveryError)); @@ -218,10 +202,8 @@ fn custodian_reencrypt() { .verify_reencrypt( &mut rng, signcrypt_results[0].ct_shares.get(&operator_role).unwrap(), - mpc_context_id, verification_key, &ephemeral_enc_key, - backup_id, ) .unwrap(); } @@ -251,13 +233,12 @@ fn full_flow( let ops_addresses = operators.keys(); let backups = custodian_recover( &mut rng, - &backup_id, &mnemonics, &payload_for_custodians, custodian_threshold, ); assert!(backups.len() == operator_count); - let recovered_secrets = operator_recover(&backups, &operators, &backup_id); + let recovered_secrets = operator_recover(&backups, &operators); assert!(recovered_secrets.len() == operator_count); for op_addr in ops_addresses { @@ -303,13 +284,12 @@ fn full_flow_drop_msg() { assert!(mnemonics_dropped.len() > custodian_threshold); let backups = custodian_recover( &mut rng, - &backup_id, &mnemonics_dropped, &payload_for_custodians, custodian_threshold, ); assert!(backups.len() == operator_count); - let recovered_secrets = operator_recover(&backups, &operators, &backup_id); + let recovered_secrets = operator_recover(&backups, &operators); assert!(recovered_secrets.len() == operator_count); for addr in op_addresses { @@ -384,6 +364,7 @@ fn full_flow_malicious_custodian_init() { &mut rng, &bc2wrap::serialize(&backup_priv_key).unwrap(), backup_id, + *DEFAULT_MPC_CONTEXT, ) .unwrap(); assert!( @@ -423,14 +404,13 @@ fn full_flow_malicious_custodian_second() { .to_string()); let backups = custodian_recover( &mut rng, - &backup_id, &mnemonics_malicious, &payload_for_custodians, custodian_threshold, ); // We should still be able to recover even though one custodian is malicious assert!(backups.len() == operator_count); - let recovered_secrets = operator_recover(&backups, &operators, &backup_id); + let recovered_secrets = operator_recover(&backups, &operators); assert!(recovered_secrets.len() == operator_count); for op_addr in &op_addresses { @@ -464,13 +444,12 @@ fn full_flow_malicious_custodian_second() { .to_string()); let backups = custodian_recover( &mut rng, - &backup_id, &mnemonics_malicious_dropped, &payload_for_custodians, custodian_threshold, ); assert!(backups.len() == operator_count); - let recovered_secrets = operator_recover(&backups, &operators, &backup_id); + let recovered_secrets = operator_recover(&backups, &operators); assert!(recovered_secrets.len() == operator_count); for op_addr in &op_addresses { @@ -527,14 +506,13 @@ fn full_flow_malicious_operator() { let backups = custodian_recover( &mut rng, - &backup_id, &mnemonics, &payload_for_custodians_malicious, custodian_threshold, ); // One missing and one malicious operator assert!(backups.len() == operator_count - 2); - let recovered_secrets = operator_recover(&backups, &operators, &backup_id); + let recovered_secrets = operator_recover(&backups, &operators); assert!(recovered_secrets.len() == operator_count - 2); for (cur_role, cur_secret) in recovered_secrets { @@ -544,6 +522,80 @@ fn full_flow_malicious_operator() { } } +/// Negative test for the `mpc_context_id` binding added to `BackupMaterial`. +#[test] +fn verify_and_recover_rejects_mpc_context_mismatch() { + let mut rng = AesRng::seed_from_u64(1338); + let backup_id = derive_request_id(std::stringify!( + verify_and_recover_rejects_mpc_context_mismatch + )) + .unwrap(); + let operator_count = 4usize; + let custodian_count = 3usize; + let custodian_threshold = 1usize; + + let (setup_msgs, mnemonics) = generate_setup_messages(&mut rng, custodian_count); + let (mut operators, payload_for_custodians) = operator_handle_init( + &mut rng, + &setup_msgs, + &backup_id, + operator_count, + custodian_threshold, + custodian_count, + ); + + // An MPC context ID different from the default one + let wrong_mpc_context = ContextId::from_bytes([0x99u8; crate::consts::ID_LENGTH]); + for op_state in operators.values_mut() { + let original_payload = op_state.1.payload.clone(); + let (_, sk) = gen_sig_keys(&mut rng); + // Change the validation material to contain the wrong context id + op_state.1 = RecoveryValidationMaterial::new( + original_payload.cts, + original_payload.commitments, + original_payload.custodian_context, + &sk, + wrong_mpc_context, + ) + .unwrap(); + } + + let backups = custodian_recover( + &mut rng, + &mnemonics, + &payload_for_custodians, + custodian_threshold, + ); + assert_eq!(backups.len(), operator_count); + + for (op_addr, (operator, validation_material, dec_key, enc_key)) in &operators { + let reencs: Vec<_> = backups + .get(op_addr) + .expect("custodian recovery outputs for each operator") + .values() + .cloned() + .collect(); + let err = operator + .verify_and_recover(&reencs, validation_material, dec_key, enc_key) + .expect_err("expected RecoveryThresholdNotMet due to mpc_context_id mismatch"); + match err { + BackupError::RecoveryThresholdNotMet { + received, skipped, .. + } => { + assert_eq!(received, 0, "no share should validate"); + assert!( + !skipped.is_empty() + && skipped + .iter() + .all(|r| *r == RecoverySkipReason::MpcContextIdMismatch), + "expected every skip reason to be MpcContextIdMismatch, got: {skipped:?}" + ); + } + other => panic!("unexpected BackupError variant: {other}"), + } + } +} + fn generate_setup_messages( rng: &mut AesRng, custodian_count: usize, @@ -602,6 +654,7 @@ fn operator_handle_init( rng, &bc2wrap::serialize(&backup_dec_key).unwrap(), *backup_id, + *DEFAULT_MPC_CONTEXT, ) .unwrap(); let cur_op_output = signcrypt_result.ct_shares; @@ -656,7 +709,6 @@ type OperatorsMap = BTreeMap< fn custodian_recover( rng: &mut AesRng, - backup_id: &RequestId, mnemonics: &BTreeMap, // keyed by custodian role backups: &CustodianBackupsMap, // Operator role to verf key, ephemeral key and backup ct map custodian_threshold: usize, @@ -670,10 +722,8 @@ fn custodian_recover( match custodian.verify_reencrypt( rng, cur_backup.get(cur_cus_role).unwrap(), - RequestId::from_bytes([7u8; crate::consts::ID_LENGTH]), verification_key, ephemeral_enc_key, - *backup_id, ) { Ok(cur_res) => cur_operator_res.insert(*cur_cus_role, cur_res), Err(_) => { @@ -692,7 +742,6 @@ fn custodian_recover( fn operator_recover( reencryptions: &BTreeMap, BTreeMap>, operators: &OperatorsMap, - backup_id: &RequestId, ) -> BTreeMap, Vec> { let mut res = BTreeMap::new(); for (cur_op_addr, (cur_op, cur_com, cur_emphemeral_dec, cur_ephemeral_enc)) in operators { @@ -701,7 +750,6 @@ fn operator_recover( match cur_op.verify_and_recover( &reencs_vec, cur_com, - *backup_id, cur_emphemeral_dec, cur_ephemeral_enc, ) { diff --git a/core/service/src/bin/kms-custodian.rs b/core/service/src/bin/kms-custodian.rs index 90d0fbd55f..5622d696ed 100644 --- a/core/service/src/bin/kms-custodian.rs +++ b/core/service/src/bin/kms-custodian.rs @@ -1,7 +1,6 @@ use aes_prng::AesRng; use clap::Parser; use hashing::{DomainSep, hash_element}; -use kms_grpc::ContextId; use kms_lib::backup::SEED_PHRASE_DESC; use kms_lib::engine::context::SoftwareVersion; use kms_lib::{ @@ -163,11 +162,6 @@ async fn main() -> Result<(), anyhow::Error> { "Decrypting ciphertexts for custodian role: {}", params.custodian_role ); - let mpc_context_id_str = params.mpc_context_id; - let mpc_context_id = ContextId::try_from(&mpc_context_id_str).map_err(|e| anyhow::anyhow!( - "Invalid MPC context ID: {}. Expected a hex string representing the MPC context ID: {e}.", - mpc_context_id_str - ))?; let operator_verf_key: PublicSigKey = safe_read_element_versioned(¶ms.operator_verf_key).await?; let recovery_request: InternalRecoveryRequest = @@ -191,10 +185,8 @@ async fn main() -> Result<(), anyhow::Error> { let res = custodian.verify_reencrypt( &mut rng, custodian_backup, - mpc_context_id.into(), &operator_verf_key, recovery_request.backup_enc_key(), - recovery_request.backup_id(), )?; tracing::info!("Verified reencryption successfully"); safe_write_element_versioned(¶ms.output_path, &res).await?; diff --git a/core/service/src/client/tests/centralized/custodian_backup_tests.rs b/core/service/src/client/tests/centralized/custodian_backup_tests.rs index 69f70af86a..b77b42e29e 100644 --- a/core/service/src/client/tests/centralized/custodian_backup_tests.rs +++ b/core/service/src/client/tests/centralized/custodian_backup_tests.rs @@ -261,6 +261,7 @@ async fn decrypt_after_recovery(amount_custodians: usize, threshold: u32) { let cus_rec_req = emulate_custodian( &mut rng, recovery_req_resp, + env.req_new_cus, env.mnemonics.clone(), env.test_path(), ) @@ -356,6 +357,7 @@ async fn decrypt_after_recovery_negative(amount_custodians: usize, threshold: u3 let mut cus_rec_req = emulate_custodian( &mut rng, recovery_req_resp, + env.req_new_cus, env.mnemonics.clone(), env.test_path(), ) @@ -515,10 +517,10 @@ async fn test_mpc_context_backup_central() { async fn emulate_custodian( rng: &mut AesRng, recovery_request: RecoveryRequest, + custodian_context_id: RequestId, mnemonics: Vec, test_path: Option<&Path>, ) -> CustodianRecoveryRequest { - let backup_id = recovery_request.backup_id.clone().unwrap(); let mut cus_outputs = Vec::new(); for (cur_idx, cur_mnemonic) in mnemonics.iter().enumerate() { let custodian: Custodian = @@ -541,17 +543,15 @@ async fn emulate_custodian( .verify_reencrypt( rng, &cur_cus_reenc.to_owned().try_into().unwrap(), - kms_grpc::RequestId::from_bytes([7u8; 32]), &verf_key, &cur_enc_key, - backup_id.clone().try_into().unwrap(), ) .unwrap(); // Add the result from this custodian to the map of results to the correct operator cus_outputs.push(cur_out.try_into().unwrap()); } CustodianRecoveryRequest { - custodian_context_id: Some(backup_id.clone()), + custodian_context_id: Some(custodian_context_id.into()), custodian_recovery_outputs: cus_outputs, } } diff --git a/core/service/src/client/tests/threshold/custodian_backup_tests.rs b/core/service/src/client/tests/threshold/custodian_backup_tests.rs index 9734fb4081..70984c5d09 100644 --- a/core/service/src/client/tests/threshold/custodian_backup_tests.rs +++ b/core/service/src/client/tests/threshold/custodian_backup_tests.rs @@ -125,6 +125,10 @@ impl ThresholdBackupTestEnv { &PRIVATE_STORAGE_PREFIX_THRESHOLD_ALL[0..Self::AMOUNT_PARTIES] } + fn pub_prefixes(&self) -> &[Option] { + &PUBLIC_STORAGE_PREFIX_THRESHOLD_ALL[0..Self::AMOUNT_PARTIES] + } + fn kms_clients(&self) -> &HashMap> { self.kms_clients.as_ref().unwrap() } @@ -273,6 +277,10 @@ async fn backup_after_crs(amount_custodians: usize, threshold: u32) { env.shutdown().await; + // Capture operator verification keys before purging + let operator_verf_keys = operator_verf_key_map(env.test_path(), env.pub_prefixes()).await; + let custodian_context_id = env.req_new_cus; + // Purge the private storage to test the backup recovery purge_priv(env.test_path(), env.priv_prefixes()).await; @@ -285,7 +293,15 @@ async fn backup_after_crs(amount_custodians: usize, threshold: u32) { purge_priv(env.test_path(), env.priv_prefixes()).await; // Execute the backup restoring - run_full_custodian_recovery(&kms_clients, env.mnemonics.clone(), n, None).await; + run_full_custodian_recovery( + &kms_clients, + &operator_verf_keys, + custodian_context_id, + env.mnemonics.clone(), + n, + None, + ) + .await; // Verify CRS metadata was recovered correctly for (i, storage_prefix) in env.priv_prefixes().iter().enumerate() { @@ -354,6 +370,8 @@ async fn decrypt_after_recovery(amount_custodians: usize, threshold: u32) { // Read the private signing keys for reference let sig_keys = read_signing_keys(env.test_path(), env.priv_prefixes()).await; + let operator_verf_keys = operator_verf_key_map(env.test_path(), env.pub_prefixes()).await; + let custodian_context_id = env.req_new_cus; // Purge the private storage to test the backup purge_priv(env.test_path(), env.priv_prefixes()).await; @@ -366,7 +384,15 @@ async fn decrypt_after_recovery(amount_custodians: usize, threshold: u32) { purge_priv(env.test_path(), env.priv_prefixes()).await; // Execute the backup restoring - run_full_custodian_recovery(&kms_clients, env.mnemonics.clone(), n, None).await; + run_full_custodian_recovery( + &kms_clients, + &operator_verf_keys, + custodian_context_id, + env.mnemonics.clone(), + n, + None, + ) + .await; // Check that the key material is back let recovered_keys = read_signing_keys(env.test_path(), env.priv_prefixes()).await; @@ -451,6 +477,8 @@ async fn decrypt_after_recovery_negative(amount_custodians: usize, threshold: u3 // Read the private signing keys for reference let sig_keys = read_signing_keys(env.test_path(), env.priv_prefixes()).await; + let operator_verf_keys = operator_verf_key_map(env.test_path(), env.pub_prefixes()).await; + let custodian_context_id = env.req_new_cus; // Purge the private storage to test the backup purge_priv(env.test_path(), env.priv_prefixes()).await; @@ -465,6 +493,8 @@ async fn decrypt_after_recovery_negative(amount_custodians: usize, threshold: u3 // Execute the backup restoring with corrupted custodian outputs run_full_custodian_recovery( &kms_clients, + &operator_verf_keys, + custodian_context_id, env.mnemonics.clone(), n, Some(corrupt_custodian_outputs), @@ -918,6 +948,8 @@ async fn shutdown_servers_and_client( #[allow(clippy::type_complexity)] async fn run_full_custodian_recovery( kms_clients: &HashMap>, + operator_verf_keys: &HashMap, + custodian_context_id: RequestId, mnemonics: Vec, amount_parties: usize, mutate_outputs: Option)>, @@ -925,7 +957,14 @@ async fn run_full_custodian_recovery( let mut rng = AesRng::seed_from_u64(13); let recovery_req_resp = run_custodian_recovery_init(kms_clients).await; assert_eq!(recovery_req_resp.len(), amount_parties); - let mut cus_out = emulate_custodian(&mut rng, recovery_req_resp, mnemonics).await; + let mut cus_out = emulate_custodian( + &mut rng, + recovery_req_resp, + operator_verf_keys, + custodian_context_id, + mnemonics, + ) + .await; if let Some(mutate) = mutate_outputs { mutate(&mut cus_out); } @@ -956,6 +995,31 @@ async fn read_signing_keys( sig_keys } +#[cfg(feature = "testing")] +async fn operator_verf_key_map( + test_path: Option<&std::path::Path>, + pub_storage_prefixes: &[Option], +) -> HashMap { + let mut verf_keys = Vec::new(); + for storage_prefix in pub_storage_prefixes.iter() { + let cur_pub_store = + FileStorage::new(test_path, StorageType::PUB, storage_prefix.as_deref()).unwrap(); + let cur_pk: PublicSigKey = read_versioned_at_request_id( + &cur_pub_store, + &SIGNING_KEY_ID, + &PubDataType::VerfKey.to_string(), + ) + .await + .unwrap(); + verf_keys.push(cur_pk); + } + verf_keys + .into_iter() + .enumerate() + .map(|(idx, pk)| ((idx + 1) as u32, pk)) + .collect() +} + // Right now only used by insecure tests #[cfg(feature = "insecure")] async fn run_custodian_recovery_init( @@ -1056,10 +1120,10 @@ async fn run_restore_from_backup( async fn emulate_custodian( rng: &mut AesRng, recovery_requests: Vec<(u32, RecoveryRequest)>, + operator_verf_keys: &HashMap, + custodian_context_id: RequestId, mnemonics: Vec, ) -> HashMap { - let backup_id = recovery_requests[0].1.backup_id.clone().unwrap(); - // Setup a map to contain the results for each operator role let mut outputs_for_operators: HashMap<(u32, Address), Vec> = HashMap::new(); @@ -1068,8 +1132,9 @@ async fn emulate_custodian( let custodian: Custodian = custodian_from_seed_phrase(cur_mnemonic, Role::indexed_from_zero(cur_idx)).unwrap(); for (i, cur_recovery_req) in &recovery_requests { - let cur_verf_key: PublicSigKey = - bc2wrap::deserialize_safe(&cur_recovery_req.operator_verification_key).unwrap(); + let cur_verf_key = operator_verf_keys + .get(i) + .expect("operator verification key missing for party {cur_idx}"); let cur_cus_reenc = cur_recovery_req.cts.get(&((cur_idx + 1) as u64)).unwrap(); let cur_enc_key = safe_deserialize( std::io::Cursor::new(&cur_recovery_req.ephem_op_enc_key), @@ -1080,10 +1145,8 @@ async fn emulate_custodian( .verify_reencrypt( rng, &cur_cus_reenc.to_owned().try_into().unwrap(), - kms_grpc::RequestId::from_bytes([7u8; 32]), - &cur_verf_key, + cur_verf_key, &cur_enc_key, - backup_id.clone().try_into().unwrap(), ) .unwrap(); // Add the result from this custodian to the map of results to the correct operator @@ -1105,7 +1168,7 @@ async fn emulate_custodian( ( i, CustodianRecoveryRequest { - custodian_context_id: Some(backup_id.clone()), + custodian_context_id: Some(custodian_context_id.into()), custodian_recovery_outputs: v, }, ), diff --git a/core/service/src/engine/backup_operator.rs b/core/service/src/engine/backup_operator.rs index cdc4ec03c2..37d118b42b 100644 --- a/core/service/src/engine/backup_operator.rs +++ b/core/service/src/engine/backup_operator.rs @@ -1,7 +1,8 @@ use crate::backup::custodian::InternalCustodianRecoveryOutput; use crate::backup::error::{BackupError, RecoverySkipReason}; -use crate::backup::operator::DSEP_BACKUP_RECOVERY; +use crate::backup::operator::BackupMaterial; use crate::consts::DEFAULT_EPOCH_ID; +use crate::cryptography::signcryption::UnifiedSigncryption; use crate::engine::base::{CrsGenMetadata, KmsFheKeyHandles, derive_request_id}; use crate::engine::context::ContextInfo; use crate::engine::threshold::service::session::PRSSSetupCombined; @@ -21,7 +22,6 @@ use crate::{ Encryption, PkeScheme, PkeSchemeType, UnifiedPrivateEncKey, UnifiedPublicEncKey, }, signatures::{PrivateSigKey, PublicSigKey}, - signcryption::{UnifiedUnsigncryptionKey, Unsigncrypt}, }, engine::{ base::BaseKmsStruct, threshold::service::ThresholdFheKeys, traits::BackupOperator, @@ -38,6 +38,7 @@ use crate::{ }; use algebra::galois_rings::degree_4::{ResiduePolyF4Z64, ResiduePolyF4Z128}; use itertools::Itertools; +use kms_grpc::ContextId; use kms_grpc::kms::v1::{CustodianRecoveryInitRequest, CustodianRecoveryOutput}; use kms_grpc::{ RequestId, @@ -123,8 +124,6 @@ where let recovery_request = RecoveryRequest { ephem_op_enc_key: serialized_pub_key, cts: grpc_cts, - backup_id: Some(backup_id.into()), - operator_verification_key: bc2wrap::serialize(&self.base_kms.verf_key())?, }; tracing::info!( "Generated outer recovery request for backup_id/context_id={}", @@ -137,16 +136,16 @@ where )) } + /// Validate the recovery request from the custodian and return the fully-validated, decrypted + /// per-role `BackupMaterial`s. + /// + /// Returns (validated_rec, operator). pub(crate) async fn validate_custodian_backup_recovery_request( &self, ephemeral_dec_key: &UnifiedPrivateEncKey, ephemeral_enc_key: &UnifiedPublicEncKey, req: CustodianRecoveryRequest, - ) -> anyhow::Result<( - RequestId, - RecoveryValidationMaterial, - HashMap, - )> { + ) -> anyhow::Result<(HashMap, Operator)> { let custodian_context_id = parse_optional_grpc_request_id( &req.custodian_context_id, RequestIdParsingErr::BackupRecovery, @@ -159,32 +158,37 @@ where ) .await? }; - let parsed_custodian_rec = { - filter_custodian_data( - req.custodian_recovery_outputs, - &recovery_material, - &self.base_kms.verf_key(), - ephemeral_dec_key, - ephemeral_enc_key, - ) - .await? - }; - // Check that we have enough valid recovery outputs - if parsed_custodian_rec.len() - < (recovery_material.custodian_context().threshold as usize) + 1 - { + // The MPC context to validate against is taken from the operator-signed `RecoveryValidationMaterial` + // stored at backup time. `filter_custodian_data` enforces the per-share equality. + let mpc_context_id = recovery_material.mpc_context(); + if !mpc_context_id.is_valid() { return Err(anyhow::anyhow!( - "Only received {} valid recovery outputs, but threshold is {}. Cannot recover the backup decryption key.", - parsed_custodian_rec.len(), - recovery_material.custodian_context().threshold + "Invalid MPC context ID in recovery validation material" )); } - Ok(( - custodian_context_id, - recovery_material, - parsed_custodian_rec, - )) + let amount_custodians = recovery_material.custodian_context().custodian_nodes.len(); + let operator = Operator::new_for_validating( + recovery_material + .custodian_context() + .custodian_nodes + .values() + .cloned() + .collect_vec(), + (*self.base_kms.verf_key()).clone(), + recovery_material.custodian_context().threshold as usize, + amount_custodians, + )?; + + let validated_rec = filter_custodian_data( + req.custodian_recovery_outputs, + &operator, + &recovery_material, + ephemeral_dec_key, + ephemeral_enc_key, + ) + .await?; + Ok((validated_rec, operator)) } } @@ -351,7 +355,7 @@ where } }; let inner = request.into_inner(); - let (context_id, recovery_material, parsed_custodian_rec) = self + let (parsed_custodian_rec, operator) = self .validate_custodian_backup_recovery_request( &ephemeral_dec_key, &ephemeral_enc_key, @@ -372,43 +376,16 @@ where backup_vault.lock().await; match backup_vault.keychain { Some(KeychainProxy::SecretSharing(ref mut keychain)) => { - // Amount of custodians get defined during context creation - let amount_custodians = recovery_material - .payload - .custodian_context - .custodian_nodes - .len(); - let operator = Operator::new_for_validating( - recovery_material.custodian_context().custodian_nodes.values().cloned().collect_vec(), - (*self.base_kms.verf_key()).clone(), - recovery_material.custodian_context().threshold as usize, - amount_custodians, - ).map_err(|e| { - MetricedError::new( - OP_CUSTODIAN_BACKUP_RECOVERY, - None, - anyhow::anyhow!("Failed to create operator for secret sharing based decryption: {e}"), - tonic::Code::Internal, - ) - })?; - let custodian_outputs: Vec = - parsed_custodian_rec.values().cloned().collect(); let serialized_dec_key = operator - .verify_and_recover( - &custodian_outputs, - &recovery_material, - context_id, - &ephemeral_dec_key, - &ephemeral_enc_key, - ) + .recover_from_validated(&parsed_custodian_rec) .map_err(|e| { MetricedError::new( OP_CUSTODIAN_BACKUP_RECOVERY, None, anyhow::anyhow!( - "Failed to verify the backup decryption request: {e}" + "Failed to reconstruct the backup decryption key: {e}" ), - tonic::Code::Unauthenticated, + tonic::Code::Internal, ) })?; let backup_dec_key: UnifiedPrivateEncKey = safe_deserialize( @@ -519,7 +496,7 @@ where /// Load and validate the recovery validation material associated with the provided context ID async fn load_recovery_validation_material( public_storage: &Mutex, - custodian_context_id: &RequestId, + custodian_context_id: &ContextId, verf_key: &Arc, ) -> anyhow::Result where @@ -528,11 +505,11 @@ where let public_storage_guard = public_storage.lock().await; let recovery_material: RecoveryValidationMaterial = public_storage_guard .read_data( - custodian_context_id, + &(*custodian_context_id).into(), &PubDataType::RecoveryMaterial.to_string(), ) .await?; - if &recovery_material.custodian_context().context_id != custodian_context_id { + if recovery_material.custodian_context().context_id != (*custodian_context_id).into() { anyhow::bail!("The custodian context associated with the provided context ID is invalid",); } if !recovery_material.validate(verf_key) { @@ -541,31 +518,23 @@ where Ok(recovery_material) } -/// Filter and validate the custodian recovery outputs, returning a map from custodian role to recovery output -/// Each output is verified to be correctly signed by the custodian and to be intended for the current operator role. +/// Proto-side adapter for the operator-side validator. +/// Validates and unsigncrypts the [`BackupMaterial`] of the custodians. async fn filter_custodian_data( custodian_recovery_outputs: Vec, + operator: &Operator, recovery_material: &RecoveryValidationMaterial, - my_verf_key: &PublicSigKey, ephemeral_dec_key: &UnifiedPrivateEncKey, ephemeral_enc_key: &UnifiedPublicEncKey, -) -> anyhow::Result> { - let mut parsed_custodian_rec: HashMap = HashMap::new(); +) -> anyhow::Result> { + // Use the number of custodian nodes that was part of the context, not the amount we have received from + let outputs_len = recovery_material.custodian_context().custodian_nodes.len(); + let mut parsed_custodian_rec: HashMap = HashMap::new(); let mut skip_reasons: Vec = Vec::new(); + for cur_recovery_output in &custodian_recovery_outputs { - let current_verf_key: PublicSigKey = - bc2wrap::deserialize_safe(&cur_recovery_output.operator_verification_key)?; - if current_verf_key != *my_verf_key { - tracing::warn!( - "Received recovery output for operator {}, but current server is {}. The output will be ignored.", - current_verf_key.address(), - my_verf_key.address(), - ); - skip_reasons.push(RecoverySkipReason::WrongOperator); - continue; - } if cur_recovery_output.custodian_role == 0 - || cur_recovery_output.custodian_role > custodian_recovery_outputs.len() as u64 + || cur_recovery_output.custodian_role > outputs_len as u64 { tracing::warn!( "Received recovery output with invalid custodian role {}. The output will be ignored.", @@ -574,28 +543,9 @@ async fn filter_custodian_data( skip_reasons.push(RecoverySkipReason::InvalidRole); continue; } - let cur_verf = match recovery_material.custodian_context().custodian_nodes.get( - &Role::indexed_from_one(cur_recovery_output.custodian_role as usize), - ) { - Some(custodian_setup_msg) => &custodian_setup_msg.public_verf_key, - None => { - tracing::warn!( - "Could not find verification key for custodian role {}", - cur_recovery_output.custodian_role - ); - skip_reasons.push(RecoverySkipReason::MissingVerificationKey); - continue; - } - }; + let role = Role::indexed_from_one(cur_recovery_output.custodian_role as usize); - let verf_key_id = my_verf_key.verf_key_id(); - let unsign_key = UnifiedUnsigncryptionKey::new( - ephemeral_dec_key, - ephemeral_enc_key, - cur_verf, - &verf_key_id, - ); - let cur_signcryption = match &cur_recovery_output.backup_output { + let cur_signcryption: UnifiedSigncryption = match &cur_recovery_output.backup_output { Some(cur_op_out) => cur_op_out.try_into()?, None => { tracing::warn!( @@ -606,40 +556,29 @@ async fn filter_custodian_data( continue; } }; - if unsign_key - .validate_signcryption(&DSEP_BACKUP_RECOVERY, &cur_signcryption) - .is_err() - { - tracing::warn!( - "Could not validate signcryption for custodian role {}", - cur_recovery_output.custodian_role - ); - skip_reasons.push(RecoverySkipReason::InvalidSigncryption); - continue; - } - match >::try_from( - cur_recovery_output.to_owned(), + let internal = InternalCustodianRecoveryOutput { + signcryption: cur_signcryption, + custodian_role: role, + }; + + match operator.validate_one_recovery_output( + &internal, + recovery_material, + ephemeral_dec_key, + ephemeral_enc_key, ) { - Ok(output) => match parsed_custodian_rec.entry(output.custodian_role) { + Ok(backup_material) => match parsed_custodian_rec.entry(role) { std::collections::hash_map::Entry::Occupied(_) => { tracing::warn!( - "Received multiple recovery outputs for custodian {}. Only the first one will be used.", - current_verf_key.address(), + "Received multiple recovery outputs for custodian role {role}. Only the first one will be used." ); skip_reasons.push(RecoverySkipReason::DuplicateRole); } std::collections::hash_map::Entry::Vacant(vacant_entry) => { - vacant_entry.insert(output); + vacant_entry.insert(backup_material); } }, - Err(e) => { - tracing::warn!( - "Failed to parse custodian recovery output for operator role {}: {e}. The output will be ignored.", - current_verf_key.address(), - ); - skip_reasons.push(RecoverySkipReason::ParseError); - continue; - } + Err(reason) => skip_reasons.push(reason), } } let threshold = recovery_material.custodian_context().threshold as usize; @@ -1232,21 +1171,14 @@ mod tests { (rec_material, verf_key, dec_key, enc_key) } - fn dummy_output_for_operator( - custodian_role: u64, - operator_verification_key: PublicSigKey, - ) -> CustodianRecoveryOutput { + fn dummy_output_for_operator(custodian_role: u64) -> CustodianRecoveryOutput { CustodianRecoveryOutput { custodian_role, - // TODO(zama-ai/kms-internal/issues/2836) - // we may change how the verification key is serialized - operator_verification_key: bc2wrap::serialize(&operator_verification_key).unwrap(), backup_output: Some(OperatorBackupOutput { signcryption: vec![1, 2, 3], pke_type: 0, signing_type: 0, }), - mpc_context_id: Some(kms_grpc::RequestId::from_bytes([7u8; 32]).into()), } } @@ -1264,104 +1196,117 @@ mod tests { } } - #[tokio::test] - async fn test_filter_custodian_missing_cus_output() { - let (recovery_material, verf_key, dec_key, enc_key) = dummy_recovery_material(1); - let mut outputs = vec![dummy_output_for_operator(1, verf_key.clone())]; - let cus_2 = CustodianRecoveryOutput { - custodian_role: 2, - operator_verification_key: bc2wrap::serialize(&verf_key).unwrap(), - backup_output: None, // Missing backup output for custodian role 2 - mpc_context_id: Some(kms_grpc::RequestId::from_bytes([7u8; 32]).into()), - }; - outputs.push(cus_2); + /// Build the recovering `Operator` from a `RecoveryValidationMaterial` for tests that call + /// `filter_custodian_data` directly. + fn build_operator_from_recovery_material( + recovery_material: &RecoveryValidationMaterial, + verf_key: &PublicSigKey, + ) -> Operator { + let amount_custodians = recovery_material.custodian_context().custodian_nodes.len(); + Operator::new_for_validating( + recovery_material + .custodian_context() + .custodian_nodes + .values() + .cloned() + .collect_vec(), + verf_key.clone(), + recovery_material.custodian_context().threshold as usize, + amount_custodians, + ) + .expect("operator construction for test") + } + + async fn run_filter_expect_skip( + outputs: Vec, + recovery_material: &RecoveryValidationMaterial, + verf_key: &PublicSigKey, + dec_key: &UnifiedPrivateEncKey, + enc_key: &UnifiedPublicEncKey, + expected: RecoverySkipReason, + ) { + let operator = build_operator_from_recovery_material(recovery_material, verf_key); let result = - filter_custodian_data(outputs, &recovery_material, &verf_key, &dec_key, &enc_key).await; + filter_custodian_data(outputs, &operator, recovery_material, dec_key, enc_key).await; let (received, skipped) = expect_threshold_not_met(result.unwrap_err()); assert_eq!(received, 0); assert!( - skipped.contains(&RecoverySkipReason::MissingSigncryption), - "expected MissingSigncryption in skip reasons: {skipped:?}" + skipped.contains(&expected), + "expected {expected:?} in skip reasons: {skipped:?}" ); } + #[tokio::test] + async fn test_filter_custodian_missing_cus_output() { + let (rec, verf_key, dec_key, enc_key) = dummy_recovery_material(1); + let outputs = vec![ + dummy_output_for_operator(1), + CustodianRecoveryOutput { + custodian_role: 2, + backup_output: None, + }, + ]; + run_filter_expect_skip( + outputs, + &rec, + &verf_key, + &dec_key, + &enc_key, + RecoverySkipReason::MissingSigncryption, + ) + .await; + } + #[tokio::test] async fn test_filter_custodian_data_invalid_operator_role() { - let (recovery_material, verf_key, dec_key, enc_key) = dummy_recovery_material(1); - let outputs = vec![dummy_output_for_operator(1, verf_key.clone())]; + let (rec, _verf_key, dec_key, enc_key) = dummy_recovery_material(1); let (bad_verf_key, _bad_sig_key) = gen_sig_keys(&mut AesRng::seed_from_u64(42)); - let result = filter_custodian_data( - outputs, - &recovery_material, + run_filter_expect_skip( + vec![dummy_output_for_operator(1)], + &rec, &bad_verf_key, &dec_key, &enc_key, + RecoverySkipReason::InvalidSigncryption, ) .await; - let (received, skipped) = expect_threshold_not_met(result.unwrap_err()); - assert_eq!(received, 0); - assert!( - skipped.contains(&RecoverySkipReason::WrongOperator), - "expected WrongOperator in skip reasons: {skipped:?}" - ); } #[tokio::test] async fn test_filter_custodian_data_invalid_custodian_role() { - let (recovery_material, verf_key, dec_key, enc_key) = dummy_recovery_material(1); - let outputs = vec![ - dummy_output_for_operator(0, verf_key.clone()), // custodian_role == 0 - dummy_output_for_operator(99, verf_key.clone()), // custodian_role out of bounds - ]; - let result = - filter_custodian_data(outputs, &recovery_material, &verf_key, &dec_key, &enc_key).await; - let (received, skipped) = expect_threshold_not_met(result.unwrap_err()); - assert_eq!(received, 0); - assert!( - skipped.contains(&RecoverySkipReason::InvalidRole), - "expected InvalidRole in skip reasons: {skipped:?}" - ); + let (rec, verf_key, dec_key, enc_key) = dummy_recovery_material(1); + run_filter_expect_skip( + vec![ + dummy_output_for_operator(0), // custodian_role == 0 + dummy_output_for_operator(99), // custodian_role out of bounds + ], + &rec, + &verf_key, + &dec_key, + &enc_key, + RecoverySkipReason::InvalidRole, + ) + .await; } #[tokio::test] async fn test_filter_custodian_data_invalid_signature() { - let (recovery_material, verf_key, dec_key, enc_key) = dummy_recovery_material(1); - let outputs = vec![ - dummy_output_for_operator(1, verf_key.clone()), - dummy_output_for_operator(2, verf_key.clone()), - dummy_output_for_operator(3, verf_key.clone()), - ]; - let result = - filter_custodian_data(outputs, &recovery_material, &verf_key, &dec_key, &enc_key).await; - let (received, skipped) = expect_threshold_not_met(result.unwrap_err()); - assert_eq!(received, 0); - assert!( - skipped.contains(&RecoverySkipReason::InvalidSigncryption), - "expected InvalidSigncryption in skip reasons: {skipped:?}" - ); + let (rec, verf_key, dec_key, enc_key) = dummy_recovery_material(1); + run_filter_expect_skip( + vec![ + dummy_output_for_operator(1), + dummy_output_for_operator(2), + dummy_output_for_operator(3), + ], + &rec, + &verf_key, + &dec_key, + &enc_key, + RecoverySkipReason::InvalidSigncryption, + ) + .await; } - #[tokio::test] - async fn test_filter_custodian_data_missing_verification_key() { - let (mut recovery_material, verf_key, dec_key, enc_key) = dummy_recovery_material(1); - recovery_material - .payload - .custodian_context - .custodian_nodes - .remove(&Role::indexed_from_one(2)); - let outputs = vec![ - dummy_output_for_operator(1, verf_key.clone()), - dummy_output_for_operator(2, verf_key.clone()), - ]; - let result = - filter_custodian_data(outputs, &recovery_material, &verf_key, &dec_key, &enc_key).await; - let (received, skipped) = expect_threshold_not_met(result.unwrap_err()); - assert_eq!(received, 0); - assert!( - skipped.contains(&RecoverySkipReason::MissingVerificationKey), - "expected MissingVerificationKey in skip reasons: {skipped:?}" - ); - } #[tokio::test] async fn test_update_backup_vault() { let mut priv_storage = RamStorage::new(); diff --git a/core/service/src/engine/context_manager.rs b/core/service/src/engine/context_manager.rs index e0d50a6498..70de5a477e 100644 --- a/core/service/src/engine/context_manager.rs +++ b/core/service/src/engine/context_manager.rs @@ -962,6 +962,7 @@ async fn gen_recovery_validation( rng, &serialized_priv_key, custodian_context.context_id, + mpc_context_id, )?; let ct_map = signcrypt_result.ct_shares; let commitments = signcrypt_result.commitments; @@ -1785,8 +1786,6 @@ mod tests { let internal_rec_req = InternalRecoveryRequest::new( recovery_material.payload.custodian_context.backup_enc_key, recovery_material.payload.cts, - backup_id, - server_verf_key.clone(), ) .unwrap(); let custodian_id = custodian1.verification_key().verf_key_id(); diff --git a/core/service/src/vault/keychain/mod.rs b/core/service/src/vault/keychain/mod.rs index 24675db6bc..3f1ec8da39 100644 --- a/core/service/src/vault/keychain/mod.rs +++ b/core/service/src/vault/keychain/mod.rs @@ -184,6 +184,11 @@ pub fn encrypt_under_data_key( ) -> anyhow::Result> { let cipher = Aes256GcmSiv::new_from_slice(key) .map_err(|_| anyhow_error_and_log("Invalid data key length: must be 256 bits"))?; + if iv.len() != 12 { + return Err(anyhow_error_and_log( + "Invalid IV length: must be exactly 96 bits for AES-256-GCM-SIV", + )); + } #[allow(deprecated)] let nonce = Nonce::from_slice(iv); let auth_tag = cipher @@ -202,6 +207,16 @@ pub fn decrypt_under_data_key( #[allow(deprecated)] let cipher = Aes256GcmSiv::new_from_slice(key) .map_err(|_| anyhow_error_and_log("Invalid data key length: must be 256 bits"))?; + if iv.len() != 12 { + return Err(anyhow_error_and_log( + "Invalid IV length: must be exactly 96 bits for AES-256-GCM-SIV", + )); + } + if auth_tag.len() != 16 { + return Err(anyhow_error_and_log( + "Invalid auth tag length: must be exactly 128 bits for AES-256-GCM-SIV", + )); + } #[allow(deprecated)] let nonce = Nonce::from_slice(iv); cipher @@ -215,11 +230,140 @@ pub mod tests { use super::{ RootKeyMeasurements, awskms::{canonicalize_iam_policy, make_root_key_policy}, - verify_root_key_measurements, + decrypt_under_data_key, encrypt_under_data_key, verify_root_key_measurements, }; use iam_rs::{IAMPolicy, IAMVersion}; use threshold_networking::tls::ReleasePCRValues; + /// Sunshine test: a plaintext encrypted with a valid 32-byte key and + /// 12-byte IV must decrypt back to the original bytes. + #[test] + fn test_encrypt_decrypt_under_data_key_roundtrip() { + let key = [0x42u8; 32]; + let iv = [0xABu8; 12]; + let plaintext = b"hello world, this is a secret payload".to_vec(); + + let mut buf = plaintext.clone(); + let auth_tag = encrypt_under_data_key(&mut buf, &key, &iv) + .expect("encryption should succeed with a 32-byte key and 12-byte IV"); + assert_ne!(buf, plaintext, "ciphertext must differ from plaintext"); + + decrypt_under_data_key(&mut buf, &key, &iv, &auth_tag) + .expect("decryption should succeed with the same key, IV and auth tag"); + assert_eq!(buf, plaintext, "decrypted bytes must match the plaintext"); + } + + #[test] + fn test_encrypt_decrypt_empty_plaintext() { + let key = [0x42u8; 32]; + let iv = [0xABu8; 12]; + let mut buf: Vec = Vec::new(); + + let auth_tag = encrypt_under_data_key(&mut buf, &key, &iv) + .expect("encryption should succeed on empty plaintext"); + assert!( + buf.is_empty(), + "ciphertext of empty plaintext must also be empty" + ); + assert_eq!(auth_tag.len(), 16, "auth tag must be 128 bits"); + + decrypt_under_data_key(&mut buf, &key, &iv, &auth_tag) + .expect("decryption should succeed on empty ciphertext with valid auth tag"); + assert!( + buf.is_empty(), + "decrypted empty ciphertext must remain empty" + ); + } + + #[test] + fn test_encrypt_same_plaintext() { + let key = [0x42u8; 32]; + let iv1 = [0xAAu8; 12]; + let iv2 = [0xBBu8; 12]; + let plaintext = b"identical payload encrypted twice".to_vec(); + + let mut buf1 = plaintext.clone(); + let tag_1 = encrypt_under_data_key(&mut buf1, &key, &iv1).expect("encryption must succeed"); + let mut buf2 = plaintext.clone(); + let tag_2 = encrypt_under_data_key(&mut buf2, &key, &iv2).expect("encryption must succeed"); + + assert_ne!( + buf1, buf2, + "ciphertexts under distinct IVs must not be identical" + ); + // Tag will also be different + assert_ne!( + tag_1, tag_2, + "auth tags should be different under distinct IVs" + ); + } + + #[test] + fn test_encrypt_decrypt_rejects_swapped_key_and_iv() { + let key = [0x42u8; 32]; + let iv = [0xABu8; 12]; + let mut buf = b"some payload".to_vec(); + + // Pass IV (12 bytes) as the key: must fail the key length check. + let err = encrypt_under_data_key(&mut buf, &iv, &key) + .expect_err("encryption must reject swapped key and IV"); + assert!( + err.to_string().contains("data key length"), + "error should mention data key length, got: {err}" + ); + + // Same on the decryption path. + let auth_tag = vec![0u8; 16]; + let err = decrypt_under_data_key(&mut buf, &iv, &key, &auth_tag) + .expect_err("decryption must reject swapped key and IV"); + assert!( + err.to_string().contains("data key length"), + "error should mention data key length, got: {err}" + ); + } + + #[test] + fn test_decrypt_rejects_bitflipped_ciphertext() { + let key = [0x42u8; 32]; + let iv = [0xABu8; 12]; + let plaintext = b"sensitive payload that must not be tampered with".to_vec(); + + let mut buf = plaintext.clone(); + let auth_tag = + encrypt_under_data_key(&mut buf, &key, &iv).expect("encryption must succeed"); + + // Flip a single bit in the first ciphertext byte. + buf[0] ^= 0x01; + + let err = decrypt_under_data_key(&mut buf, &key, &iv, &auth_tag) + .expect_err("decryption of tampered ciphertext must fail the auth tag check"); + assert!( + !err.to_string().is_empty(), + "must surface a decryption error, got empty message" + ); + assert_ne!( + buf, plaintext, + "tampered ciphertext must not decrypt back to the original plaintext" + ); + } + + #[test] + fn test_encrypt_under_data_key_rejects_invalid_iv() { + let key = [0u8; 32]; + let invalid_iv_lens = (0..12).chain([13usize, 16, 24, 32, 64]); + for iv_len in invalid_iv_lens { + let iv = vec![0u8; iv_len]; + let mut buf = b"some payload".to_vec(); + let err = encrypt_under_data_key(&mut buf, &key, &iv).expect_err(&format!( + "encryption should reject an IV of length {iv_len}" + )); + assert!( + err.to_string().contains("IV length"), + "error should mention IV length, got: {err}" + ); + } + } + #[test] fn test_verify_root_key_measurements() { let good_pcr_values = ReleasePCRValues { diff --git a/core/service/tests/backward_compatibility_kms.rs b/core/service/tests/backward_compatibility_kms.rs index 6c011f539e..27a2ff1492 100644 --- a/core/service/tests/backward_compatibility_kms.rs +++ b/core/service/tests/backward_compatibility_kms.rs @@ -845,6 +845,7 @@ fn test_recovery_material( let (custodian_pk, _) = gen_sig_keys(&mut rng); let backup_material = BackupMaterial { backup_id, + mpc_context_id: kms_grpc::identifiers::ContextId::from_bytes([9u8; 32]), custodian_pk, custodian_role: cus_role, operator_pk: operator_pk.clone(), @@ -894,10 +895,8 @@ fn test_internal_recovery_request( let original_versionized: InternalRecoveryRequest = load_and_unversionize(dir, test, format)?; let mut rng = AesRng::seed_from_u64(test.state); - let backup_id: RequestId = RequestId::new_random(&mut rng); let mut encryption = Encryption::new(PkeSchemeType::MlKem512, &mut rng); let (_dec_key, enc_key) = encryption.keygen().unwrap(); - let (verf_key, _) = gen_sig_keys(&mut rng); let mut cts = BTreeMap::new(); for role_j in 1..=test.amount { let cur_role = Role::indexed_from_one(role_j as usize); @@ -910,7 +909,7 @@ fn test_internal_recovery_request( }; cts.insert(cur_role, InnerOperatorBackupOutput { signcryption }); } - let new_versionized = InternalRecoveryRequest::new(enc_key, cts, backup_id, verf_key).unwrap(); + let new_versionized = InternalRecoveryRequest::new(enc_key, cts).unwrap(); if original_versionized != new_versionized { Err(test.failure( @@ -980,7 +979,6 @@ fn test_internal_custodian_recovery_output( let original_versionized: InternalCustodianRecoveryOutput = load_and_unversionize(dir, test, format)?; let mut rng = AesRng::seed_from_u64(test.state); - let (operator_verification_key, _sk) = gen_sig_keys(&mut rng); let mut buf = [0u8; 100]; rng.fill_bytes(&mut buf); let signcryption = UnifiedSigncryption { @@ -992,8 +990,6 @@ fn test_internal_custodian_recovery_output( let new_versionized = InternalCustodianRecoveryOutput { signcryption, custodian_role: Role::indexed_from_one(2), - operator_verification_key, - mpc_context_id: kms_grpc::RequestId::from_bytes([7u8; 32]), }; if original_versionized != new_versionized { @@ -1213,6 +1209,7 @@ fn test_operator_backup_output( &mut rng, &test.plaintext, RequestId::from_bytes(test.backup_id), + kms_grpc::identifiers::ContextId::from_bytes([9u8; 32]), ) .unwrap(); diff --git a/core/service/tests/integration_test.rs b/core/service/tests/integration_test.rs index 0b1bc94a54..cd92fcedf5 100644 --- a/core/service/tests/integration_test.rs +++ b/core/service/tests/integration_test.rs @@ -595,6 +595,7 @@ mod kms_custodian_binary_tests { &mut rng, &bc2wrap::serialize(&backup_ske).unwrap(), backup_id, + *DEFAULT_MPC_CONTEXT, ) .unwrap(); let ct_map = signcrypt_result.ct_shares; @@ -625,13 +626,8 @@ mod kms_custodian_binary_tests { let ct = ct_map.get(&custodian_role).unwrap(); ciphertexts.insert(custodian_role, ct.to_owned()); } - let recovery_request = InternalRecoveryRequest::new( - ephemeral_pub_key.clone(), - ciphertexts, - backup_id, - verification_key.clone(), - ) - .unwrap(); + let recovery_request = + InternalRecoveryRequest::new(ephemeral_pub_key.clone(), ciphertexts).unwrap(); safe_write_element_versioned(&Path::new(&operator_verf_path), &verification_key) .await .unwrap(); @@ -670,13 +666,7 @@ mod kms_custodian_binary_tests { outputs.push(payload); } operator - .verify_and_recover( - &outputs, - recovery_material, - backup_id, - ephem_dec_key, - ephem_enc_key, - ) + .verify_and_recover(&outputs, recovery_material, ephem_dec_key, ephem_enc_key) .unwrap() } } diff --git a/docs/guides/backup.md b/docs/guides/backup.md index ef3f876a94..d94e885fe6 100644 --- a/docs/guides/backup.md +++ b/docs/guides/backup.md @@ -53,16 +53,145 @@ $ kms-backup decrypt --seed-phrase --rando ``` Observe that the `randomness` supplied is used along with entropy of the current system to do re-encryption, and thus the command is *not* idempotent. -IMPORTANT: IT IS NOT POSSIBLE FOR THE CUSTODIAN TO VALIDATE THE AUTHENTICITY OF A REQUEST! HENCE IT IS PARAMOUNT THAT IT IS VALIDATED OUT-OF-BOUNDS, E.G. THROUGH A DIGEST ON A BLOCKCHAIN. -Run the CLI tool with the `decrypt` command in order decrypt a backup, and then reencrypt it under a supplied operator keyset. More specifically: +IMPORTANT: IT IS NOT POSSIBLE FOR THE CUSTODIAN TO VALIDATE THE AUTHENTICITY OF A REQUEST! HENCE IT IS PARAMOUNT THAT IT IS VALIDATED OUT-OF-BAND, E.G. THROUGH A DIGEST ON A BLOCKCHAIN. + +Run the CLI tool with the `decrypt` command in order to decrypt a backup, and then reencrypt it under a supplied operator keyset. More specifically: ```{bash} -$ cargo run --bin kms-custodian decrypt --seed-phrase --randomness --custodian-role <1-indexed role of the custodian> --recovery-request-path --operator-verf-key --mpc-context-id --output-path +$ cargo run --bin kms-custodian decrypt --seed-phrase --randomness --custodian-role <1-indexed role of the custodian> --recovery-request-path --operator-verf-key --output-path ``` Observe that the `randomness` supplied is used along with entropy of the current system to do re-encryption, and thus the command is *not* idempotent. For example: ```{bash} -$ cargo run --bin kms-custodian decrypt --seed-phrase "stick essence exhaust bunker meat orchard wolf timber tackle gesture video cheap" --randomness 123 --custodian-role 1 --recovery-request-path core-client/tests/data/keys/CUSTODIAN/recovery/1 --operator-verf-key core-client/tests/data/keys/PUB-p1/VerfKey/60b7070add74be3827160aa635fb255eeeeb88586c4debf7ab1134ddceb4beee --mpc-context-id 0x0700000000000000000000000000000000000000000000000000000000000001 --output-path core-client/tests/data/keys/CUSTODIAN/response/recovery-response-1-1 +$ cargo run --bin kms-custodian decrypt --seed-phrase "stick essence exhaust bunker meat orchard wolf timber tackle gesture video cheap" --randomness 123 --custodian-role 1 --recovery-request-path core-client/tests/data/keys/CUSTODIAN/recovery/1 --operator-verf-key core-client/tests/data/keys/PUB-p1/VerfKey/60b7070add74be3827160aa635fb255eeeeb88586c4debf7ab1134ddceb4beee --output-path core-client/tests/data/keys/CUSTODIAN/response/recovery-response-1-1 +``` + +--- + +## Protocol details + +The rest of this document is background material for readers who want to understand what the CLI commands do under the hood. It is not required to use the tool. + +The custodian-based backup protocol provides disaster recovery for KMS operators by Shamir-sharing a per-context backup encryption key across `n` offline custodians, tolerating up to `t` corruptions (`t < n/2`). At recovery time, `t + 1` honest custodians cooperate with the recovering operator to reconstruct the backup decryption key, which is then used to decrypt the operator's private key material from the backup vault. + +The alternative backup mode — wrapping the same key under an AWS KMS CMK — is documented in [ai-docs/ARCHITECTURE.md](../../ai-docs/ARCHITECTURE.md#backup-and-recovery). + +### Parties + +| Party | What it does | +|---|---| +| **Custodian `B_j`** (`j = 1..n`) | Human-held, offline party. Owns a long-term signing key `sk^{S_j}` and a post-quantum encryption key `sk^{E_j}`, both deterministically derived from a BIP39 seed phrase. Stores nothing online beyond its public-key published in the `CustodianSetupMessage`. Re-signcrypts its share of the backup key on request. | +| **Operator `P_i`** (KMS node) | Online KMS server. Holds a long-term signing key `sk^{P_i}`, a TFHE secret key, and other private material that needs backing up. Receives `NewCustodianContext` and, later, `CustodianRecoveryInit` / `CustodianBackupRecovery` gRPC calls from the core-client. | +| **core-client** | The CLI that drives every gRPC call into the KMS for custodian-based backup. It bundles the operator-bound RPCs (`NewCustodianContext`, `CustodianRecoveryInit`, `CustodianBackupRecovery`, `RestoreFromBackup`) and shuttles the resulting `RecoveryRequest` / `InternalCustodianRecoveryOutput` files between the operator and the custodians out-of-band. Documented in [docs/guides/core_client.md](core_client.md). | +| **Recovering operator `P_i'`** | A fresh operator recovers the content of the private storage of a previous operator. Reads only the public storage and the backup vault; coordinates with custodians (via the core-client) to rebuild private state. | + +### Data components + +All names below match the Rust/proto types so you can grep for them. + +| Component | Where it lives | Carries | +|---|---|---| +| [`CustodianSetupMessage`](../../core/grpc/proto/kms.v1.proto) | gRPC + custodian's `--path` file | `{ custodian_role, name, payload }`. `payload` is a versioned [`CustodianSetupMessagePayload`](../../core/service/src/backup/custodian.rs) `{ header, random_value, timestamp, public_enc_key = pk^{E_j}, verification_key = pk^{S_j} }`. | +| [`CustodianContext`](../../core/grpc/proto/kms.v1.proto) | Argument to `NewCustodianContext` RPC | `{ custodian_nodes: [CustodianSetupMessage], custodian_context_id, threshold }`. | +| [`InternalCustodianContext`](../../core/service/src/backup/custodian.rs) | Operator's private storage (replicated through the backup vault) | `{ threshold, context_id, custodian_nodes, backup_enc_key }`. `backup_enc_key = pk^{B}` is the per-context public key whose secret half is Shamir-shared to the custodians. | +| [`BackupMaterial`](../../core/service/src/backup/operator.rs) | Plaintext payload **inside** every operator→custodian signcryption | `{ backup_id (= custodian_context_id), mpc_context_id, custodian_pk = pk^{S_j}, custodian_role, operator_pk, shares: Vec }`. Authenticates the binding between operator, custodian, and context. | +| [`OperatorBackupOutput`](../../core/grpc/proto/kms.v1.proto) | gRPC value | A signcryption `(payload, pke_type, signing_type)`. Plaintext is `BackupMaterial`. Created with `(sk^{P_i}, pk^{E_j})` and the custodian's verf-key ID as `receiver_id`. | +| [`RecoveryValidationMaterial`](../../core/service/src/backup/operator.rs) | Operator's **public** storage at `custodian_context_id` | Operator-signed `{ cts: BTreeMap, commitments: BTreeMap, custodian_context: InternalCustodianContext, mpc_context }`. Lets the recovering operator re-fetch the original signcryptions and verify them against the operator-signed commitments. | +| `BackupCiphertext` | Backup vault | Long-term private material (signing key, threshold FHE keys, custodian context, …) encrypted under `pk^{B}` (`backup_enc_key`). Tagged with `RequestId` + `PrivDataType` — see [ARCHITECTURE.md](../../ai-docs/ARCHITECTURE.md#backup-and-recovery). | +| [`RecoveryRequest`](../../core/grpc/proto/kms.v1.proto) | Result of `CustodianRecoveryInit` (operator → core-client → custodian's `--recovery-request-path`) | `{ ephem_op_enc_key = pk^{e_i}, cts: map }`. Carries (a) the operator's ephemeral encryption key for this recovery session and (b) the same signcrypted shares the operator stored at backup time. | +| [`InternalCustodianRecoveryOutput`](../../core/service/src/backup/custodian.rs) | Custodian's `--output-path` file → core-client | `{ signcryption, custodian_role }`. The signcryption is the **custodian → recovering-operator** envelope, made with `(sk^{S_j}, pk^{e_i})` over the same `BackupMaterial`. | +| [`CustodianRecoveryOutput`](../../core/grpc/proto/kms.v1.proto) | gRPC payload | Wire form of `InternalCustodianRecoveryOutput`: `{ backup_output, custodian_role }`. | +| [`CustodianRecoveryRequest`](../../core/grpc/proto/kms.v1.proto) | core-client → operator gRPC | `{ custodian_context_id, custodian_recovery_outputs: [CustodianRecoveryOutput] }`. | + +### Protocol overview + +The diagram below shows every message that crosses a party boundary in the six phases. Internal computation (how each party builds or validates a message) is described in the prose under each phase; here we focus only on which object is sent where. + +```mermaid +sequenceDiagram + autonumber + participant Cus as Custodian B_j (air-gapped) + participant Cli as core-client + participant Op as Operator P_i / P_i' + participant Pub as Public storage + participant Vault as Backup vault + + rect rgba(200, 230, 255, 0.25) + Note over Cus, Vault: Phase 1 — Custodian setup + Cus->>Cli: CustodianSetupMessage (file, out-of-band) + end + + rect rgba(220, 240, 220, 0.25) + Note over Cus, Vault: Phase 2 — Custodian context creation + Cli->>Op: NewCustodianContextRequest + Op->>Pub: RecoveryValidationMaterial + Op->>Vault: BackupCiphertext (InternalCustodianContext) + end + + rect rgba(245, 235, 200, 0.25) + Note over Cus, Vault: Phase 3 — Ongoing backup + Op->>Vault: BackupCiphertext (continuous, per PrivDataType write) + end + + rect rgba(255, 220, 220, 0.25) + Note over Cus, Vault: Phase 4 — Recovery init + Cli->>Op: CustodianRecoveryInitRequest + Pub-->>Op: RecoveryValidationMaterial + Op-->>Cli: RecoveryRequest + end + + rect rgba(230, 220, 245, 0.25) + Note over Cus, Vault: Phase 5 — Custodian re-encryption + Cli->>Cus: RecoveryRequest (+ operator verf-key, out-of-band) + Cus-->>Cli: InternalCustodianRecoveryOutput + end + + rect rgba(220, 245, 240, 0.25) + Note over Cus, Vault: Phase 6 — Recovery finalization + Cli->>Op: CustodianRecoveryRequest + Pub-->>Op: RecoveryValidationMaterial + Cli->>Op: RestoreFromBackup + Vault-->>Op: BackupCiphertext (per PrivDataType, in a loop) + end ``` -IMPORTANT: IT IS NOT POSSIBLE FOR THE CUSTODIAN TO VALIDATE THE AUTHENTICITY OF A REQUEST! HENCE IT IS PARAMOUNT THAT IT IS VALIDATED OUT-OF-BOUNDS, E.G. THROUGH A DIGEST ON A BLOCKCHAIN. +### Phase 1 — Custodian setup (offline, one-time per custodian) + +Corresponds to [`kms-custodian generate`](#custodian-setup). A future custodian boots the air-gapped machine and runs the command. Keys are derived from system entropy mixed with a user-supplied `--randomness` string; the matching BIP39 seed phrase is printed to stdout **once** and must be copied onto paper. + +The seed phrase is the only durable secret the custodian holds. `sk^{E_j}` and `sk^{S_j}` are re-derived from it whenever the custodian participates in a recovery. + +### Phase 2 — Custodian context creation (online, one-time per context) + +The core-client gathers `n` `CustodianSetupMessage`s (from each custodian's `--path` file), picks a corruption threshold `t < n/2`, and issues a `NewCustodianContext` gRPC call to every operator in the KMS cluster. Each operator, independently: generates a per-context backup keypair `(sk^{B}, pk^{B})`, Shamir-shares `sk^{B}` into `n` shares, builds and signcrypts one `BackupMaterial` per custodian role, computes a commitment over each `BackupMaterial`, packages everything into a signed `RecoveryValidationMaterial` (written to the operator's own public storage at `request_id = custodian_context_id`), and encrypts the resulting `InternalCustodianContext` into a `BackupCiphertext` for the backup vault. After secret-sharing, the operator drops `sk^{B}` and installs `pk^{B}` in its `SecretSharing` keychain. + +Notes: +- The Shamir threshold encoded inside `RecoveryValidationMaterial.custodian_context.threshold` is the **recovery** threshold (`t + 1` shares needed). `Operator::new_for_sharing` enforces `t < n/2`. +- `sk^{B}` is **only** held in memory during this RPC. After secret-sharing it, the operator drops it. +- The commitment `c_j = H(BackupMaterial_j)` is what the recovering operator later checks against the decrypted material — it lets a single signature on `RecoveryValidationMaterial` authenticate every share at once, without making the encrypted plaintext public. + +### Phase 3 — Ongoing backup (whenever the operator writes private material) + +When the operator writes any `PrivDataType` (signing key, threshold FHE keys, custodian context itself, etc.) to private storage, the `SecretSharing` keychain encrypts the data under `pk^{B}` and stores the resulting `BackupCiphertext` in the backup vault, tagged with the originating `RequestId` and `PrivDataType`. The custodians never see this material — only `pk^{B}` matters here. + +This phase is invisible to custodians and to the core-client. It runs continuously for the life of the operator. + +### Phase 4 — Recovery init (operator's private storage is gone) + +The recovering operator boots against the same **public** storage and backup vault but with empty private storage. It calls `CustodianRecoveryInit`, generates an ephemeral encryption keypair `(sk^{e_i}, pk^{e_i})` pinned in process memory, reads `RecoveryValidationMaterial` from public storage at `ctx_id`, verifies the operator's signature on it, and returns a `RecoveryRequest` to the core-client (which writes it to disk for later distribution to the custodians). + +The recovering operator has the same long-term signing key as the original (recovered out of band from public storage), so `RecoveryValidationMaterial`'s signature still verifies. + +`sk^{e_i}` lives only in process memory; restarting the server discards it. `overwrite_ephemeral_key=true` lets a stuck recovery be re-initiated. + +### Phase 5 — Custodian re-encryption (offline, manual) + +Corresponds to [`kms-custodian decrypt`](#recovery-decryption-of-backup). The core-client (or operator's human operator) distributes the `RecoveryRequest` and the recovering operator's verification key out-of-band to each custodian's air-gapped machine. The custodian boots, types in the seed phrase, and runs the command: re-derives `(sk^{E_j}, sk^{S_j})` from the seed phrase, unsigncrypts its share of `BackupMaterial` from `cts[j]`, sanity-checks the metadata inside and then re-signcrypts the same `BackupMaterial` to the operator's ephemeral key `pk^{e_i}`, and writes the resulting `InternalCustodianRecoveryOutput` to `--output-path`. + +The custodian's only cryptographic obligation is "decrypt your share and re-signcrypt it for the operator's ephemeral key". The custodian can't (and isn't asked to) judge whether this request is legitimate — see the warning at the top of the [Recovery](#recovery-decryption-of-backup) section. Furthermore, observe that the way the custodian receives the operator's recovery request and material, is through an out-of-band channel (e.g. Slack and/or Signal). + +### Phase 6 — Recovery finalization (operator reconstructs) + +For each operator that needs recovery, their core-client collects `t + 1` (or more) custodian output files and sends them, in a single `CustodianRecoveryRequest`, and sends this to the KMS core. The recovering operator re-reads `RecoveryValidationMaterial` from public storage and validates each `CustodianRecoveryOutput`, and once at least `t + 1` shares pass — Shamir-reconstructs `sk^{B}` and installs it in the `SecretSharing` keychain. A subsequent `RestoreFromBackup` call then iterates every `BackupCiphertext` in the backup vault, decrypts each with `sk^{B}`, and writes the plaintext into the now-empty private storage. + +After Phase 6 the recovering operator's private storage is repopulated and the node resumes normal service. The operator-side commands that drive Phases 4 and 6 are documented in [docs/guides/core_client.md](core_client.md). diff --git a/docs/guides/core_client.md b/docs/guides/core_client.md index 187733b506..6568a168f9 100644 --- a/docs/guides/core_client.md +++ b/docs/guides/core_client.md @@ -309,20 +309,20 @@ To further make this a manual test, make sure a [key is generated](#Key-generati IMPORTANT: DO NOT CHANGE THE NAME OF THE VERIFICATION KEY! THAT IS, THE KEY FILE MUST KEEP THE NAME WHEN FETCHING IT FROM THE OPERATOR! Then execute the following command in root of the KMS project, replacing the seed_phrases with the appropriate ones learned from step 1: ```{bash} - cargo run --bin kms-custodian decrypt --seed-phrase "prosper wool oak moon light situate end palm sick monster clever solid" --randomness 123 --custodian-role 1 --recovery-request-path core-client/tests/data/keys/CUSTODIAN/recovery/1 --operator-verf-key core-client/tests/data/keys/PUB-p1/VerfKey/60b7070add74be3827160aa635fb255eeeeb88586c4debf7ab1134ddceb4beee --mpc-context-id 0x0700000000000000000000000000000000000000000000000000000000000001 --output-path core-client/tests/data/keys/CUSTODIAN/response/recovery-response-1-1 - cargo run --bin kms-custodian decrypt --seed-phrase "prosper wool oak moon light situate end palm sick monster clever solid" --randomness 123 --custodian-role 1 --recovery-request-path core-client/tests/data/keys/CUSTODIAN/recovery/2 --operator-verf-key core-client/tests/data/keys/PUB-p2/VerfKey/60b7070add74be3827160aa635fb255eeeeb88586c4debf7ab1134ddceb4beee --mpc-context-id 0x0700000000000000000000000000000000000000000000000000000000000001 --output-path core-client/tests/data/keys/CUSTODIAN/response/recovery-response-2-1 - cargo run --bin kms-custodian decrypt --seed-phrase "prosper wool oak moon light situate end palm sick monster clever solid" --randomness 123 --custodian-role 1 --recovery-request-path core-client/tests/data/keys/CUSTODIAN/recovery/3 --operator-verf-key core-client/tests/data/keys/PUB-p3/VerfKey/60b7070add74be3827160aa635fb255eeeeb88586c4debf7ab1134ddceb4beee --mpc-context-id 0x0700000000000000000000000000000000000000000000000000000000000001 --output-path core-client/tests/data/keys/CUSTODIAN/response/recovery-response-3-1 - cargo run --bin kms-custodian decrypt --seed-phrase "prosper wool oak moon light situate end palm sick monster clever solid" --randomness 123 --custodian-role 1 --recovery-request-path core-client/tests/data/keys/CUSTODIAN/recovery/4 --operator-verf-key core-client/tests/data/keys/PUB-p4/VerfKey/60b7070add74be3827160aa635fb255eeeeb88586c4debf7ab1134ddceb4beee --mpc-context-id 0x0700000000000000000000000000000000000000000000000000000000000001 --output-path core-client/tests/data/keys/CUSTODIAN/response/recovery-response-4-1 - - cargo run --bin kms-custodian decrypt --seed-phrase "swallow around patrol toe bottom very pulse habit boy couch guide vendor" --randomness 123 --custodian-role 2 --recovery-request-path core-client/tests/data/keys/CUSTODIAN/recovery/1 --operator-verf-key core-client/tests/data/keys/PUB-p1/VerfKey/60b7070add74be3827160aa635fb255eeeeb88586c4debf7ab1134ddceb4beee --mpc-context-id 0x0700000000000000000000000000000000000000000000000000000000000001 --output-path core-client/tests/data/keys/CUSTODIAN/response/recovery-response-1-2 - cargo run --bin kms-custodian decrypt --seed-phrase "swallow around patrol toe bottom very pulse habit boy couch guide vendor" --randomness 123 --custodian-role 2 --recovery-request-path core-client/tests/data/keys/CUSTODIAN/recovery/2 --operator-verf-key core-client/tests/data/keys/PUB-p2/VerfKey/60b7070add74be3827160aa635fb255eeeeb88586c4debf7ab1134ddceb4beee --mpc-context-id 0x0700000000000000000000000000000000000000000000000000000000000001 --output-path core-client/tests/data/keys/CUSTODIAN/response/recovery-response-2-2 - cargo run --bin kms-custodian decrypt --seed-phrase "swallow around patrol toe bottom very pulse habit boy couch guide vendor" --randomness 123 --custodian-role 2 --recovery-request-path core-client/tests/data/keys/CUSTODIAN/recovery/3 --operator-verf-key core-client/tests/data/keys/PUB-p3/VerfKey/60b7070add74be3827160aa635fb255eeeeb88586c4debf7ab1134ddceb4beee --mpc-context-id 0x0700000000000000000000000000000000000000000000000000000000000001 --output-path core-client/tests/data/keys/CUSTODIAN/response/recovery-response-3-2 - cargo run --bin kms-custodian decrypt --seed-phrase "swallow around patrol toe bottom very pulse habit boy couch guide vendor" --randomness 123 --custodian-role 2 --recovery-request-path core-client/tests/data/keys/CUSTODIAN/recovery/4 --operator-verf-key core-client/tests/data/keys/PUB-p4/VerfKey/60b7070add74be3827160aa635fb255eeeeb88586c4debf7ab1134ddceb4beee --mpc-context-id 0x0700000000000000000000000000000000000000000000000000000000000001 --output-path core-client/tests/data/keys/CUSTODIAN/response/recovery-response-4-2 - - cargo run --bin kms-custodian decrypt --seed-phrase "two often advance excite shiver speed vessel melt panther fiction giraffe voyage" --randomness 123 --custodian-role 3 --recovery-request-path core-client/tests/data/keys/CUSTODIAN/recovery/1 --operator-verf-key core-client/tests/data/keys/PUB-p1/VerfKey/60b7070add74be3827160aa635fb255eeeeb88586c4debf7ab1134ddceb4beee --mpc-context-id 0x0700000000000000000000000000000000000000000000000000000000000001 --output-path core-client/tests/data/keys/CUSTODIAN/response/recovery-response-1-3 - cargo run --bin kms-custodian decrypt --seed-phrase "two often advance excite shiver speed vessel melt panther fiction giraffe voyage" --randomness 123 --custodian-role 3 --recovery-request-path core-client/tests/data/keys/CUSTODIAN/recovery/2 --operator-verf-key core-client/tests/data/keys/PUB-p2/VerfKey/60b7070add74be3827160aa635fb255eeeeb88586c4debf7ab1134ddceb4beee --mpc-context-id 0x0700000000000000000000000000000000000000000000000000000000000001 --output-path core-client/tests/data/keys/CUSTODIAN/response/recovery-response-2-3 - cargo run --bin kms-custodian decrypt --seed-phrase "two often advance excite shiver speed vessel melt panther fiction giraffe voyage" --randomness 123 --custodian-role 3 --recovery-request-path core-client/tests/data/keys/CUSTODIAN/recovery/3 --operator-verf-key core-client/tests/data/keys/PUB-p3/VerfKey/60b7070add74be3827160aa635fb255eeeeb88586c4debf7ab1134ddceb4beee --mpc-context-id 0x0700000000000000000000000000000000000000000000000000000000000001 --output-path core-client/tests/data/keys/CUSTODIAN/response/recovery-response-3-3 - cargo run --bin kms-custodian decrypt --seed-phrase "two often advance excite shiver speed vessel melt panther fiction giraffe voyage" --randomness 123 --custodian-role 3 --recovery-request-path core-client/tests/data/keys/CUSTODIAN/recovery/4 --operator-verf-key core-client/tests/data/keys/PUB-p4/VerfKey/60b7070add74be3827160aa635fb255eeeeb88586c4debf7ab1134ddceb4beee --mpc-context-id 0x0700000000000000000000000000000000000000000000000000000000000001 --output-path core-client/tests/data/keys/CUSTODIAN/response/recovery-response-4-3 + cargo run --bin kms-custodian decrypt --seed-phrase "prosper wool oak moon light situate end palm sick monster clever solid" --randomness 123 --custodian-role 1 --recovery-request-path core-client/tests/data/keys/CUSTODIAN/recovery/1 --operator-verf-key core-client/tests/data/keys/PUB-p1/VerfKey/60b7070add74be3827160aa635fb255eeeeb88586c4debf7ab1134ddceb4beee --output-path core-client/tests/data/keys/CUSTODIAN/response/recovery-response-1-1 + cargo run --bin kms-custodian decrypt --seed-phrase "prosper wool oak moon light situate end palm sick monster clever solid" --randomness 123 --custodian-role 1 --recovery-request-path core-client/tests/data/keys/CUSTODIAN/recovery/2 --operator-verf-key core-client/tests/data/keys/PUB-p2/VerfKey/60b7070add74be3827160aa635fb255eeeeb88586c4debf7ab1134ddceb4beee --output-path core-client/tests/data/keys/CUSTODIAN/response/recovery-response-2-1 + cargo run --bin kms-custodian decrypt --seed-phrase "prosper wool oak moon light situate end palm sick monster clever solid" --randomness 123 --custodian-role 1 --recovery-request-path core-client/tests/data/keys/CUSTODIAN/recovery/3 --operator-verf-key core-client/tests/data/keys/PUB-p3/VerfKey/60b7070add74be3827160aa635fb255eeeeb88586c4debf7ab1134ddceb4beee --output-path core-client/tests/data/keys/CUSTODIAN/response/recovery-response-3-1 + cargo run --bin kms-custodian decrypt --seed-phrase "prosper wool oak moon light situate end palm sick monster clever solid" --randomness 123 --custodian-role 1 --recovery-request-path core-client/tests/data/keys/CUSTODIAN/recovery/4 --operator-verf-key core-client/tests/data/keys/PUB-p4/VerfKey/60b7070add74be3827160aa635fb255eeeeb88586c4debf7ab1134ddceb4beee --output-path core-client/tests/data/keys/CUSTODIAN/response/recovery-response-4-1 + + cargo run --bin kms-custodian decrypt --seed-phrase "swallow around patrol toe bottom very pulse habit boy couch guide vendor" --randomness 123 --custodian-role 2 --recovery-request-path core-client/tests/data/keys/CUSTODIAN/recovery/1 --operator-verf-key core-client/tests/data/keys/PUB-p1/VerfKey/60b7070add74be3827160aa635fb255eeeeb88586c4debf7ab1134ddceb4beee --output-path core-client/tests/data/keys/CUSTODIAN/response/recovery-response-1-2 + cargo run --bin kms-custodian decrypt --seed-phrase "swallow around patrol toe bottom very pulse habit boy couch guide vendor" --randomness 123 --custodian-role 2 --recovery-request-path core-client/tests/data/keys/CUSTODIAN/recovery/2 --operator-verf-key core-client/tests/data/keys/PUB-p2/VerfKey/60b7070add74be3827160aa635fb255eeeeb88586c4debf7ab1134ddceb4beee --output-path core-client/tests/data/keys/CUSTODIAN/response/recovery-response-2-2 + cargo run --bin kms-custodian decrypt --seed-phrase "swallow around patrol toe bottom very pulse habit boy couch guide vendor" --randomness 123 --custodian-role 2 --recovery-request-path core-client/tests/data/keys/CUSTODIAN/recovery/3 --operator-verf-key core-client/tests/data/keys/PUB-p3/VerfKey/60b7070add74be3827160aa635fb255eeeeb88586c4debf7ab1134ddceb4beee --output-path core-client/tests/data/keys/CUSTODIAN/response/recovery-response-3-2 + cargo run --bin kms-custodian decrypt --seed-phrase "swallow around patrol toe bottom very pulse habit boy couch guide vendor" --randomness 123 --custodian-role 2 --recovery-request-path core-client/tests/data/keys/CUSTODIAN/recovery/4 --operator-verf-key core-client/tests/data/keys/PUB-p4/VerfKey/60b7070add74be3827160aa635fb255eeeeb88586c4debf7ab1134ddceb4beee --output-path core-client/tests/data/keys/CUSTODIAN/response/recovery-response-4-2 + + cargo run --bin kms-custodian decrypt --seed-phrase "two often advance excite shiver speed vessel melt panther fiction giraffe voyage" --randomness 123 --custodian-role 3 --recovery-request-path core-client/tests/data/keys/CUSTODIAN/recovery/1 --operator-verf-key core-client/tests/data/keys/PUB-p1/VerfKey/60b7070add74be3827160aa635fb255eeeeb88586c4debf7ab1134ddceb4beee --output-path core-client/tests/data/keys/CUSTODIAN/response/recovery-response-1-3 + cargo run --bin kms-custodian decrypt --seed-phrase "two often advance excite shiver speed vessel melt panther fiction giraffe voyage" --randomness 123 --custodian-role 3 --recovery-request-path core-client/tests/data/keys/CUSTODIAN/recovery/2 --operator-verf-key core-client/tests/data/keys/PUB-p2/VerfKey/60b7070add74be3827160aa635fb255eeeeb88586c4debf7ab1134ddceb4beee --output-path core-client/tests/data/keys/CUSTODIAN/response/recovery-response-2-3 + cargo run --bin kms-custodian decrypt --seed-phrase "two often advance excite shiver speed vessel melt panther fiction giraffe voyage" --randomness 123 --custodian-role 3 --recovery-request-path core-client/tests/data/keys/CUSTODIAN/recovery/3 --operator-verf-key core-client/tests/data/keys/PUB-p3/VerfKey/60b7070add74be3827160aa635fb255eeeeb88586c4debf7ab1134ddceb4beee --output-path core-client/tests/data/keys/CUSTODIAN/response/recovery-response-3-3 + cargo run --bin kms-custodian decrypt --seed-phrase "two often advance excite shiver speed vessel melt panther fiction giraffe voyage" --randomness 123 --custodian-role 3 --recovery-request-path core-client/tests/data/keys/CUSTODIAN/recovery/4 --operator-verf-key core-client/tests/data/keys/PUB-p4/VerfKey/60b7070add74be3827160aa635fb255eeeeb88586c4debf7ab1134ddceb4beee --output-path core-client/tests/data/keys/CUSTODIAN/response/recovery-response-4-3 ``` 5. KMS nodes recover the backup decryption key. Execute the following from `core-client` replacing the ID following `-i` with the appropriate ID learned in step 3. @@ -642,7 +642,7 @@ $ cargo run -- -f destroy-mpc-epoch --epoch-id