Add project files.

This commit is contained in:
micro
2026-06-20 09:31:59 -05:00
parent 9bb3f00d86
commit 4baa4ce8c0
346 changed files with 55751 additions and 0 deletions
+309
View File
@@ -0,0 +1,309 @@
/*
* Copyright 2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
syntax = "proto3";
package signal;
option java_package = "org.signal.storageservice.storage.protos.groups";
option java_outer_classname = "GroupProtos";
option java_multiple_files = true;
message AvatarUploadAttributes {
string key = 1;
string credential = 2;
string acl = 3;
string algorithm = 4;
string date = 5;
string policy = 6;
string signature = 7;
}
// Stored data
message Member {
enum Role {
UNKNOWN = 0;
DEFAULT = 1;
ADMINISTRATOR = 2;
}
bytes userId = 1;
Role role = 2;
bytes profileKey = 3;
bytes presentation = 4;
uint32 joinedAtVersion = 5;
bytes labelEmoji = 6; // decrypts to a UTF-8 string
bytes labelString = 7; // decrypts to a UTF-8 string
}
message MemberPendingProfileKey {
Member member = 1;
bytes addedByUserId = 2;
uint64 timestamp = 3; // ms since epoch
}
message MemberPendingAdminApproval {
bytes userId = 1;
bytes profileKey = 2;
bytes presentation = 3;
uint64 timestamp = 4; // ms since epoch
}
message MemberBanned {
bytes userId = 1;
uint64 timestamp = 2; // ms since epoch
}
message AccessControl {
enum AccessRequired {
UNKNOWN = 0;
ANY = 1;
MEMBER = 2;
ADMINISTRATOR = 3;
UNSATISFIABLE = 4;
}
AccessRequired attributes = 1;
AccessRequired members = 2;
AccessRequired addFromInviteLink = 3;
AccessRequired memberLabel = 4;
}
message Group {
bytes publicKey = 1;
bytes title = 2;
bytes description = 11;
// The URL for this group's avatar. The content at this URL can be
// decrypted/deserialized into a `GroupAttributeBlob`.
string avatarUrl = 3;
bytes disappearingMessagesTimer = 4;
AccessControl accessControl = 5;
uint32 version = 6;
repeated Member members = 7;
repeated MemberPendingProfileKey membersPendingProfileKey = 8;
repeated MemberPendingAdminApproval membersPendingAdminApproval = 9;
bytes inviteLinkPassword = 10;
bool announcements_only = 12;
repeated MemberBanned members_banned = 13;
bool terminated = 14;
// next: 15
}
message GroupAttributeBlob {
oneof content {
string title = 1;
bytes avatar = 2;
uint32 disappearingMessagesDuration = 3;
string descriptionText = 4;
}
}
message GroupInviteLink {
message GroupInviteLinkContentsV1 {
bytes groupMasterKey = 1;
bytes inviteLinkPassword = 2;
}
oneof contents {
GroupInviteLinkContentsV1 contentsV1 = 1;
}
}
message GroupJoinInfo {
bytes publicKey = 1;
bytes title = 2;
bytes description = 8;
string avatar = 3;
uint32 memberCount = 4;
AccessControl.AccessRequired addFromInviteLink = 5;
uint32 version = 6;
bool pendingAdminApproval = 7;
// bool pendingAdminApprovalFull = 9;
// next: 10
}
// Deltas
message GroupChange {
message Actions {
message AddMemberAction {
Member added = 1;
bool joinFromInviteLink = 2;
}
message DeleteMemberAction {
bytes deletedUserId = 1;
}
message ModifyMemberRoleAction {
bytes userId = 1;
Member.Role role = 2;
}
message ModifyMemberLabelAction {
bytes userId = 1;
bytes labelEmoji = 2; // decrypts to a UTF-8 string
bytes labelString = 3; // decrypts to a UTF-8 string
}
message ModifyMemberProfileKeyAction {
bytes presentation = 1;
bytes user_id = 2;
bytes profile_key = 3;
}
message AddMemberPendingProfileKeyAction {
MemberPendingProfileKey added = 1;
}
message DeleteMemberPendingProfileKeyAction {
bytes deletedUserId = 1;
}
message PromoteMemberPendingProfileKeyAction {
bytes presentation = 1;
bytes user_id = 2;
bytes profile_key = 3;
}
message PromoteMemberPendingPniAciProfileKeyAction {
bytes presentation = 1;
bytes user_id = 2;
bytes pni = 3;
bytes profile_key = 4;
}
message AddMemberPendingAdminApprovalAction {
MemberPendingAdminApproval added = 1;
}
message DeleteMemberPendingAdminApprovalAction {
bytes deletedUserId = 1;
}
message PromoteMemberPendingAdminApprovalAction {
bytes userId = 1;
Member.Role role = 2;
}
message AddMemberBannedAction {
MemberBanned added = 1;
}
message DeleteMemberBannedAction {
bytes deletedUserId = 1;
}
message ModifyTitleAction {
bytes title = 1;
}
message ModifyDescriptionAction {
bytes description = 1;
}
message ModifyAvatarAction {
string avatar = 1;
}
message ModifyDisappearingMessageTimerAction {
bytes timer = 1;
}
message ModifyAttributesAccessControlAction {
AccessControl.AccessRequired attributesAccess = 1;
}
message ModifyMembersAccessControlAction {
AccessControl.AccessRequired membersAccess = 1;
}
message ModifyAddFromInviteLinkAccessControlAction {
AccessControl.AccessRequired addFromInviteLinkAccess = 1;
}
message ModifyMemberLabelAccessControlAction {
AccessControl.AccessRequired memberLabelAccess = 1;
}
message ModifyInviteLinkPasswordAction {
bytes inviteLinkPassword = 1;
}
message ModifyAnnouncementsOnlyAction {
bool announcements_only = 1;
}
message TerminateGroupAction {}
bytes sourceUserId = 1;
// clients should not provide this value; the server will provide it in the response buffer to ensure the signature is binding to a particular group
// if clients set it during a request the server will respond with 400.
bytes group_id = 25;
uint32 version = 2;
repeated AddMemberAction addMembers = 3;
repeated DeleteMemberAction deleteMembers = 4;
repeated ModifyMemberRoleAction modifyMemberRoles = 5;
repeated ModifyMemberProfileKeyAction modifyMemberProfileKeys = 6;
repeated AddMemberPendingProfileKeyAction addMembersPendingProfileKey = 7;
repeated DeleteMemberPendingProfileKeyAction deleteMembersPendingProfileKey = 8;
repeated PromoteMemberPendingProfileKeyAction promoteMembersPendingProfileKey = 9;
ModifyTitleAction modifyTitle = 10;
ModifyAvatarAction modifyAvatar = 11;
ModifyDisappearingMessageTimerAction modifyDisappearingMessageTimer = 12;
ModifyAttributesAccessControlAction modifyAttributesAccess = 13;
ModifyMembersAccessControlAction modifyMemberAccess = 14;
ModifyAddFromInviteLinkAccessControlAction modifyAddFromInviteLinkAccess = 15; // change epoch = 1
repeated AddMemberPendingAdminApprovalAction addMembersPendingAdminApproval = 16; // change epoch = 1
repeated DeleteMemberPendingAdminApprovalAction deleteMembersPendingAdminApproval = 17; // change epoch = 1
repeated PromoteMemberPendingAdminApprovalAction promoteMembersPendingAdminApproval = 18; // change epoch = 1
ModifyInviteLinkPasswordAction modifyInviteLinkPassword = 19; // change epoch = 1
ModifyDescriptionAction modifyDescription = 20; // change epoch = 2
ModifyAnnouncementsOnlyAction modify_announcements_only = 21; // change epoch = 3
repeated AddMemberBannedAction add_members_banned = 22; // change epoch = 4
repeated DeleteMemberBannedAction delete_members_banned = 23; // change epoch = 4
repeated PromoteMemberPendingPniAciProfileKeyAction promote_members_pending_pni_aci_profile_key = 24; // change epoch = 5
repeated ModifyMemberLabelAction modifyMemberLabels = 26; // change epoch = 6;
ModifyMemberLabelAccessControlAction modifyMemberLabelAccess = 27; // change epoch = 6
TerminateGroupAction terminate_group = 28; // change epoch = 7
// next: 29
}
bytes actions = 1;
bytes serverSignature = 2;
uint32 changeEpoch = 3;
}
// External credentials
message ExternalGroupCredential {
string token = 1;
}
// API responses
message GroupResponse {
Group group = 1;
bytes group_send_endorsements_response = 2;
}
message GroupChanges {
message GroupChangeState {
GroupChange groupChange = 1;
Group groupState = 2;
}
repeated GroupChangeState groupChanges = 1;
bytes group_send_endorsements_response = 2;
}
message GroupChangeResponse {
GroupChange group_change = 1;
bytes group_send_endorsements_response = 2;
}
File diff suppressed because it is too large Load Diff
+447
View File
@@ -0,0 +1,447 @@
//
// Copyright 2020-2022 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use const_str::hex;
use curve25519_dalek_signal::ristretto::RistrettoPoint;
use sha2::Sha256;
use zkgroup::{SECONDS_PER_DAY, Timestamp};
/// Simple wrapper around `assert_eq` that prints the hex-encoded values on
/// failure.
macro_rules! assert_hex_eq {
($lhs:expr, $rhs: expr) => {
assert_eq!(
&$lhs,
&$rhs,
"{} = {}, {} = {}",
stringify!($lhs),
hex::encode(&$lhs),
stringify!($rhs),
hex::encode(&$rhs),
);
};
}
#[test]
fn test_lizard() {
let p = RistrettoPoint::lizard_encode::<Sha256>(&zkgroup::common::constants::TEST_ARRAY_16);
let data_out = p.lizard_decode::<Sha256>();
assert_hex_eq!(data_out.unwrap(), zkgroup::common::constants::TEST_ARRAY_16);
}
const AUTH_CREDENTIAL_PRESENTATION_V1: &[u8] = &hex!(
"000cde979737ed30bbeb16362e4e076945ce02069f727b0ed4c3c33c011e82546e1cdf081fbdf37c03a851ad060bdcbf6378cb4cb16dc3154d08de5439b5323203729d1841b517033af2fd177d30491c138ae723655734f6e5cc01c00696f4e92096d8c33df26ba2a820d42e9735d30f8eeef96d399079073c099f7035523bfe716638659319d3c36ad34c00ef8850f663c4d93030235074312a8878b6a5c5df4fbc7d32935278bfa5996b44ab75d6f06f4c30b98640ad5de74742656c8977567de000000000000000fde69f82ad2dcb4909650ac6b2573841af568fef822b32b45f625a764691a704d11b6f385261468117ead57fa623338e21c66ed846ab65809fcac158066d8e0e444077b99540d886e7dc09555dd6faea2cd3697f1e089f82d54e5d0fe4a185008b5cbc3979391ad71686bc03be7b00ea7e42c08d9f1d75c3a56c27ae2467b80636c0b5343eda7cd578ba88ddb7a0766568477fed63cf531862122c6c15b4a707973d41782cfc0ef4fe6c3115988a2e339015938d2df0a5d30237a2592cc10c05a9e4ef6b695bca99736b1a49ea39606a381ecfb05efe60d28b54823ec5a3680c765de9df4cfa5487f360e29e99343e91811baec331c4680985e608ca5d408e21725c6aa1b61d5a8b48d75f4aaa9a3cbe88d3e0f1a54319081f77c72c8f52547440e20100"
);
const AUTH_CREDENTIAL_PRESENTATION_V4_RESULT: &[u8] = &hex!(
"035e3e79afda8dc0d489fcf7c78f71e1502f2e06e8aeb20149046f85b3004d3f7f982d57dfad49cd1e6c335755cef4cc5e8d3de1eb4f5e8f24d71cf9f2220ae750f47181d71aaabbd48a1916813ec08eea935eb013395bf72f9139da8ef4f9530d05000000000000007a080544e6ee8ee2ff0dc298f18841103a9f9ec38631df8682e241755f86f74e26301872f4f32a9bb80f5b17651c0c83253a8013532384061a1febf79e58e60fa215b31da678305fa2a271655e35824630d0804680ec0bf29b1c775652683c3a5cec537c3514df730f267371d909f29cc6252af30afe3ea846c0cf56478bdc5b7a7f983ea7c24ecef4b371286a6414b2c38a57a7f59a9df33e430736c1a2ca14e00000000000000015416082ff7e3a741a4c3c31be3c95d4a31f2cf742685e0b17cd7f7205230e0e4e67b4b6ed45e705de13a1cb7170897bb32c9db6f9a1108fddfc7fae9eb2ca0c5fc3d8ccbd79d992eeed333626a1f0c37f0b25625955611e5ba33c782c50550045923582280cd93c3e9555b4e36eec20993f60b6aeb9ddb7f2856c4659546f037b33534a0292c77a501a70796f24ff37c8311bdfea8bb6c78f909563fe6e3b0386f36adc92090694ebb106a837bac046ad26e2472ee16408e9fd84269fd78c00c5dde91fcf202a6afad3441b9e2a34f4831d5bf560c81b38d951cb7c88e4d701765de9df4cfa5487f360e29e99343e91811baec331c4680985e608ca5d408e21725c6aa1b61d5a8b48d75f4aaa9a3cbe88d3e0f1a54319081f77c72c8f525474de749a0fef17b06bbce74ca5d0f7a0c45f443a1901f1e3e016d3548e50a2fa19ce6b27ac467ed9c9f5018b2a4456b6c2b1a91454422fdd473c9636a8459e1c170060c77b02000000"
);
const PROFILE_KEY_CREDENTIAL_PRESENTATION_V1: &[u8] = &hex!(
"00c4d19bca1ae844585168869da4133e0e0bb59f2ce17b7ac65bff5da9610eca103429d8022a94bae2b5b1057b5595b8ad70bfc2d0e1ad662cb75e6bae0782be6f00e3db793bc28561f0196c2e74da6f303fa8bcb70c94096671b73f7b3a95fb002200d5b9180fa0ef7d3014d01344145b4d38480d72ff25c24294e305e5705072e0d32cc4e84f5caf31486089a4b934c80c92eba43472ff23a5af93c397535d33801f0e6fc6eb2ee0d117f03bb4fd38a8b9c88d94708131f38742ca804a3cfc4f9476bc2d03f53d17001c36478afbe9cc535a224b2df6b2b08bef06cbc7d4dc42ccfc3459f7ac5c4419ae9f3c8a161d554d047778943216240858da3b1101984c40010000000000007a01eea6b2adad14d71ab8b8e411bef3c596e954b70e4031570cb1abd7e932083241f1caca3116708fa4319fbbdfe351376c23644ae09a42f0155db4996c9d0c7ffc8521c1914c0e1a20ae51e65df64dd5e6e5985b3d9d31732046d2d77f9c08aaccf056b84026073976eec6164cbdaee5d9e76e497f0c290af681cabd5c5101282abb26c3680d6087ce053310fe8a94f59d8ae23caac5fc0ed0c379888abf028a6f29f89d4fe2acc1706341b2245ba1885bca57e1e27ccf7ed79371500965009f960c2ba00fad3e93383b87ce119cac0b3360eb99284ce78e2cbed680f7960373e0ab75c190254160c2353614109489e653c9b2e1c93f92c7c5ad583d987a04bd3541b24485c33ea49bac43c87c4ab3efde2e2d7ec10a40be544199f925b20b2c55542bc56410571e41cd8e0286f609a66768b5061ccb4777af32309928dd09765de9df4cfa5487f360e29e99343e91811baec331c4680985e608ca5d408e21725c6aa1b61d5a8b48d75f4aaa9a3cbe88d3e0f1a54319081f77c72c8f52547448c03ab4afbf6b8fb0e126c037a0ad4094600dd0e0634d76f88c21087f3cfb485a89bc1e3abc4c95041d1d170eccf02933ec5393d4be1dc573f83c33d3b9a746"
);
const PROFILE_KEY_CREDENTIAL_PRESENTATION_V2_RESULT: [u8;
zkgroup::PROFILE_KEY_CREDENTIAL_PRESENTATION_V2_LEN] = hex!(
"01e0f49cef4f25c31d1bfdc4a328fd508d2222b6decee2a253cf71e8821e97cc3f86824f79b1884b43c67f854717b1a47f56c8ff50a1c07fddbf4f6e857027d548583b54079dd61d54cdd39cd4acae5f8b3bbfa2bb6b3502b69b36da77addddc145ef254a16f2baec1e3d7e8dc80730bc608fcd0e4d8cfef3330a496380c7ac648686b9c5b914d0a77ee84848aa970b2404450179b4022eef003387f6bdbcba30344cadfd5e3f1677caa2c785f4fefe042a1b2adf4f4b8fa6023e41d704bda901d3a697904770ac46e0e304cf19f91ce9ab0ed1ccad8a6febd72313455f139b9222e9a30a2265c6cd22ee5b907fc95967417a0d8ca338a5ee4d51bba78039c314e4001000000000000749d54772b8137e570157c068a5cfebb464b6c1133c72d9abfda72db421cd00561ac4eecb94313c6912013e32c322ea36743b01814fe919ca84b9aea9c78b10ba021506f7ad8c6625e87e07ce32b559036af6b67e2c0383a643cb93cdc2b9800e90588a18fcc449cd466c28c6db73507d8282dd00808b5927fee3336ed0a2202dfb1e176fece6a4104caa2a866c475209967638ea2f1466847da7301a77b9007dfb332a30e9bbfae8a8398165ec9dd4778214e0d6ed35a34071bdf3b3b19510ff2a617bc53eb0e6b0ddc501db027bb47e4f4127d7a0104945f3d3dc7ec1741038b9b80e2c7f131c519ee26ffcb7cb9d3556cd35a12bef1d4b376fc513197ba00ce8f012a0b374164222ba79a39e74e150813474ca6f87ba705c0f06e7b7068039c5edd9dd1a5ab6793ac211989907686b45650221187d4d59ae492679f3b4308765de9df4cfa5487f360e29e99343e91811baec331c4680985e608ca5d408e21725c6aa1b61d5a8b48d75f4aaa9a3cbe88d3e0f1a54319081f77c72c8f52547448c03ab4afbf6b8fb0e126c037a0ad4094600dd0e0634d76f88c21087f3cfb485a89bc1e3abc4c95041d1d170eccf02933ec5393d4be1dc573f83c33d3b9a746"
);
const PROFILE_KEY_CREDENTIAL_PRESENTATION_V3_RESULT: &[u8] = &hex!(
"02fc58a4f2c9bd736238abfc28890c8b2363d084bee430692f05ee559bd37dea3378949e72b271fe0d815b6d908035106cd670b45892df40780c62c37fae106c41be38371fe042a4d4f697db112972d79204b3d48d1253d3231c22926e107f661d40897cb7fdb4777c1680a57008655db71efaac1f69cd9ddf8cda33b226662d7ba443416281508fcdbb026d63f83168470a83e12803a6d2ee2c907343f2f6b063fe6bf0f17a032fabe61e77e904dfe7d3042125728c1984c86a094a0e3991ba554c1ebf604c14a8b13c384c5c01909656c114b24f9d3615d3b14bde7ce9cf126aca3e073e804b2016f7c5affa158a3a68ed9024c6880ecb441a346e7e91aedd6240010000000000002e70f27fb3f4c58cb40dfe58ce1d122312969426abb0bbb820bfbc5ff61d400a419d5ddb7c30c546427273d4fca3096ee4dd2fd03ccbbd26304ffcfe54fef50db8538177ebc61117a222253b4d4189f795abbde3b3d8a0a72d97b7750e0394010a01b474c3e942ef1ee807e17421689c6ca793c4f30b09c989b8a9679aee130eb034f64a34dbcaf12616970d2c8d58ca715bf5c4d42475fa6a1b82ba31574e072506652253e86cd783e30e1c06d2e861ba864a5373759472b31c5b26a8e46d062b8b5da2ec0a3ba499648e80f307728b7815aa60d167a0a9d01c2d2cbfb0a60ddc9dfc5343564b5f021fd1adba6d2a389e7c331bfffeed2a5d1887634323840574e49255a62d9e00ffc21f56afbb12fb9660e185f979223ec714c01e403a3a0a3276d0ef78182f12c092f5237befe3f0afea7693370788f854ec697e44c9bd02765de9df4cfa5487f360e29e99343e91811baec331c4680985e608ca5d408e21725c6aa1b61d5a8b48d75f4aaa9a3cbe88d3e0f1a54319081f77c72c8f52547448c03ab4afbf6b8fb0e126c037a0ad4094600dd0e0634d76f88c21087f3cfb485a89bc1e3abc4c95041d1d170eccf02933ec5393d4be1dc573f83c33d3b9a7468069160000000000"
);
const PROFILE_KEY_CREDENTIAL_PRESENTATION_V4_RESULT: &[u8] = &hex!(
"03fc58a4f2c9bd736238abfc28890c8b2363d084bee430692f05ee559bd37dea3378949e72b271fe0d815b6d908035106cd670b45892df40780c62c37fae106c41be38371fe042a4d4f697db112972d79204b3d48d1253d3231c22926e107f661d40897cb7fdb4777c1680a57008655db71efaac1f69cd9ddf8cda33b226662d7ba443416281508fcdbb026d63f83168470a83e12803a6d2ee2c907343f2f6b063fe6bf0f17a032fabe61e77e904dfe7d3042125728c1984c86a094a0e3991ba554c1ebf604c14a8b13c384c5c01909656c114b24f9d3615d3b14bde7ce9cf126aca3e073e804b2016f7c5affa158a3a68ed9024c6880ecb441a346e7e91aedd6240010000000000006b4403e6adc3322acd34eea13fcc1ae971aab14386fa9b4d85ba0490dfca2f0d21de78e92719ad40a6a49d7549551a4bc6f7e4e4f0da81e9d6cc054da9529e053d1360b9d83e3ab9d3c8a9b7d3b07e2bab0979f3912c4bb41f621bf7685ee607fc147670196e009a9cd92b0fb525ca9b8e8fdf4c332732c02ccbcf57b5c19f001df37a5104d367f0f3f5cc0d9a2bd299e1f37872d7580b05596eadd5dec3e80366ab47357205c407bbcac49540a1fd69f36d308d0fdb72d0273f0c7a0d92220fe25daba5885a162bf238495463971c61b380084b7f79ad817d6f343e254722019da9ad32e72f2d864074023096cb8dd615b6fbbb47c6bef0926290a68403340e0a4dfb961e5b9002fc104e480823f12178cb91fafd51eb5dc9eb9fec4e6f7c0a130f31eb84cc70be9e5fd0bfb7d279590bdb49e7a4fe3b156a922fc73f78a50e765de9df4cfa5487f360e29e99343e91811baec331c4680985e608ca5d408e21725c6aa1b61d5a8b48d75f4aaa9a3cbe88d3e0f1a54319081f77c72c8f52547448c03ab4afbf6b8fb0e126c037a0ad4094600dd0e0634d76f88c21087f3cfb485a89bc1e3abc4c95041d1d170eccf02933ec5393d4be1dc573f83c33d3b9a7468069160000000000"
);
#[test]
fn test_auth_credential_presentation_v1_is_rejected() {
assert!(
zkgroup::auth::AnyAuthCredentialPresentation::new(AUTH_CREDENTIAL_PRESENTATION_V1).is_err()
);
}
#[test]
fn test_integration_auth_zkc() {
let server_secret_params = zkgroup::ServerSecretParams::generate(zkgroup::TEST_ARRAY_32);
let server_public_params = server_secret_params.get_public_params();
let master_key = zkgroup::groups::GroupMasterKey::new(zkgroup::TEST_ARRAY_32_1);
let group_secret_params =
zkgroup::groups::GroupSecretParams::derive_from_master_key(master_key);
let group_public_params = group_secret_params.get_public_params();
// Random UID and issueTime
let aci = libsignal_core::Aci::from(uuid::Uuid::from_bytes(zkgroup::TEST_ARRAY_16));
let pni = libsignal_core::Pni::from(uuid::Uuid::from_bytes(zkgroup::TEST_ARRAY_16_1));
let redemption_time = zkgroup::Timestamp::from_epoch_seconds(123456 * SECONDS_PER_DAY);
// SERVER
// Issue credential
let randomness = zkgroup::TEST_ARRAY_32_2;
let auth_credential_response =
zkgroup::auth::AuthCredentialWithPniZkcResponse::issue_credential(
aci,
pni,
redemption_time,
&server_secret_params,
randomness,
);
// CLIENT
let auth_credential = auth_credential_response
.clone()
.receive(aci, pni, redemption_time, &server_public_params)
.unwrap();
// Create and receive presentation
let randomness = zkgroup::TEST_ARRAY_32_5;
let presentation =
auth_credential.present(&server_public_params, &group_secret_params, randomness);
let presentation_bytes = &bincode::serialize(&presentation).unwrap();
let presentation_any: zkgroup::auth::AnyAuthCredentialPresentation = presentation.into();
let presentation_any_bytes = &bincode::serialize(&presentation_any).unwrap();
assert_hex_eq!(
AUTH_CREDENTIAL_PRESENTATION_V4_RESULT[..],
presentation_bytes[..]
);
assert_hex_eq!(
AUTH_CREDENTIAL_PRESENTATION_V4_RESULT[..],
presentation_any_bytes[..]
);
let presentation_parsed = bincode::deserialize::<
zkgroup::auth::AuthCredentialWithPniZkcPresentation,
>(presentation_bytes)
.unwrap();
assert!(
presentation_any.get_pni_ciphertext() == group_secret_params.encrypt_service_id(pni.into())
);
presentation_parsed
.verify(&server_secret_params, &group_public_params, redemption_time)
.unwrap();
server_secret_params
.verify_auth_credential_presentation(
group_public_params,
&presentation_any,
redemption_time,
)
.unwrap();
server_secret_params
.verify_auth_credential_presentation(
group_public_params,
&presentation_any,
redemption_time.sub_seconds(SECONDS_PER_DAY + 1),
)
.expect_err("credential not valid before redemption time (allowing for clock skew)");
server_secret_params
.verify_auth_credential_presentation(
group_public_params,
&presentation_any,
redemption_time.add_seconds(2 * SECONDS_PER_DAY + 2),
)
.expect_err("credential not valid past deadline");
// Test encoding, which will also detect if the serialized lengths change.
let mut auth_credential_response_bytes =
[0u8; zkgroup::common::constants::AUTH_CREDENTIAL_WITH_PNI_RESPONSE_LEN];
let mut auth_credential_bytes = [0u8; zkgroup::common::constants::AUTH_CREDENTIAL_WITH_PNI_LEN];
auth_credential_response_bytes
.copy_from_slice(&bincode::serialize(&auth_credential_response).unwrap());
auth_credential_bytes.copy_from_slice(&bincode::serialize(&auth_credential).unwrap());
}
fn test_integration_expiring_profile<const V: u8>()
where
zkgroup::profiles::AnyProfileKeyCredentialPresentation:
From<zkgroup::profiles::ExpiringProfileKeyCredentialPresentation<V>>,
{
// SERVER
let server_secret_params = zkgroup::ServerSecretParams::generate(zkgroup::TEST_ARRAY_32);
let server_public_params = server_secret_params.get_public_params();
// CLIENT
let master_key = zkgroup::groups::GroupMasterKey::new(zkgroup::TEST_ARRAY_32_1);
let group_secret_params =
zkgroup::groups::GroupSecretParams::derive_from_master_key(master_key);
let group_public_params = group_secret_params.get_public_params();
let aci = libsignal_core::Aci::from_uuid_bytes(zkgroup::TEST_ARRAY_16);
let profile_key =
zkgroup::profiles::ProfileKey::create(zkgroup::common::constants::TEST_ARRAY_32_1);
let profile_key_commitment = profile_key.get_commitment(aci);
// Create context and request
let randomness = zkgroup::TEST_ARRAY_32_3;
let context = server_public_params.create_profile_key_credential_request_context(
randomness,
aci,
profile_key,
);
let request = context.get_request();
// SERVER
let randomness = zkgroup::TEST_ARRAY_32_4;
let expiration = zkgroup::Timestamp::from_epoch_seconds(17u64 * 24 * 60 * 60);
let current_time = expiration.sub_seconds(2 * 24 * 60 * 60);
let response = server_secret_params
.issue_expiring_profile_key_credential(
randomness,
&request,
aci,
profile_key_commitment,
expiration,
)
.unwrap();
// CLIENT
// Gets stored profile credential
let profile_key_credential = server_public_params
.receive_expiring_profile_key_credential(&context, &response, current_time)
.unwrap();
// Create encrypted UID and profile key
let uuid_ciphertext = group_secret_params.encrypt_service_id(aci.into());
let plaintext = group_secret_params
.decrypt_service_id(uuid_ciphertext)
.unwrap();
assert_eq!(plaintext, aci);
let profile_key_ciphertext = group_secret_params.encrypt_profile_key(profile_key, aci);
let decrypted_profile_key = group_secret_params
.decrypt_profile_key(profile_key_ciphertext, aci)
.unwrap();
assert_hex_eq!(decrypted_profile_key.get_bytes(), profile_key.get_bytes());
// Create presentation
let randomness = zkgroup::TEST_ARRAY_32_5;
let presentation: zkgroup::profiles::ExpiringProfileKeyCredentialPresentation<V> =
server_public_params.create_expiring_profile_key_credential_presentation(
randomness,
group_secret_params,
profile_key_credential,
);
assert_eq!(expiration, presentation.get_expiration_time());
let presentation_bytes = &bincode::serialize(&presentation).unwrap();
let presentation_any: zkgroup::profiles::AnyProfileKeyCredentialPresentation =
presentation.into();
let presentation_any_bytes = &bincode::serialize(&presentation_any).unwrap();
let expected_hex = match V {
zkgroup::PRESENTATION_VERSION_3 => PROFILE_KEY_CREDENTIAL_PRESENTATION_V3_RESULT,
zkgroup::PRESENTATION_VERSION_4 => PROFILE_KEY_CREDENTIAL_PRESENTATION_V4_RESULT,
_ => panic!("unexpected ExpiringProfileKeyCredentialPresentation version {V}"),
};
assert_hex_eq!(expected_hex, &presentation_bytes[..]);
assert_hex_eq!(expected_hex, &presentation_any_bytes[..]);
server_secret_params
.verify_profile_key_credential_presentation(
group_public_params,
&presentation_any,
expiration.sub_seconds(5),
)
.unwrap();
assert!(
server_secret_params
.verify_profile_key_credential_presentation(
group_public_params,
&presentation_any,
expiration,
)
.is_err()
);
assert!(
server_secret_params
.verify_profile_key_credential_presentation(
group_public_params,
&presentation_any,
expiration.add_seconds(5),
)
.is_err()
);
let presentation_parsed =
zkgroup::profiles::AnyProfileKeyCredentialPresentation::new(presentation_bytes).unwrap();
server_secret_params
.verify_profile_key_credential_presentation(
group_public_params,
&presentation_parsed,
expiration.sub_seconds(5),
)
.unwrap();
// test encoding
// these tests will also discover if the serialized sizes change,
// necessitating an update to the LEN constants
let mut profile_key_commitment_bytes =
[0u8; zkgroup::common::constants::PROFILE_KEY_COMMITMENT_LEN];
let mut profile_key_credential_bytes =
[0u8; zkgroup::common::constants::EXPIRING_PROFILE_KEY_CREDENTIAL_LEN];
let mut profile_key_credential_request_bytes =
[0u8; zkgroup::common::constants::PROFILE_KEY_CREDENTIAL_REQUEST_LEN];
let mut profile_key_credential_request_context_bytes =
[0u8; zkgroup::common::constants::PROFILE_KEY_CREDENTIAL_REQUEST_CONTEXT_LEN];
let mut profile_key_credential_response_bytes =
[0u8; zkgroup::common::constants::EXPIRING_PROFILE_KEY_CREDENTIAL_RESPONSE_LEN];
profile_key_commitment_bytes
.copy_from_slice(&bincode::serialize(&profile_key_commitment).unwrap());
profile_key_credential_bytes
.copy_from_slice(&bincode::serialize(&profile_key_credential).unwrap());
profile_key_credential_request_bytes.copy_from_slice(&bincode::serialize(&request).unwrap());
profile_key_credential_request_context_bytes
.copy_from_slice(&bincode::serialize(&context).unwrap());
profile_key_credential_response_bytes.copy_from_slice(&bincode::serialize(&response).unwrap());
}
#[test]
fn test_integration_expiring_profile_v1() {
test_integration_expiring_profile::<{ zkgroup::PRESENTATION_VERSION_3 }>();
}
#[test]
fn test_integration_expiring_profile_v2() {
test_integration_expiring_profile::<{ zkgroup::PRESENTATION_VERSION_4 }>();
}
#[test]
fn test_server_sigs() {
let server_secret_params =
zkgroup::api::server_params::ServerSecretParams::generate(zkgroup::TEST_ARRAY_32);
let server_public_params = server_secret_params.get_public_params();
let randomness = zkgroup::TEST_ARRAY_32_2;
let message = zkgroup::TEST_ARRAY_32_1;
let signature = server_secret_params.sign(randomness, &message);
const EXPECTED_SIGNATURE: &[u8] = &hex!(
"87d354564d35ef91edba851e0815612e864c227a0471d50c270698604406d003a55473f576cf241fc6b41c6b16e5e63b333c02fe4a33858022fdd7a4ab367b06"
);
assert_eq!(
&signature[..],
EXPECTED_SIGNATURE,
"signature = {}",
hex::encode(signature)
);
server_public_params
.verify_signature(&message, signature)
.unwrap();
}
#[test]
fn test_blob_encryption() {
let master_key = zkgroup::groups::GroupMasterKey::new(zkgroup::TEST_ARRAY_32_1);
let group_secret_params =
zkgroup::groups::GroupSecretParams::derive_from_master_key(master_key);
let randomness = zkgroup::TEST_ARRAY_32_2;
let plaintext_vec = vec![
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
0x18, 0x19,
];
// WARNING: THIS VECTOR DOES *NOT* MATCH JAVA/SWIFT/NODE AS THEY IMPLEMENT PADDING
let ciphertext_vec = vec![
0xe9, 0x58, 0x07, 0xb1, 0x90, 0xd4, 0x78, 0xd7, 0xbe, 0x3a, 0x77, 0xb2, 0x29, 0x27, 0x13,
0x2e, 0xeb, 0xa5, 0x1c, 0x73, 0x9c, 0xd5, 0x70, 0x73, 0x17, 0xf7, 0x3e, 0x59, 0x1a, 0x91,
0x5f, 0xff, 0x1f, 0x20, 0xa3, 0x02, 0x69, 0x2a, 0xfd, 0xc7, 0x08, 0x7f, 0x10, 0x19, 0x60,
0x00,
];
let calc_ciphertext_vec = group_secret_params.encrypt_blob(randomness, &plaintext_vec);
let calc_plaintext_vec = group_secret_params
.decrypt_blob(&calc_ciphertext_vec)
.unwrap();
assert_hex_eq!(calc_plaintext_vec, plaintext_vec);
assert_hex_eq!(calc_ciphertext_vec, ciphertext_vec);
}
/// Check that older clients can still retrieve the UUID and profile key ciphertexts from a v2
/// presentation.
///
/// This only matters for ProfileKeyCredentialPresentations; other presentation kinds are only
/// presented to the server.
#[test]
fn test_profile_key_credential_presentation_v2_as_v1() {
let v2 = zkgroup::profiles::AnyProfileKeyCredentialPresentation::new(
&PROFILE_KEY_CREDENTIAL_PRESENTATION_V2_RESULT,
)
.unwrap();
let v2_as_v1 = bincode::deserialize::<zkgroup::profiles::ProfileKeyCredentialPresentationV1>(
&PROFILE_KEY_CREDENTIAL_PRESENTATION_V2_RESULT,
)
.unwrap();
assert!(v2.get_uuid_ciphertext() == v2_as_v1.get_uuid_ciphertext());
assert!(v2.get_profile_key_ciphertext() == v2_as_v1.get_profile_key_ciphertext());
}
/// Check that an expiring presentation can be converted to a v1 presentation, at least structurally.
#[test]
fn test_profile_key_credential_presentation_expiring_as_v1() {
let presentation = zkgroup::profiles::AnyProfileKeyCredentialPresentation::new(
PROFILE_KEY_CREDENTIAL_PRESENTATION_V3_RESULT,
)
.unwrap();
let presentation_as_v1_bytes = presentation.to_structurally_valid_v1_presentation_bytes();
let presentation_as_v1 = bincode::deserialize::<
zkgroup::profiles::ProfileKeyCredentialPresentationV1,
>(&presentation_as_v1_bytes)
.unwrap();
assert!(presentation.get_uuid_ciphertext() == presentation_as_v1.get_uuid_ciphertext());
assert!(
presentation.get_profile_key_ciphertext()
== presentation_as_v1.get_profile_key_ciphertext()
);
}
#[test]
fn test_profile_key_credential_presentation_v1_does_not_verify() {
// Originally from test_integration_profile.
// SERVER
let server_secret_params = zkgroup::ServerSecretParams::generate(zkgroup::TEST_ARRAY_32);
// CLIENT
let master_key = zkgroup::groups::GroupMasterKey::new(zkgroup::TEST_ARRAY_32_1);
let group_secret_params =
zkgroup::groups::GroupSecretParams::derive_from_master_key(master_key);
let group_public_params = group_secret_params.get_public_params();
let redemption_time = Timestamp::from_epoch_seconds(123456 * SECONDS_PER_DAY);
let presentation = zkgroup::profiles::AnyProfileKeyCredentialPresentation::new(
PROFILE_KEY_CREDENTIAL_PRESENTATION_V1,
)
.unwrap();
assert!(
server_secret_params
.verify_profile_key_credential_presentation(
group_public_params,
&presentation,
redemption_time.add_seconds(60)
)
.is_err()
);
}
+72
View File
@@ -0,0 +1,72 @@
//! Helper functions for use with Lizard
#![allow(non_snake_case)]
use subtle::Choice;
use subtle::ConditionallyNegatable;
use subtle::ConditionallySelectable;
use subtle::ConstantTimeEq;
use super::lizard_constants;
use crate::constants;
use crate::field::FieldElement;
/// Represents a point (s,t) on the the Jacobi quartic associated
/// to the Edwards curve.
#[derive(Copy, Clone)]
#[allow(missing_docs)]
pub struct JacobiPoint {
pub S: FieldElement,
pub T: FieldElement,
}
impl JacobiPoint {
/// Elligator2 is defined in two steps: first a field element is converted
/// to a point (s,t) on the Jacobi quartic associated to the Edwards curve.
/// Then this point is mapped to a point on the Edwards curve.
/// This function computes a field element that is mapped to a given (s,t)
/// with Elligator2 if it exists.
pub(crate) fn elligator_inv(&self) -> (Choice, FieldElement) {
let mut out = FieldElement::ZERO;
// Special case: s = 0. If s is zero, either t = 1 or t = -1.
// If t=1, then sqrt(i*d) is the preimage. Otherwise it's 0.
let s_is_zero = self.S.is_zero();
let t_equals_one = self.T.ct_eq(&FieldElement::ONE);
out.conditional_assign(&lizard_constants::SQRT_ID, t_equals_one);
let mut ret = s_is_zero;
let mut done = s_is_zero;
// a := (t+1) (d+1)/(d-1)
let a = &(&self.T + &FieldElement::ONE) * &lizard_constants::DP1_OVER_DM1;
let a2 = a.square();
// y := 1/sqrt(i (s^4 - a^2)).
let s2 = self.S.square();
let s4 = s2.square();
let invSqY = &(&s4 - &a2) * &constants::SQRT_M1;
// There is no preimage if the square root of i*(s^4-a^2) does not exist.
let (sq, y) = invSqY.invsqrt();
ret |= sq;
done |= !sq;
// x := (a + sign(s)*s^2) y
let mut pms2 = s2;
pms2.conditional_negate(self.S.is_negative());
let mut x = &(&a + &pms2) * &y;
let x_is_negative = x.is_negative();
x.conditional_negate(x_is_negative);
out.conditional_assign(&x, !done);
(ret, out)
}
pub(crate) fn dual(&self) -> JacobiPoint {
JacobiPoint {
S: -(&self.S),
T: -(&self.T),
}
}
}
+49
View File
@@ -0,0 +1,49 @@
//! Constants for use in Lizard
//!
//! Could be moved into backend/serial/u??/constants.rs
#[cfg(curve25519_dalek_bits = "64")]
pub(crate) use super::u64_constants::*;
#[cfg(curve25519_dalek_bits = "32")]
pub(crate) use super::u32_constants::*;
// ------------------------------------------------------------------------
// Tests
// ------------------------------------------------------------------------
#[cfg(test)]
mod test {
use super::*;
use crate::constants;
use crate::field::FieldElement;
#[test]
fn test_lizard_constants() {
let (_, sqrt_id) = FieldElement::sqrt_ratio_i(
&(&constants::SQRT_M1 * &constants::EDWARDS_D),
&FieldElement::ONE,
);
assert_eq!(sqrt_id, SQRT_ID);
assert_eq!(
&(&constants::EDWARDS_D + &FieldElement::ONE)
* &(&constants::EDWARDS_D - &FieldElement::ONE).invert(),
DP1_OVER_DM1
);
assert_eq!(
MDOUBLE_INVSQRT_A_MINUS_D,
-&(&constants::INVSQRT_A_MINUS_D + &constants::INVSQRT_A_MINUS_D)
);
assert_eq!(
MIDOUBLE_INVSQRT_A_MINUS_D,
&MDOUBLE_INVSQRT_A_MINUS_D * &constants::SQRT_M1
);
let (_, invsqrt_one_plus_d) = (&constants::EDWARDS_D + &FieldElement::ONE).invsqrt();
assert_eq!(-&invsqrt_one_plus_d, MINVSQRT_ONE_PLUS_D);
}
}
+332
View File
@@ -0,0 +1,332 @@
//! Defines additional methods on RistrettoPoint for Lizard
#![allow(non_snake_case)]
use digest::generic_array::typenum::U32;
use digest::Digest;
use crate::constants;
use crate::field::FieldElement;
use subtle::Choice;
use subtle::ConditionallySelectable;
use subtle::ConstantTimeEq;
use crate::edwards::EdwardsPoint;
use super::jacobi_quartic::JacobiPoint;
use super::lizard_constants;
use crate::ristretto::RistrettoPoint;
#[allow(unused_imports)]
use core::prelude::*;
impl RistrettoPoint {
/// Directly encode 253 bits as a RistrettoPoint, using Elligator
pub fn from_uniform_bytes_single_elligator(bytes: &[u8; 32]) -> RistrettoPoint {
RistrettoPoint::elligator_ristretto_flavor(&FieldElement::from_bytes(bytes))
}
/// Encode 16 bytes of data to a RistrettoPoint, using the Lizard method
pub fn lizard_encode<D: Digest>(data: &[u8; 16]) -> RistrettoPoint
where
D: Digest<OutputSize = U32>,
{
let mut fe_bytes: [u8; 32] = Default::default();
let digest = D::digest(data);
fe_bytes[0..32].copy_from_slice(digest.as_slice());
fe_bytes[8..24].copy_from_slice(data);
fe_bytes[0] &= 254; // make positive since Elligator on r and -r is the same
fe_bytes[31] &= 63;
let fe = FieldElement::from_bytes(&fe_bytes);
RistrettoPoint::elligator_ristretto_flavor(&fe)
}
/// Decode 16 bytes of data from a RistrettoPoint, using the Lizard method
pub fn lizard_decode<D: Digest>(&self) -> Option<[u8; 16]>
where
D: Digest<OutputSize = U32>,
{
let mut result: [u8; 16] = Default::default();
let mut h: [u8; 32] = Default::default();
let (mask, fes) = self.elligator_ristretto_flavor_inverse();
let mut n_found = 0;
for (j, fe_j) in fes.iter().enumerate() {
let mut ok = Choice::from((mask >> j) & 1);
let buf2 = fe_j.as_bytes(); // array
h.copy_from_slice(&D::digest(&buf2[8..24])); // array
h[8..24].copy_from_slice(&buf2[8..24]);
h[0] &= 254;
h[31] &= 63;
ok &= h.ct_eq(&buf2);
for i in 0..16 {
result[i] = u8::conditional_select(&result[i], &buf2[8 + i], ok);
}
n_found += ok.unwrap_u8();
}
if n_found == 1 {
Some(result)
} else {
None
}
}
/// Directly encode 253 bits as a RistrettoPoint, using Elligator
pub fn encode_253_bits(data: &[u8; 32]) -> Option<RistrettoPoint> {
if data.len() != 32 {
return None;
}
let fe = FieldElement::from_bytes(data);
let p = RistrettoPoint::elligator_ristretto_flavor(&fe);
Some(p)
}
/// Directly decode a RistrettoPoint as 253 bits, using Elligator
pub fn decode_253_bits(&self) -> (u8, [[u8; 32]; 8]) {
let mut ret = [[0u8; 32]; 8];
let (mask, fes) = self.elligator_ristretto_flavor_inverse();
for j in 0..8 {
ret[j] = fes[j].as_bytes();
}
(mask, ret)
}
/// Return the coset self + E[4], for debugging.
pub fn xcoset4(&self) -> [EdwardsPoint; 4] {
[
self.0,
self.0 + constants::EIGHT_TORSION[2],
self.0 + constants::EIGHT_TORSION[4],
self.0 + constants::EIGHT_TORSION[6],
]
}
/// Computes the at most 8 positive FieldElements f such that
/// self == elligator_ristretto_flavor(f).
/// Assumes self is even.
///
/// Returns a bitmask of which elements in fes are set.
pub fn elligator_ristretto_flavor_inverse(&self) -> (u8, [FieldElement; 8]) {
// Elligator2 computes a Point from a FieldElement in two steps: first
// it computes a (s,t) on the Jacobi quartic and then computes the
// corresponding even point on the Edwards curve.
//
// We invert in three steps. Any Ristretto point has four representatives
// as even Edwards points. For each of those even Edwards points,
// there are two points on the Jacobi quartic that map to it.
// Each of those eight points on the Jacobi quartic might have an
// Elligator2 preimage.
//
// Essentially we first loop over the four representatives of our point,
// then for each of them consider both points on the Jacobi quartic and
// check whether they have an inverse under Elligator2. We take the
// following shortcut though.
//
// We can compute two Jacobi quartic points for (x,y) and (-x,-y)
// at the same time. The four Jacobi quartic points are two of
// such pairs.
let mut mask: u8 = 0;
let jcs = self.to_jacobi_quartic_ristretto();
let mut ret = [FieldElement::ONE; 8];
for i in 0..4 {
let (ok, fe) = jcs[i].elligator_inv();
let mut tmp: u8 = 0;
ret[2 * i] = fe;
tmp.conditional_assign(&1, ok);
mask |= tmp << (2 * i);
let jc = jcs[i].dual();
let (ok, fe) = jc.elligator_inv();
let mut tmp: u8 = 0;
ret[2 * i + 1] = fe;
tmp.conditional_assign(&1, ok);
mask |= tmp << (2 * i + 1);
}
(mask, ret)
}
/// Find a point on the Jacobi quartic associated to each of the four
/// points Ristretto equivalent to p.
///
/// There is one exception: for (0,-1) there is no point on the quartic and
/// so we repeat one on the quartic equivalent to (0,1).
fn to_jacobi_quartic_ristretto(self) -> [JacobiPoint; 4] {
let x2 = self.0.X.square(); // X^2
let y2 = self.0.Y.square(); // Y^2
let y4 = y2.square(); // Y^4
let z2 = self.0.Z.square(); // Z^2
let z_min_y = &self.0.Z - &self.0.Y; // Z - Y
let z_pl_y = &self.0.Z + &self.0.Y; // Z + Y
let z2_min_y2 = &z2 - &y2; // Z^2 - Y^2
// gamma := 1/sqrt( Y^4 X^2 (Z^2 - Y^2) )
let (_, gamma) = (&(&y4 * &x2) * &z2_min_y2).invsqrt();
let den = &gamma * &y2;
let s_over_x = &den * &z_min_y;
let sp_over_xp = &den * &z_pl_y;
let s0 = &s_over_x * &self.0.X;
let s1 = &(-(&sp_over_xp)) * &self.0.X;
// t_0 := -2/sqrt(-d-1) * Z * sOverX
// t_1 := -2/sqrt(-d-1) * Z * spOverXp
let tmp = &lizard_constants::MDOUBLE_INVSQRT_A_MINUS_D * &self.0.Z;
let mut t0 = &tmp * &s_over_x;
let mut t1 = &tmp * &sp_over_xp;
// den := -1/sqrt(1+d) (Y^2 - Z^2) gamma
let den = &(&(-(&z2_min_y2)) * &lizard_constants::MINVSQRT_ONE_PLUS_D) * &gamma;
// Same as before but with the substitution (X, Y, Z) = (Y, X, i*Z)
let iz = &constants::SQRT_M1 * &self.0.Z; // iZ
let iz_min_x = &iz - &self.0.X; // iZ - X
let iz_pl_x = &iz + &self.0.X; // iZ + X
let s_over_y = &den * &iz_min_x;
let sp_over_yp = &den * &iz_pl_x;
let mut s2 = &s_over_y * &self.0.Y;
let mut s3 = &(-(&sp_over_yp)) * &self.0.Y;
// t_2 := -2/sqrt(-d-1) * i*Z * sOverY
// t_3 := -2/sqrt(-d-1) * i*Z * spOverYp
let tmp = &lizard_constants::MDOUBLE_INVSQRT_A_MINUS_D * &iz;
let mut t2 = &tmp * &s_over_y;
let mut t3 = &tmp * &sp_over_yp;
// Special case: X=0 or Y=0. Then return
//
// (0,1) (1,-2i/sqrt(-d-1) (-1,-2i/sqrt(-d-1))
//
// Note that if X=0 or Y=0, then s_i = t_i = 0.
let x_or_y_is_zero = self.0.X.is_zero() | self.0.Y.is_zero();
t0.conditional_assign(&FieldElement::ONE, x_or_y_is_zero);
t1.conditional_assign(&FieldElement::ONE, x_or_y_is_zero);
t2.conditional_assign(
&lizard_constants::MIDOUBLE_INVSQRT_A_MINUS_D,
x_or_y_is_zero,
);
t3.conditional_assign(
&lizard_constants::MIDOUBLE_INVSQRT_A_MINUS_D,
x_or_y_is_zero,
);
s2.conditional_assign(&FieldElement::ONE, x_or_y_is_zero);
s3.conditional_assign(&(-(&FieldElement::ONE)), x_or_y_is_zero);
[
JacobiPoint { S: s0, T: t0 },
JacobiPoint { S: s1, T: t1 },
JacobiPoint { S: s2, T: t2 },
JacobiPoint { S: s3, T: t3 },
]
}
}
// ------------------------------------------------------------------------
// Tests
// ------------------------------------------------------------------------
#[cfg(test)]
mod test {
use sha2;
use self::sha2::Sha256;
use super::*;
use crate::ristretto::CompressedRistretto;
use rand_core::RngCore;
#[cfg(feature = "rand")]
use rand_os::OsRng;
fn test_lizard_encode_helper(data: &[u8; 16], result: &[u8; 32]) {
let p = RistrettoPoint::lizard_encode::<Sha256>(data);
let p_bytes = p.compress().to_bytes();
assert!(&p_bytes == result);
let p = CompressedRistretto::from_slice(&p_bytes)
.unwrap()
.decompress()
.unwrap();
let data_out = p.lizard_decode::<Sha256>().unwrap();
assert!(&data_out == data);
}
#[test]
fn test_lizard_encode() {
test_lizard_encode_helper(
&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
&[
0xf0, 0xb7, 0xe3, 0x44, 0x84, 0xf7, 0x4c, 0xf0, 0xf, 0x15, 0x2, 0x4b, 0x73, 0x85,
0x39, 0x73, 0x86, 0x46, 0xbb, 0xbe, 0x1e, 0x9b, 0xc7, 0x50, 0x9a, 0x67, 0x68, 0x15,
0x22, 0x7e, 0x77, 0x4f,
],
);
test_lizard_encode_helper(
&[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
&[
0xcc, 0x92, 0xe8, 0x1f, 0x58, 0x5a, 0xfc, 0x5c, 0xaa, 0xc8, 0x86, 0x60, 0xd8, 0xd1,
0x7e, 0x90, 0x25, 0xa4, 0x44, 0x89, 0xa3, 0x63, 0x4, 0x21, 0x23, 0xf6, 0xaf, 0x7,
0x2, 0x15, 0x6e, 0x65,
],
);
test_lizard_encode_helper(
&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
&[
0xc8, 0x30, 0x57, 0x3f, 0x8a, 0x8e, 0x77, 0x78, 0x67, 0x1f, 0x76, 0xcd, 0xc7, 0x96,
0xdc, 0xa, 0x23, 0x5c, 0xf1, 0x77, 0xf1, 0x97, 0xd9, 0xfc, 0xba, 0x6, 0xe8, 0x4e,
0x96, 0x24, 0x74, 0x44,
],
);
}
#[test]
fn test_elligator_inv() {
let mut rng = rand::thread_rng();
for i in 0..100 {
let mut fe_bytes = [0u8; 32];
if i == 0 {
// Test for first corner-case: fe = 0
fe_bytes = [0u8; 32];
} else if i == 1 {
// Test for second corner-case: fe = +sqrt(i*d)
fe_bytes = [
168, 27, 92, 74, 203, 42, 48, 117, 170, 109, 234, 14, 45, 169, 188, 205, 21,
110, 235, 115, 153, 84, 52, 117, 151, 235, 123, 244, 88, 85, 179, 5,
];
} else {
// For the rest, just generate a random field element to test.
rng.fill_bytes(&mut fe_bytes);
}
fe_bytes[0] &= 254; // positive
fe_bytes[31] &= 127; // < 2^255-19
let fe = FieldElement::from_bytes(&fe_bytes);
let pt = RistrettoPoint::elligator_ristretto_flavor(&fe);
for pt2 in &pt.xcoset4() {
let (mask, fes) = RistrettoPoint(*pt2).elligator_ristretto_flavor_inverse();
let mut found = false;
for (j, fe_j) in fes.iter().enumerate() {
if mask & (1 << j) != 0 {
assert_eq!(RistrettoPoint::elligator_ristretto_flavor(fe_j), pt);
if *fe_j == fe {
found = true;
}
}
}
assert!(found);
}
}
}
}
+13
View File
@@ -0,0 +1,13 @@
//! The Lizard method for encoding/decoding 16 bytes into Ristretto points.
#![allow(non_snake_case)]
#[cfg(curve25519_dalek_bits = "32")]
mod u32_constants;
#[cfg(curve25519_dalek_bits = "64")]
mod u64_constants;
pub mod jacobi_quartic;
pub mod lizard_constants;
pub mod lizard_ristretto;
+63
View File
@@ -0,0 +1,63 @@
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(curve25519_dalek_backend = "fiat")] {
pub use crate::backend::serial::fiat_u64::field::FieldElement51;
const fn field_element(element: [u64; 5]) -> FieldElement51 {
FieldElement51(fiat_crypto::curve25519_64::fiat_25519_tight_field_element(element))
}
} else {
pub use crate::backend::serial::u64::field::FieldElement51;
const fn field_element(element: [u64; 5]) -> FieldElement51 {
FieldElement51(element)
}
}
}
/// `= sqrt(i*d)`, where `i = +sqrt(-1)` and `d` is the Edwards curve parameter.
pub const SQRT_ID: FieldElement51 = field_element([
2298852427963285,
3837146560810661,
4413131899466403,
3883177008057528,
2352084440532925,
]);
/// `= (d+1)/(d-1)`, where `d` is the Edwards curve parameter.
pub const DP1_OVER_DM1: FieldElement51 = field_element([
2159851467815724,
1752228607624431,
1825604053920671,
1212587319275468,
253422448836237,
]);
/// `= -2/sqrt(a-d)`, where `a = -1 (mod p)`, `d` are the Edwards curve parameters.
pub const MDOUBLE_INVSQRT_A_MINUS_D: FieldElement51 = field_element([
1693982333959686,
608509411481997,
2235573344831311,
947681270984193,
266558006233600,
]);
/// `= -2i/sqrt(a-d)`, where `a = -1 (mod p)`, `d` are the Edwards curve parameters
/// and `i = +sqrt(-1)`.
pub const MIDOUBLE_INVSQRT_A_MINUS_D: FieldElement51 = field_element([
1608655899704280,
1999971613377227,
49908634785720,
1873700692181652,
353702208628067,
]);
/// `= -1/sqrt(1+d)`, where `d` is the Edwards curve parameters.
pub const MINVSQRT_ONE_PLUS_D: FieldElement51 = field_element([
321571956990465,
1251814006996634,
2226845496292387,
189049560751797,
2074948709371214,
]);
+155
View File
@@ -0,0 +1,155 @@
//
// Copyright 2020 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use std::cmp;
use hmac::{Hmac, Mac};
use sha2::Sha256;
use crate::shoapi::ShoApi;
pub const BLOCK_LEN: usize = 64;
pub const HASH_LEN: usize = 32;
#[derive(Clone, PartialEq, Eq)]
#[expect(clippy::upper_case_acronyms)]
enum Mode {
ABSORBING,
RATCHETED,
}
#[derive(Clone)]
pub struct ShoHmacSha256 {
hasher: Hmac<Sha256>,
cv: [u8; HASH_LEN],
mode: Mode,
}
impl ShoApi for ShoHmacSha256 {
fn new(label: &[u8]) -> ShoHmacSha256 {
let mut sho = ShoHmacSha256 {
hasher: Hmac::<Sha256>::new_from_slice(&[0; HASH_LEN])
.expect("HMAC accepts 256-bit keys"),
cv: [0; HASH_LEN],
mode: Mode::RATCHETED,
};
sho.absorb_and_ratchet(label);
sho
}
fn absorb(&mut self, input: &[u8]) {
if let Mode::RATCHETED = self.mode {
self.hasher =
Hmac::<Sha256>::new_from_slice(&self.cv).expect("HMAC accepts 256-bit keys");
self.mode = Mode::ABSORBING;
}
self.hasher.update(input);
}
// called after absorb() only; streaming squeeze not yet supported
fn ratchet(&mut self) {
if let Mode::RATCHETED = self.mode {
return;
}
self.hasher.update(&[0x00]);
self.cv
.copy_from_slice(&self.hasher.clone().finalize().into_bytes());
self.hasher.reset();
self.mode = Mode::RATCHETED;
}
fn squeeze_and_ratchet_into(&mut self, mut target: &mut [u8]) {
assert!(self.mode == Mode::RATCHETED);
let outlen = target.len();
let output_hasher_prefix =
Hmac::<Sha256>::new_from_slice(&self.cv).expect("HMAC accepts 256-bit keys");
let mut i = 0;
while i * HASH_LEN < outlen {
let mut output_hasher = output_hasher_prefix.clone();
output_hasher.update(&(i as u64).to_be_bytes());
output_hasher.update(&[0x01]);
let digest = output_hasher.finalize().into_bytes();
let num_bytes = cmp::min(HASH_LEN, outlen - i * HASH_LEN);
let (output, tail) = target.split_at_mut(num_bytes);
output.copy_from_slice(&digest[..num_bytes]);
target = tail;
i += 1
}
let mut next_hasher = output_hasher_prefix;
next_hasher.update(&(outlen as u64).to_be_bytes());
next_hasher.update(&[0x02]);
self.cv
.copy_from_slice(&next_hasher.finalize().into_bytes()[..]);
self.mode = Mode::RATCHETED;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vectors() {
let mut sho = ShoHmacSha256::new(b"asd");
sho.absorb_and_ratchet(b"asdasd");
let out = sho.squeeze_and_ratchet(64);
/*
println!("{}", hex::encode(&out));
*/
assert!(
out == vec![
0x39, 0x2c, 0xb9, 0x44, 0x93, 0x73, 0x03, 0x7f, 0xa0, 0xc1, 0x1a, 0xeb, 0xed, 0x69,
0xcc, 0xa3, 0xb7, 0xd3, 0xbc, 0x97, 0x90, 0x87, 0x8f, 0x34, 0x17, 0x29, 0xc6, 0x5d,
0x55, 0x06, 0x44, 0x2f, 0x04, 0x98, 0x6c, 0xb5, 0xc9, 0x09, 0x8f, 0x27, 0x7c, 0x3e,
0xa6, 0x40, 0xa4, 0xdc, 0x6e, 0x90, 0x37, 0x2b, 0x43, 0x3a, 0x90, 0xaf, 0x9a, 0xea,
0x70, 0x72, 0xea, 0xba, 0x33, 0x98, 0xc4, 0xfe,
]
);
let mut sho = ShoHmacSha256::new(b"asd");
sho.absorb_and_ratchet(b"asdasd");
let out = sho.squeeze_and_ratchet(65);
/*
println!("{}", hex::encode(&out));
*/
assert!(
out == vec![
0x39, 0x2c, 0xb9, 0x44, 0x93, 0x73, 0x03, 0x7f, 0xa0, 0xc1, 0x1a, 0xeb, 0xed, 0x69,
0xcc, 0xa3, 0xb7, 0xd3, 0xbc, 0x97, 0x90, 0x87, 0x8f, 0x34, 0x17, 0x29, 0xc6, 0x5d,
0x55, 0x06, 0x44, 0x2f, 0x04, 0x98, 0x6c, 0xb5, 0xc9, 0x09, 0x8f, 0x27, 0x7c, 0x3e,
0xa6, 0x40, 0xa4, 0xdc, 0x6e, 0x90, 0x37, 0x2b, 0x43, 0x3a, 0x90, 0xaf, 0x9a, 0xea,
0x70, 0x72, 0xea, 0xba, 0x33, 0x98, 0xc4, 0xfe, 0x7a,
]
);
let mut sho = ShoHmacSha256::new(b"");
sho.absorb_and_ratchet(b"abc");
sho.absorb_and_ratchet(&[0u8; 63]);
sho.absorb_and_ratchet(&[0u8; 64]);
sho.absorb_and_ratchet(&[0u8; 65]);
sho.absorb_and_ratchet(&[0u8; 127]);
sho.absorb_and_ratchet(&[0u8; 128]);
sho.absorb_and_ratchet(&[0u8; 129]);
sho.squeeze_and_ratchet(63);
sho.squeeze_and_ratchet(64);
sho.squeeze_and_ratchet(65);
sho.squeeze_and_ratchet(127);
sho.squeeze_and_ratchet(128);
sho.squeeze_and_ratchet(129);
sho.absorb_and_ratchet(b"def");
let out = sho.squeeze_and_ratchet(63);
println!("{}", hex::encode(&out));
assert!(
out == vec![
0xc5, 0xc1, 0x3b, 0xcc, 0x65, 0x96, 0xc2, 0x5f, 0xc4, 0x51, 0x4e, 0xac, 0x92, 0x69,
0xdd, 0x6e, 0x3e, 0x57, 0xef, 0x70, 0xf4, 0xbf, 0xb8, 0xd6, 0x7f, 0xd3, 0x08, 0x2e,
0xd9, 0x73, 0x2d, 0x77, 0x90, 0xd8, 0xd2, 0x68, 0x6f, 0x19, 0xeb, 0x25, 0x33, 0xa6,
0x5c, 0x94, 0xbb, 0x8c, 0xed, 0xa0, 0xa0, 0x68, 0xe1, 0xb6, 0x15, 0xc8, 0x1b, 0xb2,
0x6e, 0x41, 0x18, 0x89, 0xda, 0x9f, 0xb7,
]
);
}
}
+163
View File
@@ -0,0 +1,163 @@
//
// Copyright 2020 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
// implements the "innerpad" SHO/SHA256 proposal
use std::cmp;
use sha2::{Digest, Sha256};
use crate::shoapi::ShoApi;
pub const BLOCK_LEN: usize = 64;
pub const HASH_LEN: usize = 32;
#[derive(Clone, PartialEq, Eq)]
#[expect(clippy::upper_case_acronyms)]
enum Mode {
ABSORBING,
RATCHETED,
}
#[derive(Clone)]
pub struct ShoSha256 {
hasher: Sha256,
cv: [u8; HASH_LEN],
mode: Mode,
}
impl ShoApi for ShoSha256 {
fn new(label: &[u8]) -> ShoSha256 {
let mut sho = ShoSha256 {
hasher: Sha256::new(),
cv: [0; HASH_LEN],
mode: Mode::RATCHETED,
};
sho.absorb_and_ratchet(label);
sho
}
fn absorb(&mut self, input: &[u8]) {
if let Mode::RATCHETED = self.mode {
// Explicitly pass a slice to avoid generating multiple versions of update().
self.hasher.update(&[0u8; BLOCK_LEN][..]);
self.hasher.update(&self.cv[..]);
self.mode = Mode::ABSORBING;
}
self.hasher.update(input);
}
// called after absorb() only; streaming squeeze not yet supported
fn ratchet(&mut self) {
if let Mode::RATCHETED = self.mode {
return;
}
// Double hash
self.cv
.copy_from_slice(&Sha256::digest(&self.hasher.finalize_reset()[..])[..]);
self.mode = Mode::RATCHETED;
}
fn squeeze_and_ratchet_into(&mut self, mut target: &mut [u8]) {
assert!(self.mode == Mode::RATCHETED);
let mut output_hasher_prefix = Sha256::new();
// Explicitly pass a slice to avoid generating multiple versions of update().
output_hasher_prefix.update(&[0u8; BLOCK_LEN - 1][..]);
output_hasher_prefix.update(&[1u8][..]); // domain separator byte
output_hasher_prefix.update(self.cv);
let mut i = 0;
let outlen = target.len();
while i * HASH_LEN < outlen {
let mut output_hasher = output_hasher_prefix.clone();
output_hasher.update((i as u64).to_be_bytes());
let digest = output_hasher.finalize();
let num_bytes = cmp::min(HASH_LEN, outlen - i * HASH_LEN);
let (output, tail) = target.split_at_mut(num_bytes);
output.copy_from_slice(&digest[0..num_bytes]);
target = tail;
i += 1
}
let mut next_hasher = Sha256::new();
next_hasher.update(&[0u8; BLOCK_LEN - 1][..]);
next_hasher.update(&[2u8][..]); // domain separator byte
next_hasher.update(self.cv);
next_hasher.update((outlen as u64).to_be_bytes());
self.cv.copy_from_slice(&next_hasher.finalize()[..]);
self.mode = Mode::RATCHETED;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vectors() {
let mut sho = ShoSha256::new(b"asd");
sho.absorb_and_ratchet(b"asdasd");
let out = sho.squeeze_and_ratchet(64);
println!("{}", hex::encode(&out));
assert!(
out == vec![
0xeb, 0xe4, 0xef, 0x29, 0xe1, 0x8a, 0xa5, 0x41, 0x37, 0xed, 0xd8, 0x9c, 0x23, 0xf8,
0xbf, 0xea, 0xc2, 0x73, 0x1c, 0x9f, 0x67, 0x5d, 0xa2, 0x0e, 0x7c, 0x67, 0xd5, 0xad,
0x68, 0xd7, 0xee, 0x2d, 0x40, 0xa4, 0x52, 0x32, 0xb5, 0x99, 0x55, 0x2d, 0x46, 0xb5,
0x20, 0x08, 0x2f, 0xb2, 0x70, 0x59, 0x71, 0xf0, 0x7b, 0x31, 0x58, 0xb0, 0x72, 0xb6,
0x3a, 0xb0, 0x93, 0x4a, 0x05, 0xe6, 0xaf, 0x64,
]
);
let mut sho = ShoSha256::new(b"asd");
sho.absorb_and_ratchet(b"asdasd");
let out = sho.squeeze_and_ratchet(65);
/*
println!("{}", hex::encode(&out));
*/
assert!(
out == vec![
0xeb, 0xe4, 0xef, 0x29, 0xe1, 0x8a, 0xa5, 0x41, 0x37, 0xed, 0xd8, 0x9c, 0x23, 0xf8,
0xbf, 0xea, 0xc2, 0x73, 0x1c, 0x9f, 0x67, 0x5d, 0xa2, 0x0e, 0x7c, 0x67, 0xd5, 0xad,
0x68, 0xd7, 0xee, 0x2d, 0x40, 0xa4, 0x52, 0x32, 0xb5, 0x99, 0x55, 0x2d, 0x46, 0xb5,
0x20, 0x08, 0x2f, 0xb2, 0x70, 0x59, 0x71, 0xf0, 0x7b, 0x31, 0x58, 0xb0, 0x72, 0xb6,
0x3a, 0xb0, 0x93, 0x4a, 0x05, 0xe6, 0xaf, 0x64, 0x48,
]
);
let mut sho = ShoSha256::new(b"");
sho.absorb_and_ratchet(b"abc");
sho.absorb_and_ratchet(&[0u8; 63]);
sho.absorb_and_ratchet(&[0u8; 64]);
sho.absorb_and_ratchet(&[0u8; 65]);
sho.absorb_and_ratchet(&[0u8; 127]);
sho.absorb_and_ratchet(&[0u8; 128]);
sho.absorb_and_ratchet(&[0u8; 129]);
sho.squeeze_and_ratchet(63);
sho.squeeze_and_ratchet(64);
sho.squeeze_and_ratchet(65);
sho.squeeze_and_ratchet(127);
sho.squeeze_and_ratchet(128);
sho.squeeze_and_ratchet(129);
sho.absorb_and_ratchet(b"def");
let out = sho.squeeze_and_ratchet(63);
/*
println!("{}", hex::encode(&out));
*/
assert!(
out == vec![
0x0d, 0xde, 0xea, 0x97, 0x3f, 0x32, 0x10, 0xf7, 0x72, 0x5a, 0x3c, 0xdb, 0x24, 0x73,
0xf8, 0x73, 0xae, 0xab, 0x8f, 0xeb, 0x32, 0xb8, 0x0d, 0xee, 0x67, 0xf0, 0xcd, 0xe7,
0x95, 0x4e, 0x92, 0x9a, 0x4e, 0x78, 0x7a, 0xef, 0xee, 0x6d, 0xbe, 0x91, 0xd3, 0xff,
0xf1, 0x62, 0x1a, 0xab, 0x8d, 0x0d, 0x29, 0x19, 0x4f, 0x8a, 0xf9, 0x86, 0xd6, 0xf3,
0x57, 0xad, 0xd0, 0x15, 0x0d, 0xf7, 0xd9,
]
);
}
}
+93
View File
@@ -0,0 +1,93 @@
//
// Copyright 2020 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use curve25519_dalek::ristretto::RistrettoPoint;
use curve25519_dalek::scalar::Scalar;
use crate::args::*;
use crate::errors::*;
use crate::statement::*;
// Signatures are such a common ZKP that we provide special functions for them:
pub fn sign(
private_key: Scalar,
public_key: RistrettoPoint,
message: &[u8],
randomness: &[u8],
) -> Result<Vec<u8>, PokshoError> {
let mut st = Statement::new();
st.add("public_key", &[("private_key", "G")]);
let mut scalar_args = ScalarArgs::new();
scalar_args.add("private_key", private_key);
let mut point_args = PointArgs::new();
point_args.add("public_key", public_key);
st.prove(&scalar_args, &point_args, message, randomness)
}
pub fn verify_signature(
signature: &[u8],
public_key: RistrettoPoint,
message: &[u8],
) -> Result<(), PokshoError> {
let mut st = Statement::new();
st.add("public_key", &[("private_key", "G")]);
let mut point_args = PointArgs::new();
point_args.add("public_key", public_key);
st.verify_proof(signature, &point_args, message)
}
#[cfg(test)]
mod tests {
#![allow(non_snake_case)]
use curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT;
use super::*;
#[test]
#[expect(clippy::cast_possible_truncation, clippy::needless_range_loop)]
fn test_signature() {
let mut block64 = [0u8; 64];
let mut block32 = [0u8; 32];
let mut block100 = [0u8; 100];
for i in 0..32 {
block32[i] = i as u8;
}
for i in 0..64 {
block64[i] = i as u8;
}
for i in 0..100 {
block100[i] = i as u8;
}
let a = Scalar::from_bytes_mod_order_wide(&block64);
let A = a * RISTRETTO_BASEPOINT_POINT;
let randomness = block32;
let message = block100;
let signature = sign(a, A, &message, &randomness).unwrap();
verify_signature(&signature, A, &message).unwrap();
/*
for b in signature.iter() {
print!("0x{:02x}, ", b);
}
println!("");
*/
assert!(
signature
== vec![
0xa0, 0x8f, 0x6b, 0x34, 0xa2, 0x82, 0xdd, 0x4c, 0x7c, 0xfc, 0x40, 0xb9, 0x18,
0xf2, 0x24, 0xa6, 0xb6, 0x31, 0xca, 0x5f, 0x64, 0x80, 0xa1, 0x0b, 0x42, 0xbd,
0x14, 0x08, 0x60, 0x2a, 0x7e, 0x00, 0x8a, 0x23, 0xa1, 0xe3, 0x24, 0x79, 0xbe,
0xfb, 0x5e, 0x26, 0xb9, 0xf0, 0xf4, 0xfe, 0x0e, 0x9e, 0x9e, 0x9e, 0xc9, 0xaf,
0xad, 0x26, 0x91, 0x43, 0xac, 0xb0, 0x3a, 0x22, 0xc6, 0x36, 0x4f, 0x03,
]
);
}
}
+615
View File
@@ -0,0 +1,615 @@
//
// Copyright 2020 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use std::borrow::Cow;
use std::collections::HashMap;
// POKSHO implements the "Sigma protocol for arbitrary linear relations" described in section
// 19.5.3 of https://crypto.stanford.edu/~dabo/cryptobook/BonehShoup_0_4.pdf
//
// We adopt the view that we are proving knowledge of the preimage of a group homomorphism
// from groups G1 -> G2, where elements in G1 are vectors of scalars and elements in G2 are vectors of
// Ristretto "points". The homomomorphism can be viewed as a system of equations of the form:
//
// P = sP + sP + sP + ...
// P = sP + sP + sP + ...
// P = sP + sP + sP + ...
// ...
//
// Where s are any scalars and P are any points. The left-hand side of these equations describes
// the element in G2 which is the image of the homomorphism, and the scalars form an element in G1
// which we are proving knowledge of (this preimage in G1 can be viewed as a vector if the system
// of equations is rewritten as matrix multiplication).
//
// We use the term "statement" for the above system of equations written with both points and
// scalars as variables. For example, the statement for signatures is the single equation:
//
// A = a*G
//
// where "A" is the public key, "a" is the private key, and "G" is the base point. The
// discrete-log equality statement used in VRFs could be written as:
//
// A = a*G
// B = a*H
//
// We use the term "witness" for a vector of scalars that satisfies a given statement and a given
// set of point assignments (equivalently: a witness is an element in G1 that is a preimage of the
// left-hand-side under the group homomorphism). In the above cases, "a" is the witness.
//
// The zero-knowledge proof is a standard Fiat-Shamir Sigma/Schnorr proof of knowledge. To
// implement Fiat-Shamir hashing we use the SHO/HMAC-SHA256 construct, which provides a stateful object
// which we can use to Absorb data, and then Squeeze out arbitrary-length output. We use a SHO
// object not only to produce the Schnorr challenge, but also to produce the Schnorr nonce by
// hashing some caller-supplied random data, the witness, and the message. This "synthetic nonce"
// strategy is intended to ensure that the nonce appears random to any attacker and that different
// Schnorr challenges will never be used with the same nonce.
//
// Below we describe the hash inputs to SHO/HMAC-SHA256:
//
// L : bytes, label = "POKSHO_Ristretto_SHOHMACSHA256"
// D : bytes, description of statement - see below
// a : G1, witness scalars for statement
// A : G2, point values for statement = homomorphism(a)
// Z : bytes, random = 32 byes of randomness
// M : bytes, message to be signed, if any
// r : G1, Schnorr nonce
// R : G2, Schnorr commitment = homomorphism(r)
// h : integer, Schnorr challenge
// sho = SHO(L)
// sho.AbsorbAndRatchet(D || A)
// sho2 = sho.Clone()
// sho2.AbsorbAndRatchet(Z || a)
// sho2.AbsorbAndRatchet(M)
// r = sho2.Squeeze(64 * num_scalars)
// sho.AbsorbAndRatchet(R || M)
// h = Squeeze(64)
//
// Description format (D)
// ---
// Ne : number of equations (1-255)
// for i=1..Ne:
// point_index : 0..255 (0 = base point)
// Nt : number of terms (1-255)
// for j=1..Nt:
// scalar_index: 0-255
// point_index: 0-255 (0 = base point)
//
// Point values for statement (A) and commitment (R)
// ---
// for index=1..total number of points (excluding base point at index 0):
// RistrettoPoint
//
// Witness (a)
// ---
// for index=0..total number of scalars:
// RistrettoScalar
use PokshoError::*;
use curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT;
use curve25519_dalek::ristretto::RistrettoPoint;
use curve25519_dalek::scalar::Scalar;
use curve25519_dalek::traits::MultiscalarMul;
use crate::args::*;
use crate::errors::*;
use crate::proof::*;
use crate::shoapi::{ShoApi, ShoApiExt as _};
use crate::shohmacsha256::ShoHmacSha256;
use crate::simple_types::*;
type ScalarIndex = u8;
type PointIndex = u8;
struct Term {
scalar: ScalarIndex,
point: PointIndex,
}
struct Equation {
lhs: PointIndex,
rhs: Vec<Term>,
}
pub struct Statement {
// We store the Schnorr ZKP equations using scalar and point indices
// which are numbered from zero, and are assigned sequentially based
// on the order in which the point or scalar appears in the equations
// (except point index 0 is pre-assigned to "G", the Ristretto base point)
//
// We also store maps from string names -> indices, and vectors for
// the reverse map. The former map is used when adding new equations,
// and the latter is used when instantiating these indices with
// concrete values.
equations: Vec<Equation>,
scalar_map: HashMap<Cow<'static, str>, ScalarIndex>,
scalar_vec: Vec<Cow<'static, str>>,
point_map: HashMap<Cow<'static, str>, PointIndex>,
point_vec: Vec<Cow<'static, str>>,
}
impl Statement {
pub fn new() -> Self {
let mut point_map = HashMap::new();
point_map.insert("G".into(), 0); // G is base point
let point_vec = vec!["G".into()];
Statement {
equations: Vec::new(),
scalar_map: HashMap::new(),
scalar_vec: Vec::new(),
point_map,
point_vec,
}
}
// panics on invalid input
pub fn add(&mut self, lhs_str: &str, rhs_pairs: &[(&str, &str)]) {
if (lhs_str.is_empty())
|| (rhs_pairs.is_empty())
|| (rhs_pairs.len() > 255)
|| (self.equations.len() >= 255)
{
panic!("Unexpected input sizes to add");
}
let lhs = self
.add_point(lhs_str.to_string())
.expect("add_point succeeds");
let mut rhs = Vec::<Term>::with_capacity(rhs_pairs.len());
for pair in rhs_pairs {
if pair.0.is_empty() || pair.1.is_empty() {
panic!("Unexpected pair size");
}
let scalar = self
.add_scalar(pair.0.to_string())
.expect("add_scalar succeeds");
let point = self
.add_point(pair.1.to_string())
.expect("add_point succeeds");
rhs.push(Term { scalar, point });
}
self.equations.push(Equation { lhs, rhs });
}
pub fn prove(
&self,
scalar_args: &ScalarArgs,
point_args: &PointArgs,
message: &[u8],
randomness: &[u8], // must be 32 bytes
) -> Result<Vec<u8>, PokshoError> {
if randomness.len() != 32 {
return Err(PokshoError::BadArgs);
}
let g1 = self.sort_scalars(scalar_args)?;
let all_points = self.sort_points(point_args)?;
// Absorb the protocol label L, description of statement D, and point values A
let mut sho = ShoHmacSha256::new(b"POKSHO_Ristretto_SHOHMACSHA256"); // L
sho.absorb(&self.to_bytes()); // D
for point in &all_points {
// A
sho.absorb(&point.compress().to_bytes());
}
sho.ratchet(); // Ratchet
// Random nonce
// "Synthetic" nonce based on hashing randomness, witness (private scalars) and message
let mut sho2 = sho.clone();
sho2.absorb(randomness); // Z
for scalar in &g1 {
sho2.absorb(&scalar.to_bytes()); // a
}
sho2.ratchet(); // Ratchet
sho2.absorb_and_ratchet(message); // M
let blinding_scalar_bytes = sho2.squeeze_and_ratchet(g1.len() * 64);
let nonce: G1 = blinding_scalar_bytes
.as_chunks::<64>()
.0
.iter()
.map(Scalar::from_bytes_mod_order_wide)
.collect();
// Commitment from nonce by applying homomorphism F: commitment = F(nonce)
let commitment = self.homomorphism_with_subtraction(&nonce, &all_points, None);
// Challenge from commitment and message
for point in &commitment {
sho.absorb(&point.compress().to_bytes());
}
sho.absorb_and_ratchet(message);
let challenge = Scalar::from_bytes_mod_order_wide(&sho.squeeze_and_ratchet_as_array());
// Response
let response = nonce
.into_iter()
.zip(g1)
.map(|(nonce, g1)| nonce + (g1 * challenge))
.collect();
let proof = Proof {
challenge,
response,
};
// Verify before returning, since a bad proof could indicate
// a glitched/faulty response that leaks private keys, or incorrect inputs
let proof_bytes = proof.to_bytes();
match self.verify_proof(&proof_bytes, point_args, message) {
Err(VerificationFailure) => Err(ProofCreationVerificationFailure),
Err(e) => Err(e),
Ok(_) => Ok(proof_bytes),
}
}
pub fn verify_proof(
&self,
proof_bytes: &[u8],
point_args: &PointArgs,
message: &[u8],
) -> Result<(), PokshoError> {
let proof = Proof::from_slice(proof_bytes).ok_or(VerificationFailure)?;
if proof.response.len() != self.scalar_vec.len() {
return Err(VerificationFailure);
}
let all_points = self.sort_points(point_args)?;
// Absorb the protocol label L, statement description D, and point values A
let mut sho = ShoHmacSha256::new(b"POKSHO_Ristretto_SHOHMACSHA256"); // L
sho.absorb(&self.to_bytes()); // D
for point in &all_points {
// A
sho.absorb(&point.compress().to_bytes());
}
sho.ratchet();
// Reconstruct commitment
//
// commitment R = F(s) - h*A
//
// F: homomorphism
// s: response element in G1
// h: challenge scalar
// A: element in G2 whose preimage we are proving knowledge of (i.e. LHS of Schnorr eqns)
let commitment =
self.homomorphism_with_subtraction(&proof.response, &all_points, Some(proof.challenge));
// Reconstruct challenge from commitment and message
for point in &commitment {
// R
sho.absorb(&point.compress().to_bytes());
}
sho.absorb_and_ratchet(message); // M
let challenge = Scalar::from_bytes_mod_order_wide(&sho.squeeze_and_ratchet_as_array());
// Check challenge (const time)
if challenge == proof.challenge {
Ok(())
} else {
Err(VerificationFailure)
}
}
fn add_scalar(
&mut self,
scalar_name: impl Into<Cow<'static, str>>,
) -> Result<ScalarIndex, PokshoError> {
let scalar_name = scalar_name.into();
match self.scalar_map.get(&scalar_name) {
Some(index) => Ok(*index),
None => {
assert!(self.scalar_map.len() == self.scalar_vec.len());
let Ok(new_index) = self.scalar_map.len().try_into() else {
return Err(BadArgs);
};
self.scalar_map.insert(scalar_name.clone(), new_index);
self.scalar_vec.push(scalar_name.clone());
Ok(new_index)
}
}
}
fn add_point(
&mut self,
point_name: impl Into<Cow<'static, str>>,
) -> Result<PointIndex, PokshoError> {
let point_name = point_name.into();
match self.point_map.get(&point_name) {
Some(index) => Ok(*index),
None => {
assert!(self.point_map.len() == self.point_vec.len());
let Ok(new_index) = self.point_map.len().try_into() else {
return Err(BadArgs);
};
self.point_map.insert(point_name.clone(), new_index);
self.point_vec.push(point_name.clone());
Ok(new_index)
}
}
}
fn to_bytes(&self) -> Vec<u8> {
let equation_count =
u8::try_from(self.equations.len()).expect("number of equations fits in a byte");
let scalar_count =
u8::try_from(self.scalar_map.len()).expect("number of scalars fits in a byte");
let point_count =
u8::try_from(self.point_map.len()).expect("number of points fits in a byte");
let mut v = vec![equation_count];
for Equation { lhs, rhs } in &self.equations {
assert!(*lhs <= point_count);
v.push(*lhs);
let term_count = u8::try_from(rhs.len()).expect("number of terms fits in a byte");
v.push(term_count);
for Term { scalar, point } in rhs {
assert!(*scalar < scalar_count);
assert!(*point < point_count);
v.push(*scalar);
v.push(*point);
}
}
v
}
// Applies the homomorphism from G1 -> G2
// If given a challenge h, also subtracts h*A for efficient recovery of
// the Schnorr commitment
fn homomorphism_with_subtraction(
&self,
g1: &[Scalar],
all_points: &[RistrettoPoint],
challenge: Option<Scalar>,
) -> G2 {
self.equations
.iter()
.map(|e| {
let scalar_iter = e
.rhs
.iter()
.map(|Term { scalar, point: _ }| g1[*scalar as usize]);
let point_iter = e
.rhs
.iter()
.map(|Term { scalar: _, point }| all_points[*point as usize]);
let (v_scalar, v_point) =
challenge.map(|h| (-h, all_points[e.lhs as usize])).unzip();
let scalar_iter = scalar_iter.chain(v_scalar);
let point_iter = point_iter.chain(v_point);
// Could use vartime_multiscalar_mul in some cases, but in the
// general case points might be secret (not just scalars!)
RistrettoPoint::multiscalar_mul(scalar_iter, point_iter)
})
.collect()
}
fn sort_scalars(&self, scalar_args: &ScalarArgs) -> Result<G1, PokshoError> {
if scalar_args.0.len() != self.scalar_vec.len() {
return Err(BadArgsWrongNumberOfScalarArgs);
}
self.scalar_vec
.iter()
.map(|scalar_name| {
scalar_args
.0
.get(scalar_name)
.copied()
.ok_or(BadArgsMissingScalarArg)
})
.collect()
}
fn sort_points(&self, point_args: &PointArgs) -> Result<Vec<RistrettoPoint>, PokshoError> {
if point_args.0.len() != self.point_vec.len() - 1 {
return Err(BadArgsWrongNumberOfPointArgs);
}
let try_iter_points = self.point_vec[1..].iter().map(|point_name| {
point_args
.0
.get(point_name)
.copied()
.ok_or(BadArgsMissingPointArg)
});
[Ok(RISTRETTO_BASEPOINT_POINT)]
.into_iter()
.chain(try_iter_points)
.collect()
}
}
impl Default for Statement {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
#![allow(non_snake_case)]
use super::*;
#[test]
fn test_statement_encoding() {
let mut s = Statement::new();
s.add("A", &[("a", "G")]);
assert!(s.to_bytes() == vec![1, 1, 1, 0, 0]);
let mut s = Statement::new();
s.add("A", &[("a", "G")]);
s.add("B", &[("a", "H")]);
assert!(s.to_bytes() == vec![2, 1, 1, 0, 0, 2, 1, 0, 3]);
let mut s = Statement::new();
s.add("A", &[("a", "G"), ("b", "H")]);
assert!(s.to_bytes() == vec![1, 1, 2, 0, 0, 1, 2]);
}
#[test]
#[allow(
clippy::cast_possible_truncation,
clippy::needless_range_loop,
clippy::redundant_clone,
clippy::unwrap_used
)]
fn test_complex_statement() {
let mut block32 = [0u8; 32];
let mut block64a = [0u8; 64];
let mut block64b = [0u8; 64];
let mut block64c = [0u8; 64];
let mut block64d = [0u8; 64];
let mut block64h = [0u8; 64];
let mut block64i = [0u8; 64];
let block0 = [0u8; 0];
for i in 0..32 {
block32[i] = i as u8;
}
for i in 0..64 {
block64a[i] = 10 + i as u8;
}
for i in 0..64 {
block64b[i] = 20 + i as u8;
}
for i in 0..64 {
block64c[i] = 30 + i as u8;
}
for i in 0..64 {
block64d[i] = 40 + i as u8;
}
for i in 0..64 {
block64h[i] = 50 + i as u8;
}
for i in 0..64 {
block64i[i] = 60 + i as u8;
}
let randomness = block32;
let message = block0;
let scalar_bytes_a = block64a;
let scalar_bytes_b = block64b;
let scalar_bytes_c = block64c;
let scalar_bytes_d = block64d;
let scalar_bytes_h_point = block64h;
let scalar_bytes_i_point = block64i;
let a = Scalar::from_bytes_mod_order_wide(&scalar_bytes_a);
let b = Scalar::from_bytes_mod_order_wide(&scalar_bytes_b);
let c = Scalar::from_bytes_mod_order_wide(&scalar_bytes_c);
let d = Scalar::from_bytes_mod_order_wide(&scalar_bytes_d);
let H =
Scalar::from_bytes_mod_order_wide(&scalar_bytes_h_point) * RISTRETTO_BASEPOINT_POINT;
let I =
Scalar::from_bytes_mod_order_wide(&scalar_bytes_i_point) * RISTRETTO_BASEPOINT_POINT;
let A = a * RISTRETTO_BASEPOINT_POINT + b * H + c * I;
let B = c * H + d * I;
let mut st = Statement::new();
st.add("A", &[("a", "G"), ("b", "H"), ("c", "I")]);
st.add("B", &[("c", "H"), ("d", "I")]);
assert!(st.to_bytes() == vec![2, 1, 3, 0, 0, 1, 2, 2, 3, 4, 2, 2, 2, 3, 3]);
let mut scalar_args = ScalarArgs::new();
scalar_args.add("a", a);
scalar_args.add("b", b);
scalar_args.add("c", c);
scalar_args.add("d", d);
let mut point_args = PointArgs::new();
point_args.add("A", A);
point_args.add("B", B);
point_args.add("H", H);
point_args.add("I", I);
let mut scalar_args2 = scalar_args.clone();
scalar_args2.add("abc", a);
// Test bad args - extra scalar
assert!(matches!(
st.prove(&scalar_args2, &point_args, &message, &randomness),
Err(PokshoError::BadArgsWrongNumberOfScalarArgs)
));
// Good proof
let mut proof = st
.prove(&scalar_args, &point_args, &message, &randomness)
.unwrap();
st.verify_proof(&proof, &point_args, &message).unwrap();
/*
for b in proof.iter() {
print!("0x{:02x}, ", b);
}
println!("");
*/
assert!(
proof
== vec![
0x8e, 0xfc, 0x67, 0x6c, 0x33, 0xe6, 0xb2, 0xd0, 0x67, 0x0e, 0xd5, 0x46, 0x1a,
0x50, 0x7f, 0x6a, 0x4b, 0xc9, 0x15, 0x3e, 0x26, 0x1d, 0xb8, 0x0f, 0xa4, 0x38,
0xf3, 0xcd, 0x80, 0xa5, 0xc9, 0x09, 0xb1, 0x13, 0xcc, 0x0d, 0x79, 0x90, 0xad,
0x61, 0x6d, 0x0a, 0x2f, 0xc4, 0xb8, 0x31, 0xd0, 0x63, 0x57, 0xa5, 0xee, 0x5d,
0x36, 0xd4, 0x4b, 0x34, 0x27, 0xc7, 0x90, 0x10, 0x61, 0x18, 0x0c, 0x0f, 0xb1,
0x79, 0x8c, 0x51, 0x68, 0x0f, 0xe2, 0x1b, 0x9f, 0x98, 0xe9, 0x79, 0x55, 0xb1,
0x59, 0x7c, 0x49, 0x31, 0x47, 0x25, 0xc1, 0x54, 0x6a, 0x36, 0x93, 0x28, 0xcf,
0x54, 0xda, 0xae, 0x71, 0x0b, 0xfc, 0x4a, 0x99, 0x11, 0x42, 0x2a, 0xa7, 0x7e,
0xd6, 0xd7, 0x23, 0x1d, 0xe3, 0x00, 0x3b, 0xa5, 0xae, 0x9d, 0x9f, 0xd0, 0xc5,
0x3c, 0xed, 0x7a, 0xd7, 0x82, 0xe2, 0x9b, 0x04, 0x68, 0x4a, 0x07, 0x22, 0x1a,
0x6e, 0xf4, 0x7c, 0xe6, 0x1d, 0x81, 0x7f, 0x01, 0x11, 0x7c, 0xf5, 0x9d, 0xf6,
0x9a, 0xc3, 0x5b, 0x5b, 0xb5, 0x90, 0xf1, 0xf7, 0xb6, 0xd0, 0x29, 0x71, 0x7b,
0xc1, 0xa6, 0x25, 0x01,
]
);
// Test bad args - extra point
let mut point_args2 = point_args.clone();
point_args2.add("xyz", A);
assert!(matches!(
st.verify_proof(&proof, &point_args2, &message),
Err(PokshoError::BadArgsWrongNumberOfPointArgs)
));
// Test bad message
assert!(matches!(
st.verify_proof(&proof, &point_args, &block32),
Err(VerificationFailure)
));
// Test bad proof #1 - extra byte at end
let mut proof2 = proof.clone();
proof2.push(0);
assert!(matches!(
st.verify_proof(&proof2, &point_args, &message),
Err(VerificationFailure)
));
// Test bad proof #2 - last byte changed
let prooflen = proof.len();
proof[prooflen - 1] += 1;
assert!(matches!(
st.verify_proof(&proof, &point_args, &message),
Err(VerificationFailure)
));
// Test bad proof #3 - incorrect # of scalars (1 too few)
let mut proof2 = proof.clone();
proof2.truncate(proof2.len() - 32);
assert!(matches!(
st.verify_proof(&proof2, &point_args, &message),
Err(VerificationFailure)
));
// Test bad proof #3 - incorrect # of scalars (1 too few)
let mut proof2 = proof.clone();
proof2.truncate(proof2.len() - 32);
assert!(matches!(
st.verify_proof(&proof2, &point_args, &message),
Err(VerificationFailure)
));
}
}
File diff suppressed because it is too large Load Diff
+116
View File
@@ -0,0 +1,116 @@
//
// Copyright 2020 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use curve25519_dalek_signal::constants::RISTRETTO_BASEPOINT_POINT;
use curve25519_dalek_signal::ristretto::RistrettoPoint;
use curve25519_dalek_signal::scalar::Scalar;
use partial_default::PartialDefault;
use serde::{Deserialize, Serialize};
use crate::common::constants::*;
use crate::common::errors::*;
use crate::common::sho::*;
use crate::common::simple_types::*;
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialDefault)]
pub struct KeyPair {
pub(crate) signing_key: Scalar,
pub(crate) public_key: RistrettoPoint,
}
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialDefault)]
pub struct PublicKey {
pub(crate) public_key: RistrettoPoint,
}
impl KeyPair {
pub fn generate(sho: &mut Sho) -> Self {
let signing_key = sho.get_scalar();
let public_key = signing_key * RISTRETTO_BASEPOINT_POINT;
KeyPair {
signing_key,
public_key,
}
}
pub fn sign(&self, message: &[u8], sho: &mut Sho) -> SignatureBytes {
let vec_bytes = poksho::sign(
self.signing_key,
self.public_key,
message,
&sho.squeeze_as_array::<RANDOMNESS_LEN>(),
)
.expect("signature failed to self-verify; bad public key?");
let mut s: SignatureBytes = [0u8; SIGNATURE_LEN];
s.copy_from_slice(&vec_bytes[..]);
s
}
pub fn get_public_key(&self) -> PublicKey {
PublicKey {
public_key: self.public_key,
}
}
}
impl PublicKey {
// Might return VerificationFailure
pub fn verify(
&self,
message: &[u8],
signature: SignatureBytes,
) -> Result<(), ZkGroupVerificationFailure> {
match poksho::verify_signature(&signature, self.public_key, message) {
Err(_) => Err(ZkGroupVerificationFailure),
Ok(_) => Ok(()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_signature() {
let group_key = TEST_ARRAY_32;
let mut sho = Sho::new(b"Test_Signature", &group_key);
let key_pair = KeyPair::generate(&mut sho);
// Test serialize of key_pair
let key_pair_bytes = bincode::serialize(&key_pair).unwrap();
assert!(key_pair_bytes.len() == 64);
let public_key_bytes = bincode::serialize(&key_pair.get_public_key()).unwrap();
assert!(public_key_bytes.len() == 32);
let key_pair2: KeyPair = bincode::deserialize(&key_pair_bytes).unwrap();
assert!(key_pair == key_pair2);
let mut message = TEST_ARRAY_32_1;
let signature = key_pair.sign(&message, &mut sho);
key_pair2
.get_public_key()
.verify(&message, signature)
.unwrap();
// test signature failure
message[0] ^= 1;
key_pair2
.get_public_key()
.verify(&message, signature)
.expect_err("signature verify should have failed");
println!("signature = {:#x?}", &signature[..]);
let signature_result = [
0xdb, 0x9b, 0xfb, 0xd6, 0x15, 0x26, 0xc3, 0x50, 0xf9, 0xbe, 0x95, 0x17, 0x11, 0x6,
0xd0, 0x6, 0x52, 0x88, 0xcb, 0x33, 0x3, 0x1b, 0xe7, 0x17, 0x25, 0x24, 0x37, 0x80, 0x53,
0x2c, 0xaa, 0x7, 0xcb, 0xda, 0x74, 0xc4, 0x19, 0x3b, 0x6e, 0xe6, 0xe9, 0x5f, 0xae,
0xcd, 0x41, 0xfb, 0x44, 0x19, 0xce, 0xae, 0x3f, 0x4d, 0x63, 0xb9, 0x47, 0x59, 0x27,
0xe1, 0x10, 0xee, 0xb7, 0x72, 0xb, 0x6,
];
assert!(signature[..] == signature_result[..]);
}
}
+420
View File
@@ -0,0 +1,420 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
//! Traits used for the attributes in a credential.
//!
//! Your attribute types must implement one of these traits. There are three kinds of supported
//! attributes:
//! - [`PublicAttribute`], which does not need to be hidden from the issuing server or verifying
//! server.
//! - [`Attribute`] (the reason for this entire credential system), which is hidden from the
//! verifying server using verifiable encryption, and may be hidden from the issuing server as
//! well with [blind issuance](crate::issuance::blind).
//! - [`RevealedAttribute`], which is hidden from the issuing server and then revealed to the
//! verifying server.
use std::marker::PhantomData;
use curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT;
use curve25519_dalek::ristretto::RistrettoPoint;
use curve25519_dalek::scalar::Scalar;
use derive_where::derive_where;
use partial_default::PartialDefault;
use poksho::ShoApi;
use serde::{Deserialize, Serialize};
use subtle::ConstantTimeEq;
use crate::VerificationFailure;
use crate::sho::ShoExt;
/// An attribute that doesn't need to be hidden from the issuing server or verifying server.
///
/// This can be encoded more efficiently, making for smaller, faster proofs.
/// (All public attributes get hashed together along with the credential type.)
pub trait PublicAttribute {
/// Mixes `self` into the hash computed by `sho`.
///
/// This will usually be implemented by calling [`ShoApi::absorb_and_ratchet`] one or more
/// times.
fn hash_into(&self, sho: &mut dyn ShoApi);
}
impl PublicAttribute for [u8] {
fn hash_into(&self, sho: &mut dyn ShoApi) {
sho.absorb_and_ratchet(self)
}
}
impl<const LEN: usize> PublicAttribute for [u8; LEN] {
fn hash_into(&self, sho: &mut dyn ShoApi) {
self.as_slice().hash_into(sho)
}
}
impl PublicAttribute for u32 {
fn hash_into(&self, sho: &mut dyn ShoApi) {
self.to_be_bytes().hash_into(sho)
}
}
impl PublicAttribute for u64 {
fn hash_into(&self, sho: &mut dyn ShoApi) {
self.to_be_bytes().hash_into(sho)
}
}
/// An attribute representable as a pair of [`RistrettoPoint`s](RistrettoPoint).
///
/// Used for credential attributes that need to take advantage of homomorphic encryption. Attributes
/// that never need to be hidden should use [`PublicAttribute`] instead. Attributes that only need
/// to be hidden during issuance may use the more compact [`RevealedAttribute`] instead.
///
/// For an attribute that is encrypted, both the attribute type and its corresponding ciphertext
/// type should conform to this trait. Note that blinded attributes do not conform to this trait, as
/// they have a different representation.
pub trait Attribute {
/// Converts `self` into a pair of points.
///
/// It is strongly recommended for an attribute's non-encrypted form that you generate the first
/// point by hashing and use the second to encode your encrypted information.
///
/// The encrypted attribute should apply [`KeyPair`]'s encryption to the point.
fn as_points(&self) -> [RistrettoPoint; 2];
}
impl Attribute for [RistrettoPoint; 2] {
fn as_points(&self) -> [RistrettoPoint; 2] {
*self
}
}
/// A domain for a [`KeyPair`].
///
/// This provides separation between different keys and ciphertexts, so that statically and
/// dynamically they can't get mixed up or substituted for one another.
///
/// # Example
///
/// ```
/// # use curve25519_dalek::RistrettoPoint;
/// # type UserId = [RistrettoPoint; 2];
/// struct UserIdEncryption;
/// impl zkcredential::attributes::Domain for UserIdEncryption {
/// type Attribute = UserId;
/// const ID: &'static str = "MyCompany_UserIdEncryption_20231011";
///
/// fn G_a() -> [RistrettoPoint; 2] {
/// static STORAGE: std::sync::OnceLock<[RistrettoPoint; 2]> = std::sync::OnceLock::new();
/// *zkcredential::attributes::derive_default_generator_points::<Self>(&STORAGE)
/// }
/// }
/// ```
pub trait Domain {
/// The attribute type used in this encryption domain.
type Attribute: Attribute;
/// A unique ID for this key (and its corresponding key pair)
///
/// This is used to identify and distinguish keys when constructing or validating a proof,
/// so make sure it's unique!
const ID: &'static str;
/// The "generator points" for this key
///
/// This can be a statically-chosen pair of points; it's used to construct the `A` point for a
/// [`PublicKey`].
///
/// A reasonable default implementation would use `derive_default_generator_points` with static
/// storage, for caching the resulting points:
///
/// ```
/// # use curve25519_dalek::RistrettoPoint;
/// # struct Example;
/// # impl zkcredential::attributes::Domain for Example {
/// # type Attribute = [RistrettoPoint; 2];
/// # const ID: &'static str = "20231030_Example";
/// fn G_a() -> [RistrettoPoint; 2] {
/// static STORAGE: std::sync::OnceLock<[RistrettoPoint; 2]> = std::sync::OnceLock::new();
/// *zkcredential::attributes::derive_default_generator_points::<Self>(&STORAGE)
/// }
/// # }
/// ```
///
/// Unfortunately this can't be provided as a default implementation, because that would result
/// in every domain sharing the same `STORAGE`, as if it were declared outside the trait.
fn G_a() -> [RistrettoPoint; 2];
}
/// Derives reasonable generator points `G_a` for `D`, based on its [`ID`][Domain::ID], and caches
/// them in `storage`.
pub fn derive_default_generator_points<D: Domain>(
storage: &std::sync::OnceLock<[RistrettoPoint; 2]>,
) -> &[RistrettoPoint; 2] {
fn derive_impl<D: Domain>() -> [RistrettoPoint; 2] {
let mut sho = poksho::ShoHmacSha256::new(b"Signal_ZKCredential_Domain_20231011");
sho.absorb_and_ratchet(D::ID.as_bytes());
let G_a1 = sho.get_point();
let G_a2 = sho.get_point();
[G_a1, G_a2]
}
let result = storage.get_or_init(derive_impl::<D>);
debug_assert!(
result == &derive_impl::<D>(),
"initialized with non-default points for {}",
D::ID,
);
result
}
/// A key used to encrypt attributes.
///
/// Using different keys for different attribute types prevents "type confusion", where two
/// attributes coincidentally have the same encoding as RistrettoPoints. The encryption may also
/// have other purposes, such as the encryption of UUIDs and profile keys in a Signal group, and
/// therefore being able to use existing keys is important.
///
/// The private key in this system is a pair of scalars `a1` and `a2`. Attributes are encrypted as
/// `E_A1 = a1 * M1; E_A2 = a2 * E_A1 + M2`.
///
/// Defined in Chase-Perrin-Zaverucha section 4.1.
///
/// See also [`PublicKey`].
#[derive(Serialize, Deserialize, PartialDefault)]
#[derive_where(Clone, Copy, Eq)]
#[partial_default(bound = "")]
#[non_exhaustive]
#[allow(missing_docs)]
pub struct KeyPair<D> {
pub a1: Scalar,
pub a2: Scalar,
#[serde(bound = "")]
pub public_key: PublicKey<D>,
}
impl<D> subtle::ConstantTimeEq for KeyPair<D> {
fn ct_eq(&self, other: &Self) -> subtle::Choice {
self.a1.ct_eq(&other.a1) & self.a2.ct_eq(&other.a2)
}
}
impl<D> PartialEq for KeyPair<D> {
fn eq(&self, other: &Self) -> bool {
self.ct_eq(other).into()
}
}
/// A key used to validate encrypted attributes.
///
/// Using different keys for different attribute types prevents "type confusion", where two
/// attributes coincidentally have the same encoding as RistrettoPoints. The encryption may also
/// have other purposes, such as the encryption of UUIDs and profile keys in a Signal group, and
/// therefore being able to use existing keys is important.
///
/// Defined in Chase-Perrin-Zaverucha section 4.1.
///
/// See also [`KeyPair`].
#[derive(Serialize, Deserialize, PartialDefault)]
#[derive_where(Clone, Copy, Eq)]
#[partial_default(bound = "")]
pub struct PublicKey<D> {
#[allow(missing_docs)]
pub A: RistrettoPoint,
#[serde(skip)]
domain: PhantomData<fn(D) -> D>,
}
impl<D> subtle::ConstantTimeEq for PublicKey<D> {
fn ct_eq(&self, other: &Self) -> subtle::Choice {
self.A.ct_eq(&other.A)
}
}
impl<D> PartialEq for PublicKey<D> {
fn eq(&self, other: &Self) -> bool {
self.ct_eq(other).into()
}
}
impl<D: Domain> KeyPair<D> {
/// Generates a new KeyPair from the hash state in `sho`.
///
/// Passing the same `sho` state in will produce the same key pair every time.
pub fn derive_from(sho: &mut dyn ShoApi) -> Self {
let a1 = sho.get_scalar();
let a2 = sho.get_scalar();
Self::from_scalars(a1, a2)
}
fn from_scalars(a1: Scalar, a2: Scalar) -> Self {
let [G_a1, G_a2] = D::G_a();
let A = a1 * G_a1 + a2 * G_a2;
Self {
a1,
a2,
public_key: PublicKey {
A,
domain: PhantomData,
},
}
}
/// Creates a KeyPair that's the inverse of `other`.
///
/// That is, if `k_inv` is `KeyPair::inverse_of(k)`, then `attr.as_points() ==
/// k_inv.encrypt(k.encrypt(&attr))`.
///
/// Note that the domain of `Self` doesn't have to be related to the domain of `other`. This can
/// be useful when the inverted key is used on derived values.
///
/// Don't use this to decrypt points; there are more efficient ways to do that. See
/// [`Self::decrypt_to_second_point`].
pub fn inverse_of<D2: Domain>(other: &KeyPair<D2>) -> Self {
assert_ne!(
D::ID,
D2::ID,
"You must provide a new domain for an inverse key"
);
let a1 = other.a1.invert();
let a2 = -(other.a1 * other.a2);
Self::from_scalars(a1, a2)
}
/// Encrypts `attr` according to Chase-Perrin-Zaverucha section 4.1.
#[inline]
pub fn encrypt(&self, attr: &D::Attribute) -> Ciphertext<D> {
self.encrypt_arbitrary_attribute(attr)
}
/// Encrypts `attr` according to Chase-Perrin-Zaverucha section 4.1, even if the attribute is
/// not normally associated with this key.
///
/// Allows controlling the domain of the resulting ciphertext, to not get confused with the
/// usual ciphertexts produced by [`Self::encrypt`].
#[inline]
pub fn encrypt_arbitrary_attribute<D2>(&self, attr: &dyn Attribute) -> Ciphertext<D2> {
let [M1, M2] = attr.as_points();
let E_A1 = self.a1 * M1;
let E_A2 = (self.a2 * E_A1) + M2;
Ciphertext {
E_A1,
E_A2,
domain: PhantomData,
}
}
/// Returns the second point from the plaintext that produced `ciphertext`
///
/// The encryption form allows recovering M2 from the ciphertext as `M2 = E_A2 - a2 * E_A1`. For
/// certain attributes, this may be enough to recover the value, making this a reversible
/// encryption system. However, it is **critical** to check that the decoded value produces the
/// same `E_A1` when re-encrypted:
///
/// ```ignored
/// a1 * HashToPoint(DecodeFromPoint(M2)) == E_A1
/// ```
///
/// This addresses the fact that this method is otherwise "garbage in, garbage out": it will
/// "decrypt" *any* ciphertext passed to it regardless of whether or not that ciphertext came
/// from a valid plaintext, encrypted using the same key.
///
/// Produces an error if `E_A1` is the Ristretto basepoint, which would imply that `a1` is not
/// actually encrypting anything.
///
/// Defined in Chase-Perrin-Zaverucha section 3.1.
pub fn decrypt_to_second_point(
&self,
ciphertext: &Ciphertext<D>,
) -> Result<RistrettoPoint, VerificationFailure> {
if ciphertext.E_A1 == RISTRETTO_BASEPOINT_POINT {
return Err(VerificationFailure);
}
Ok(ciphertext.E_A2 - self.a2 * ciphertext.E_A1)
}
}
/// An attribute encrypted with [`KeyPair::encrypt`].
#[derive(Serialize, Deserialize, PartialDefault)]
#[derive_where(Clone, Copy, Eq)]
#[partial_default(bound = "")]
pub struct Ciphertext<D> {
E_A1: RistrettoPoint,
E_A2: RistrettoPoint,
#[serde(skip)]
domain: PhantomData<fn(D) -> D>,
}
impl<D> subtle::ConstantTimeEq for Ciphertext<D> {
fn ct_eq(&self, other: &Self) -> subtle::Choice {
self.E_A1.ct_eq(&other.E_A1) & self.E_A2.ct_eq(&other.E_A2)
}
}
impl<D> PartialEq for Ciphertext<D> {
fn eq(&self, other: &Self) -> bool {
self.ct_eq(other).into()
}
}
impl<D> Attribute for Ciphertext<D> {
#[inline]
fn as_points(&self) -> [RistrettoPoint; 2] {
[self.E_A1, self.E_A2]
}
}
/// An attribute that is [blinded](crate::issuance::blind) to the issuing server but revealed to the
/// verifying server.
///
/// Used only in the very specific case described above. Attributes that never need to be hidden
/// should use [`PublicAttribute`] instead; attributes that need to be hidden from the verifying
/// server should use the standard [`Attribute`].
///
/// This scenario does not appear in the Chase-Perrin-Zaverucha paper, but is a simplified version
/// of the blind issuance protocol shown in section 5.9.
pub trait RevealedAttribute {
/// Converts `self` to a point.
///
/// It is strongly recommended you do this by hashing unless you have a specific reason to do
/// otherwise.
fn as_point(&self) -> RistrettoPoint;
}
impl RevealedAttribute for RistrettoPoint {
fn as_point(&self) -> RistrettoPoint {
*self
}
}
#[cfg(test)]
mod tests {
use std::sync::OnceLock;
use super::*;
struct ExampleDomain;
impl Domain for ExampleDomain {
type Attribute = [RistrettoPoint; 2];
const ID: &'static str = "TestDomain";
fn G_a() -> [RistrettoPoint; 2] {
static STORAGE: OnceLock<[RistrettoPoint; 2]> = OnceLock::new();
*derive_default_generator_points::<Self>(&STORAGE)
}
}
#[test]
fn derive_default_generator_points_works() {
let _ = ExampleDomain::G_a();
}
#[test]
#[should_panic]
#[cfg(debug_assertions)]
fn derive_default_generator_points_checks_for_reuse_in_debug_builds() {
let storage = std::sync::OnceLock::from([RistrettoPoint::default(); 2]);
derive_default_generator_points::<ExampleDomain>(&storage);
}
}
+262
View File
@@ -0,0 +1,262 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
//! Types used in both the issuance and presentation of credentials
use std::sync::LazyLock;
use curve25519_dalek::ristretto::RistrettoPoint;
use curve25519_dalek::scalar::Scalar;
use partial_default::PartialDefault;
use poksho::{ShoApi, ShoHmacSha256, ShoSha256};
use serde::{Deserialize, Serialize};
use crate::RANDOMNESS_LEN;
use crate::sho::ShoExt;
/// A credential created by the issuing server over a set of attributes.
///
/// Defined in Chase-Perrin-Zaverucha section 3.1.
#[derive(Clone, Serialize, Deserialize, PartialDefault)]
// This type intentionally does not implement `Copy` to make it harder to
// accidentally duplicate these values.
pub struct Credential {
pub(crate) t: Scalar,
pub(crate) U: RistrettoPoint,
pub(crate) V: RistrettoPoint,
}
/// A secret key used to compute a MAC over a set of attributes
///
/// Defined in Chase-Perrin-Zaverucha section 3.1.
#[derive(Serialize, Deserialize, Clone, PartialDefault)]
pub(crate) struct CredentialPrivateKey {
pub(crate) w: Scalar,
pub(crate) wprime: Scalar,
pub(crate) W: RistrettoPoint,
pub(crate) x0: Scalar,
pub(crate) x1: Scalar,
pub(crate) y: [Scalar; NUM_SUPPORTED_ATTRS],
}
impl CredentialPrivateKey {
/// Creates a new secret key using the given source of random bytes.
fn generate(randomness: [u8; RANDOMNESS_LEN]) -> Self {
let mut sho =
ShoHmacSha256::new(b"Signal_ZKCredential_CredentialPrivateKey_generate_20230410");
sho.absorb_and_ratchet(&randomness);
let system = *SYSTEM_PARAMS;
let w = sho.get_scalar();
let W = w * system.G_w;
let wprime = sho.get_scalar();
let x0 = sho.get_scalar();
let x1 = sho.get_scalar();
let y = [(); NUM_SUPPORTED_ATTRS].map(|_| sho.get_scalar());
Self {
w,
wprime,
W,
x0,
x1,
y,
}
}
/// Produces a MAC over the given attributes.
///
/// Implements the credential computation described in Chase-Perrin-Zaverucha section 3.1.
///
/// # Panics
/// if more than [`NUM_SUPPORTED_ATTRS`] attributes are passed in.
pub(crate) fn credential_core(&self, M: &[RistrettoPoint], sho: &mut dyn ShoApi) -> Credential {
assert!(
M.len() <= NUM_SUPPORTED_ATTRS,
"more than {NUM_SUPPORTED_ATTRS} attributes not supported"
);
let t = sho.get_scalar();
let U = sho.get_point();
let mut V = self.W + (self.x0 + self.x1 * t) * U;
for (yn, Mn) in self.y.iter().zip(M) {
V += yn * Mn;
}
Credential { t, U, V }
}
}
/// A public key used by the client to receive and verify credentials.
///
/// Defined in Chase-Perrin-Zaverucha section 3.1.
#[derive(Serialize, Deserialize, Clone, PartialDefault)]
pub struct CredentialPublicKey {
pub(crate) C_W: RistrettoPoint,
/// The value of `I` depends on the total number of attributes used.
///
/// In the original paper, `I` is computed over the maximum number of attributes only, but that
/// makes presentation proofs larger for credentials that don't use that many attributes. Here
/// we provide `I_n` for any supported number of attributes. We do skip `I_0`, since that would
/// be a credential with only public attributes, in which case you could just use a classic MAC.
I: [RistrettoPoint; NUM_SUPPORTED_ATTRS - 1],
}
impl CredentialPublicKey {
pub(crate) fn I(&self, num_attrs: usize) -> RistrettoPoint {
// `- 1` because we would normally want the third entry in the list for a three-attribute
// credential (the usual conversion from one-based counts to zero-based indexes).
// `- 1` again because we skip `I_0`; a one-attribute credential would only have public
// attributes.
self.I[num_attrs - 2]
}
}
impl<'a> From<&'a CredentialPrivateKey> for CredentialPublicKey {
fn from(private_key: &'a CredentialPrivateKey) -> Self {
let system = *SYSTEM_PARAMS;
let C_W = private_key.W + (private_key.wprime * system.G_wprime);
let mut I_i = system.G_V - (private_key.x0 * system.G_x0) - (private_key.x1 * system.G_x1);
let mut y_and_G_y_iter = private_key.y.iter().zip(system.G_y);
let (y0, G_y0) = y_and_G_y_iter.next().expect("correct number of parameters");
I_i -= y0 * G_y0;
let I = [(); NUM_SUPPORTED_ATTRS - 1].map(|_| {
let (yn, G_yn) = y_and_G_y_iter.next().expect("correct number of parameters");
I_i -= yn * G_yn;
I_i
});
debug_assert!(y_and_G_y_iter.next().is_none());
CredentialPublicKey { C_W, I }
}
}
/// A key pair used by the issuing server to sign credentials.
///
/// Defined in Chase-Perrin-Zaverucha section 3.1.
#[derive(Deserialize, Clone, PartialDefault)]
#[serde(from = "CredentialPrivateKey")]
pub struct CredentialKeyPair {
private_key: CredentialPrivateKey,
public_key: CredentialPublicKey,
}
impl CredentialKeyPair {
/// Generates a new key pair.
pub fn generate(randomness: [u8; RANDOMNESS_LEN]) -> Self {
CredentialPrivateKey::generate(randomness).into()
}
pub(crate) fn private_key(&self) -> &CredentialPrivateKey {
&self.private_key
}
/// Gets the public key.
pub fn public_key(&self) -> &CredentialPublicKey {
&self.public_key
}
}
impl From<CredentialPrivateKey> for CredentialKeyPair {
fn from(private_key: CredentialPrivateKey) -> Self {
let public_key = CredentialPublicKey::from(&private_key);
Self {
private_key,
public_key,
}
}
}
impl Serialize for CredentialKeyPair {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.private_key.serialize(serializer)
}
}
static SYSTEM_PARAMS: LazyLock<SystemParams> = LazyLock::new(SystemParams::generate);
pub(crate) const NUM_SUPPORTED_ATTRS: usize = 7; // 1 aggregate public, 3 two-point private
/// Parameters shared by the client and server.
///
/// User code never needs to explicitly reference these.
///
/// Defined in Chase-Perrin-Zaverucha section 3.1.
#[derive(Copy, Clone, Serialize, Deserialize)]
pub(crate) struct SystemParams {
pub(crate) G_w: RistrettoPoint,
pub(crate) G_wprime: RistrettoPoint,
pub(crate) G_x0: RistrettoPoint,
pub(crate) G_x1: RistrettoPoint,
pub(crate) G_V: RistrettoPoint,
pub(crate) G_z: RistrettoPoint,
pub(crate) G_y: [RistrettoPoint; NUM_SUPPORTED_ATTRS],
}
impl SystemParams {
/// An arbitrary set of independent points generated through a constant sequence of hash
/// operations.
fn generate() -> Self {
let mut sho = ShoSha256::new(b"Signal_ZKCredential_ConstantSystemParams_generate_20230410");
let G_w = sho.get_point();
let G_wprime = sho.get_point();
let G_x0 = sho.get_point();
let G_x1 = sho.get_point();
let G_V = sho.get_point();
let G_z = sho.get_point();
let G_y = [(); NUM_SUPPORTED_ATTRS].map(|_| sho.get_point());
SystemParams {
G_w,
G_wprime,
G_x0,
G_x1,
G_V,
G_z,
G_y,
}
}
pub fn get_hardcoded() -> SystemParams {
*SYSTEM_PARAMS
}
}
#[cfg(test)]
mod tests {
use const_str::hex;
use super::*;
impl SystemParams {
const SYSTEM_HARDCODED: &'static [u8] = &hex!(
"589c8718e8263a53a78932b6212a46e7fd52de3ad157b5bb277dba494cfd3471d4cc5f90685952917b33366efcce0512a1f8d70f974758266cb04fc424346d37b20f49cb2a081c94b1771fd8c172ae21785c61ea2c7e31947ce351e7b5ff07028c5329beb87b317ffcd981e440819d91136c988d6d9fbea4a87e55ed24a5993aa02f688ab1d3bd19056f94c8a44b8faddfa3c9c79c95ad44311a7bf00e5e862ec2c399f0d689dfb8c2dc0d7caba32afcf58cf0d85f78195a0b5ab732f565595492cfd982321d1f9be4b21fe6a0214306023d6a05d0d23f67ddc1c0400e5e0a5e92d17595131b7a095e740b884b8c9bb0226a39cfd027c769c4f4677c51f21b24da81fb2bd1356a9d0650f6a63fcc90d93bd74a954ba6f75f0e9fca47a6d21734bce7b28f06b76ef2c44d20a07026534e586eb8e1038874a93e44de362ce7bc0844bffc88e390c62519e281aa6fd53ff9ddd1d9ba303cf70004278ea2ae66ce05a2749d29eba56f3efe99e42902825c473dfc3c154c3762d2e76bd103f629d250b2d9d5c243a4cf8f3be21a84f153f44e2733a105cf780a20f03d84fe1ebbeb0e"
);
}
#[test]
fn test_system() {
let params = SystemParams::generate();
let serialized = bincode::serialize(&params).expect("can serialize");
println!("PARAMS = {serialized:#x?}");
assert!(serialized == SystemParams::SYSTEM_HARDCODED);
}
#[test]
fn round_trip_key_pair() {
let key_pair = CredentialKeyPair::generate([0x42; RANDOMNESS_LEN]);
let serialized = bincode::serialize(&key_pair).unwrap();
let deserialized: CredentialKeyPair = bincode::deserialize(&serialized).unwrap();
assert_eq!(&key_pair.public_key.C_W, &deserialized.public_key.C_W);
assert_eq!(&key_pair.private_key.w, &deserialized.private_key.w);
}
}
+285
View File
@@ -0,0 +1,285 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
//! The generation and verification of credential issuance proofs.
//!
//! When the issuing server issues a credential, it also generates a proof that the credential
//! covers the correct attributes. The client receives the proof and credential together, verifies
//! the proof, and extracts the credential. By providing the same attributes in the same order, the
//! generation and verification procedures have parallel invocations. The size of the proof scales
//! linearly with the number of attributes.
//!
//! Credential issuance is defined in Chase-Perrin-Zaverucha section 3.2.
pub mod blind;
use curve25519_dalek::ristretto::RistrettoPoint;
use curve25519_dalek::traits::Identity;
use partial_default::PartialDefault;
use poksho::shoapi::ShoApiExt as _;
use poksho::{ShoApi, ShoHmacSha256};
use serde::{Deserialize, Serialize};
use crate::attributes::{Attribute, PublicAttribute};
use crate::credentials::{
Credential, CredentialKeyPair, CredentialPublicKey, NUM_SUPPORTED_ATTRS, SystemParams,
};
use crate::sho::ShoExt;
use crate::{RANDOMNESS_LEN, VerificationFailure};
/// Contains a [`Credential`] along with a proof of its validity.
///
/// Use [`IssuanceProofBuilder`] to validate and extract the credential.
#[derive(Clone, Serialize, Deserialize, PartialDefault)]
pub struct IssuanceProof {
credential: Credential,
poksho_proof: Vec<u8>,
}
/// Used to generate and verify issuance proofs.
///
/// The same type is used for both generation and verification; the issuing server will end by
/// calling [`issue`](Self::issue) and the client by calling [`verify`](Self::verify).
pub struct IssuanceProofBuilder<'a> {
public_attrs: ShoHmacSha256,
/// Directly accessed by [`blind::BlindedIssuanceProofBuilder`].
attr_points: Vec<RistrettoPoint>,
authenticated_message: &'a [u8],
}
impl<'a> IssuanceProofBuilder<'a> {
/// Initializes a new proof builder.
///
/// `label` is a mandatory public attribute that should uniquely identify the credential.
pub fn new(label: &[u8]) -> Self {
Self::with_authenticated_message(label, &[])
}
/// Initializes the proof builder with a message that must match between the issuing server and
/// the client.
///
/// `label` is a mandatory public attribute that should uniquely identify the credential.
/// `message`, however, is not an attribute and will not be part of the resulting credential; it
/// is merely part of the proof. This could, for example, be used to distinguish multiple proofs
/// that produce the same kind of credential.
pub fn with_authenticated_message(label: &[u8], message: &'a [u8]) -> Self {
Self {
public_attrs: ShoHmacSha256::new(label),
// Reserve the first point for public attributes
attr_points: vec![RistrettoPoint::identity()],
authenticated_message: message,
}
}
/// Adds a public attribute to the credential.
///
/// This is order-sensitive.
pub fn add_public_attribute(mut self, attr: &dyn PublicAttribute) -> Self {
attr.hash_into(&mut self.public_attrs);
self.public_attrs.ratchet();
self
}
/// Adds an attribute to the credential.
///
/// This is order-sensitive.
pub fn add_attribute(mut self, attr: &dyn Attribute) -> Self {
self.attr_points.extend(attr.as_points());
assert!(
self.attr_points.len() <= NUM_SUPPORTED_ATTRS,
"more than {} hidden attribute points not supported",
NUM_SUPPORTED_ATTRS - 1
);
self
}
fn get_poksho_statement(&self) -> poksho::Statement {
// See Chase-Perrin-Zaverucha section 3.2.
let mut st = poksho::Statement::new();
st.add("C_W", &[("w", "G_w"), ("wprime", "G_wprime")]);
// G_V - I = x0 * G_x0 + x1 * G_x1 + sum(yi * G_yi, i = 0..n)
let G_V_minus_I_terms: [_; NUM_SUPPORTED_ATTRS + 2] = [
("x0", "G_x0"),
("x1", "G_x1"),
("y0", "G_y0"),
("y1", "G_y1"),
("y2", "G_y2"),
("y3", "G_y3"),
("y4", "G_y4"),
("y5", "G_y5"),
("y6", "G_y6"),
];
st.add("G_V-I", &G_V_minus_I_terms[..2 + self.attr_points.len()]);
// V = w * G_w + x0 * U + x1 * tU + sum(yi * Mi, i = 0..n)
let V_terms: [_; NUM_SUPPORTED_ATTRS + 3] = [
("w", "G_w"),
("x0", "U"),
("x1", "tU"),
("y0", "M0"),
("y1", "M1"),
("y2", "M2"),
("y3", "M3"),
("y4", "M4"),
("y5", "M5"),
("y6", "M6"),
];
st.add("V", &V_terms[..3 + self.attr_points.len()]);
st
}
fn finalize_public_attrs(&mut self) {
debug_assert!(self.attr_points[0] == RistrettoPoint::identity());
self.attr_points[0] = self.public_attrs.get_point();
}
/// Generates a [`poksho::PointArgs`] to be used in the final proof.
///
/// `total_attr_count` is passed in for [blind issuance](blind::BlindedIssuanceProofBuilder), in
/// which case the caller may provide additional attributes.
fn prepare_scalar_args(
&self,
key_pair: &CredentialKeyPair,
total_attr_count: usize,
) -> poksho::ScalarArgs {
assert!(
total_attr_count <= NUM_SUPPORTED_ATTRS,
"should have been enforced by the caller"
);
let mut scalar_args = poksho::ScalarArgs::new();
scalar_args.add("w", key_pair.private_key().w);
scalar_args.add("wprime", key_pair.private_key().wprime);
scalar_args.add("x0", key_pair.private_key().x0);
scalar_args.add("x1", key_pair.private_key().x1);
let y_names: [_; NUM_SUPPORTED_ATTRS] = ["y0", "y1", "y2", "y3", "y4", "y5", "y6"];
for (name, value) in y_names
.into_iter()
.take(total_attr_count)
.zip(key_pair.private_key().y.iter())
{
scalar_args.add(name, *value);
}
scalar_args
}
/// Generates a [`poksho::PointArgs`] to be used in the final proof.
///
/// The `credential` argument may be `None` when used for [blind
/// issuance](blind::BlindedIssuanceProofBuilder), in which case the caller is responsible for
/// adding its own points representing the credential.
fn prepare_point_args(
&self,
key: &CredentialPublicKey,
total_attr_count: usize,
credential: Option<&Credential>,
) -> poksho::PointArgs {
let system = SystemParams::get_hardcoded();
assert!(
total_attr_count <= NUM_SUPPORTED_ATTRS,
"should have been enforced by the caller"
);
let mut point_args = poksho::PointArgs::new();
point_args.add("C_W", key.C_W);
point_args.add("G_w", system.G_w);
point_args.add("G_wprime", system.G_wprime);
point_args.add("G_V-I", system.G_V - key.I(total_attr_count));
point_args.add("G_x0", system.G_x0);
point_args.add("G_x1", system.G_x1);
let G_y_names: [_; NUM_SUPPORTED_ATTRS] =
["G_y0", "G_y1", "G_y2", "G_y3", "G_y4", "G_y5", "G_y6"];
for (name, value) in G_y_names
.into_iter()
.take(total_attr_count)
.zip(system.G_y.iter())
{
point_args.add(name, *value);
}
if let Some(credential) = credential {
point_args.add("V", credential.V);
point_args.add("U", credential.U);
point_args.add("tU", credential.t * credential.U);
}
let M_names: [_; NUM_SUPPORTED_ATTRS] = ["M0", "M1", "M2", "M3", "M4", "M5", "M6"];
for (name, value) in M_names.into_iter().zip(&self.attr_points) {
point_args.add(name, *value);
}
point_args
}
/// Issues a new credential over the accumulated attributes using the given `key_pair`.
///
/// `randomness` ensures several important properties:
/// - The generated credential is randomized (non-deterministic).
/// - The issuance proof uses a random nonce.
///
/// It is critical that different randomness is used each time a credential is issued. Failing
/// to do so effectively reveals the server's private key.
pub fn issue(
mut self,
key_pair: &CredentialKeyPair,
randomness: [u8; RANDOMNESS_LEN],
) -> IssuanceProof {
self.finalize_public_attrs();
let mut sho = ShoHmacSha256::new(b"Signal_ZKCredential_Issuance_20230410");
sho.absorb_and_ratchet(&randomness);
let credential = key_pair
.private_key()
.credential_core(&self.attr_points, &mut sho);
let scalar_args = self.prepare_scalar_args(key_pair, self.attr_points.len());
let point_args = self.prepare_point_args(
key_pair.public_key(),
self.attr_points.len(),
Some(&credential),
);
let poksho_proof = self
.get_poksho_statement()
.prove(
&scalar_args,
&point_args,
self.authenticated_message,
&sho.squeeze_and_ratchet_as_array::<RANDOMNESS_LEN>(),
)
.expect("valid proof");
IssuanceProof {
poksho_proof,
credential,
}
}
/// Verifies the given `proof` over the accrued attributes using the given `public_key`.
///
/// On successful verification, returns the [`Credential`] that was just proven valid.
pub fn verify(
mut self,
public_key: &CredentialPublicKey,
// Even though it would work with a borrow, this deliberately consumes
// IssuanceProof to indicate that you should not keep it around after
// you have extracted the credential.
proof: IssuanceProof,
) -> Result<Credential, VerificationFailure> {
self.finalize_public_attrs();
let point_args =
self.prepare_point_args(public_key, self.attr_points.len(), Some(&proof.credential));
match self.get_poksho_statement().verify_proof(
&proof.poksho_proof,
&point_args,
self.authenticated_message,
) {
Err(_) => Err(VerificationFailure),
Ok(_) => Ok(proof.credential),
}
}
}
+86
View File
@@ -0,0 +1,86 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
//! Used to build custom _attribute-based anonymous credentials_ ("ABCs") and their associated
//! proofs.
//!
//! This crate's cryptographic abstractions are largely built around the idea of a _client,_ an
//! _issuing server,_ and a _verifying server._ The client sends a _credential request_ to the
//! issuing server, which returns a _credential_ in a _response._ The client validates the
//! credential and checks that the server has not fingerprinted it in any way, then generates a
//! _presentation_ for the credential and presents that to the verifying server when making a
//! request (to perform some operation). The verifying server validates that presentation and
//! performs that operation.
//!
//! Presentations should never be reused for multiple operations; that would allow the verifying
//! server to identify that the same user is responsible. Instead, a new presentation should be
//! generated from a cached credential for each operation.
//!
//! What's in a credential? It's essentially a MAC over several _attributes,_ such as the client's
//! account identifier. What's important is that the attributes in a credential support homomorphic
//! encryption, allowing the verifying server to validate attributes without the client revealing
//! them. A credential can even be issued over a _blinded attribute_ hidden from the issuing server,
//! matching whatever value the client has _committed_ to.
//!
//! In this model, the issuing and verifying servers share their private keys, but may otherwise be
//! independent; for Signal, the _issuing server_ is usually the main chat server (which knows who
//! the client is), and the _verifying server_ is the "storage service" where groups are managed
//! (which must not). However, it would be valid to have the same server perform both operations, as
//! long as the second connection can't be correlated with the first.
//!
//! This model is based on "[The Signal Private Group System and Anonymous Credentials Supporting
//! Efficient Verifiable Encryption][paper]", by Chase, Perrin, and Zaverucha.
//!
//! [paper]: https://eprint.iacr.org/2019/1416
#![allow(non_snake_case)]
#![warn(missing_docs, clippy::unwrap_used)]
/// A zkcredential operation failed to verify.
#[derive(Debug, thiserror::Error, displaydoc::Display)]
pub struct VerificationFailure;
/// A reasonable size of entropy to request for operations.
///
/// zkcredential uses explicit arrays of randomness rather than taking random number generators as
/// arguments because it makes it easier to write expected-output tests in the languages libsignal
/// is bridged to, which can't easily substitute a custom Rng.
pub const RANDOMNESS_LEN: usize = 32;
pub mod attributes;
pub mod credentials;
pub mod endorsements;
pub mod issuance;
pub mod presentation;
pub mod sho;
/// Helper type for implementing [`std::fmt::Debug`].
///
/// The `Debug::fmt` implementation for this type prints the wrapped value as
/// hex bytes.
pub struct PrintAsHex<T>(pub T);
impl std::fmt::Debug for PrintAsHex<&[u8]> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for b in self.0 {
write!(f, "{b:0x}")?
}
Ok(())
}
}
impl std::fmt::Debug for PrintAsHex<&curve25519_dalek::ristretto::CompressedRistretto> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
PrintAsHex(self.0.as_bytes().as_slice()).fmt(f)
}
}
impl std::fmt::Debug for PrintAsHex<&[curve25519_dalek::ristretto::CompressedRistretto]> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_list()
.entries(self.0.iter().map(PrintAsHex))
.finish()
}
}
+711
View File
@@ -0,0 +1,711 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
//! The generation and verification of credential presentation proofs
//!
//! When the client wishes to use a credential, it generates a _presentation proof_ over the same
//! attributes that went into the original credential. This allows the client to demonstrate that
//! they hold a credential over certain attributes without actually revealing those attributes. The
//! verifying server will verify the proof against the encrypted forms of those attributes and is
//! thus assured that the client does hold a credential from the issuing server.
//!
//! By providing the same attributes in the same order, a proof can be generated and verified with
//! parallel invocations. The size of the proof scales linearly with the number of attributes.
//!
//! It is recommended that the client generate a new presentation for every use of their private
//! credential, so that the verifying server cannot track repeated uses of the same presentation. Of
//! course, the encrypted forms of the attributes might also allow the verifying server to correlate
//! requests over time.
//!
//! Credential presentation is defined in Chase-Perrin-Zaverucha section 3.2; proofs for verifiable
//! encryption are defined in section 4.1.
use curve25519_dalek::Scalar;
use curve25519_dalek::ristretto::RistrettoPoint;
use curve25519_dalek::traits::Identity;
use partial_default::PartialDefault;
use poksho::shoapi::ShoApiExt as _;
use poksho::{ShoApi, ShoHmacSha256};
use serde::{Deserialize, Serialize};
use crate::attributes::{self, Attribute, PublicAttribute, RevealedAttribute};
use crate::credentials::{
Credential, CredentialKeyPair, CredentialPrivateKey, CredentialPublicKey, NUM_SUPPORTED_ATTRS,
SystemParams,
};
use crate::sho::ShoExt;
use crate::{RANDOMNESS_LEN, VerificationFailure};
#[derive(Clone, Serialize, Deserialize, PartialDefault)]
struct PresentationProofCommitments {
C_x0: RistrettoPoint,
C_x1: RistrettoPoint,
C_V: RistrettoPoint,
C_y: Vec<RistrettoPoint>,
}
/// Demonstrates to the _verifying server_ that the client holds a particular credential.
///
/// Use [`PresentationProofVerifier`] to validate the proof.
#[derive(Clone, Serialize, Deserialize, PartialDefault)]
pub struct PresentationProof {
commitments: PresentationProofCommitments,
poksho_proof: Vec<u8>,
}
struct AttributeRef {
key_index: Option<usize>,
first_point_index: usize,
second_point_index: usize,
}
/// A type-erased version of [`attributes::PublicKey`], to store heterogeneously in a
/// [`PresentationProofBuilderCore`].
struct AnyPublicKey {
id: &'static str,
G_a: fn() -> [RistrettoPoint; 2],
A: RistrettoPoint,
}
impl<D: attributes::Domain> From<attributes::PublicKey<D>> for AnyPublicKey {
fn from(value: attributes::PublicKey<D>) -> Self {
Self {
id: D::ID,
G_a: D::G_a,
A: value.A,
}
}
}
enum PublicKeyOrId {
PublicKey(AnyPublicKey),
Id(&'static str),
}
trait MayHavePublicKey {
fn id(&self) -> &'static str;
fn public_key(&self) -> Option<&AnyPublicKey>;
}
impl MayHavePublicKey for PublicKeyOrId {
fn id(&self) -> &'static str {
match self {
PublicKeyOrId::PublicKey(key) => key.id,
PublicKeyOrId::Id(id) => id,
}
}
fn public_key(&self) -> Option<&AnyPublicKey> {
if let PublicKeyOrId::PublicKey(public_key) = &self {
Some(public_key)
} else {
None
}
}
}
/// A type-erased version of [`attributes::KeyPair`], to store heterogeneously in a
/// [`PresentationProofBuilderCore`].
struct AnyKeyPair {
a1: Scalar,
a2: Scalar,
public_key_or_id: PublicKeyOrId,
}
impl AnyKeyPair {
fn without_public_key(self) -> Self {
Self {
public_key_or_id: PublicKeyOrId::Id(self.public_key_or_id.id()),
..self
}
}
}
impl<D: attributes::Domain> From<attributes::KeyPair<D>> for AnyKeyPair {
fn from(value: attributes::KeyPair<D>) -> Self {
Self {
a1: value.a1,
a2: value.a2,
public_key_or_id: PublicKeyOrId::PublicKey(value.public_key.into()),
}
}
}
impl MayHavePublicKey for AnyKeyPair {
fn id(&self) -> &'static str {
self.public_key_or_id.id()
}
fn public_key(&self) -> Option<&AnyPublicKey> {
self.public_key_or_id.public_key()
}
}
struct PresentationProofBuilderCore<'a, T: MayHavePublicKey> {
encryption_keys: Vec<T>,
attributes: Vec<AttributeRef>,
attr_points: Vec<RistrettoPoint>,
authenticated_message: &'a [u8],
}
/// Used to generate presentation proofs.
///
/// Public attributes are not included from the presentation proof; when the proof is verified, the
/// verifying server will provide its own copy of the public attributes to ensure that they haven't
/// been tampered with.
///
/// See also [`PresentationProofVerifier`].
pub struct PresentationProofBuilder<'a> {
core: PresentationProofBuilderCore<'a, AnyKeyPair>,
}
/// Used to verify presentation proofs.
///
/// By providing the same attributes in the same order, a proof can be generated and verified with
/// parallel invocations. The size of the proof scales linearly with the number of attributes.
///
/// Public attributes are not included from the presentation proof; when the proof is verified, the
/// verifying server will provide its own copy of the public attributes to ensure that they haven't
/// been tampered with, as mentioned in Chase-Perrin-Zaverucha section 3.2.
///
/// See also [`PresentationProofBuilder`].
pub struct PresentationProofVerifier<'a> {
core: PresentationProofBuilderCore<'a, PublicKeyOrId>,
public_attrs: ShoHmacSha256,
}
impl<'a, T: MayHavePublicKey> PresentationProofBuilderCore<'a, T> {
fn with_authenticated_message(message: &'a [u8]) -> Self {
Self {
encryption_keys: vec![],
attributes: vec![],
// Reserve the first point for public attributes
attr_points: vec![RistrettoPoint::identity()],
authenticated_message: message,
}
}
fn add_attribute(&mut self, attr_points: &[RistrettoPoint], key: Option<T>) {
let first_index = self.attr_points.len();
self.attr_points.extend(attr_points);
assert!(
self.attr_points.len() <= NUM_SUPPORTED_ATTRS,
"more than {} hidden attribute points not supported",
NUM_SUPPORTED_ATTRS - 1
);
let key_index = key.map(|key| {
let key_id = key.id();
match self
.encryption_keys
.iter()
.position(|key| key.id() == key_id)
{
Some(idx) => idx,
None => {
let idx = self.encryption_keys.len();
self.encryption_keys.push(key);
idx
}
}
});
// If we ever support attributes longer than two points we'll have to change this.
self.attributes.push(AttributeRef {
key_index,
first_point_index: first_index,
second_point_index: first_index + attr_points.len() - 1,
});
}
fn get_poksho_statement(&self) -> poksho::Statement {
let mut st = poksho::Statement::new();
// These terms are from Chase-Perrin-Zaverucha section 3.2.
st.add("Z", &[("z", "I")]);
st.add("C_x1", &[("t", "C_x0"), ("z0", "G_x0"), ("z", "G_x1")]);
// These terms are from Chase-Perrin-Zaverucha section 4.1,
// proving the validity of the encryption keys.
let mut encryption_sum_terms = vec![];
for key in &self.encryption_keys {
let key_id = key.id();
let a1 = format!("a1_{key_id}");
// These terms are an addition by Trevor Perrin to the original paper to more carefully
// ensure the validity of the encryption keys used.
// 0 = z1_uid * I + a1_uid * Z
st.add("0", &[(&format!("z1_{key_id}"), "I"), (&a1, "Z")]);
if key.public_key().is_some() {
encryption_sum_terms.push((a1, format!("G_a1_{key_id}")));
encryption_sum_terms.push((format!("a2_{key_id}"), format!("G_a2_{key_id}")));
}
}
if !encryption_sum_terms.is_empty() {
// sum(A) = (a1_uid * G_a1_uid) + (a2_uid * G_a2_uid) +
// (a1_profilekey * G_a1_profilekey) + (a2_profilekey * G_a2_profilekey) +
// ...
st.add(
"sum(A)",
&encryption_sum_terms
.iter()
.map(|(a, b)| (a.as_str(), b.as_str()))
.collect::<Vec<_>>(),
);
}
for attr in &self.attributes {
if let Some(key_index) = attr.key_index {
// If this attribute uses a key, it's a verifiably encrypted Attribute.
// These terms are from Chase-Perrin-Zaverucha section 4.1,
// proving that the ciphertext matches the attribute in the credential.
let key_id = self.encryption_keys[key_index].id();
// E_A1 = a1_uid * C_y1 + z1_uid * G_y1
st.add(
&format!("E_A{}", attr.first_point_index),
&[
(
&format!("a1_{key_id}"),
&format!("C_y{}", attr.first_point_index),
),
(
&format!("z1_{key_id}"),
&format!("G_y{}", attr.first_point_index),
),
],
);
// C_y2 - E_A2 = z * G_y2 + a2_uid * -E_A1
st.add(
&format!("C_y{0}-E_A{0}", attr.second_point_index),
&[
("z", &format!("G_y{}", attr.second_point_index)),
(
&format!("a2_{key_id}"),
&format!("-E_A{}", attr.first_point_index),
),
],
);
} else {
// If the attribute does not use a key, it's a RevealedAttribute.
// (We don't currently support hidden scalar attributes.)
// This is from section 3.2 again; C_y1 is otherwise unbound.
debug_assert_eq!(attr.first_point_index, attr.second_point_index);
// C_y1 = z * G_y1
st.add(
&format!("C_y{}", attr.first_point_index),
&[("z", &format!("G_y{}", attr.first_point_index))],
);
}
}
// Point 0 is a hardcoded public attribute.
st.add("C_y0", &[("z", "G_y0")]);
st
}
/// Generates [`poksho::PointArgs`] containing all points not derived from attributes.
///
/// This includes the credential key commitments `C_x0`, `C_x1`, and `C_y0`; the system points
/// `G_x0`, `G_x1`, and all `G_y{i}`; the appropriate issuing parameter point `I`; and the
/// points necessary to prove the validity of encryption keys: `0`, `G_a1_{key}`, `G_a2_{key}`,
/// and `sum(A)`.
///
/// The caller is responsible for handling the presenter's one-off public point `Z` (which the
/// verifier derives from the commitments and public attributes); the appropriate `C_y{i}` for
/// all attributes besides public attributes (depending on whether or not attributes are
/// encrypted); and the encryption-specific points `E_A{i}`, `-E_A{i}`, and `C_y{j}-E_A{j}`.
fn prepare_non_attribute_point_args(
&self,
I: RistrettoPoint,
commitments: &PresentationProofCommitments,
) -> poksho::PointArgs {
let credentials_system = SystemParams::get_hardcoded();
let mut point_args = poksho::PointArgs::new();
point_args.add("I", I);
point_args.add("C_x0", commitments.C_x0);
point_args.add("C_x1", commitments.C_x1);
point_args.add("G_x0", credentials_system.G_x0);
point_args.add("G_x1", credentials_system.G_x1);
if !self.encryption_keys.is_empty() {
point_args.add("0", RistrettoPoint::identity());
let mut sum_A = RistrettoPoint::identity();
for key in &self.encryption_keys {
if let Some(key) = key.public_key() {
let [G_a1, G_a2] = (key.G_a)();
point_args.add(format!("G_a1_{}", key.id), G_a1);
point_args.add(format!("G_a2_{}", key.id), G_a2);
sum_A += key.A;
}
}
if sum_A != RistrettoPoint::identity() {
point_args.add("sum(A)", sum_A);
}
}
let G_y_names: [_; NUM_SUPPORTED_ATTRS] =
["G_y0", "G_y1", "G_y2", "G_y3", "G_y4", "G_y5", "G_y6"];
for (G_y_name, G_yn) in G_y_names
.into_iter()
.take(self.attr_points.len())
.zip(credentials_system.G_y)
{
point_args.add(G_y_name, G_yn)
}
point_args.add("C_y0", commitments.C_y[0]);
// Other C_y depend on the form of the attribute.
point_args
}
}
impl<'a> PresentationProofBuilder<'a> {
/// Initializes a new proof builder.
///
/// `label` is a mandatory public attribute that should uniquely identify the credential, but as
/// a public attribute it is ignored. It is merely here for symmetry with
/// [`PresentationProofVerifier::new`].
pub fn new(label: &[u8]) -> Self {
Self::with_authenticated_message(label, &[])
}
/// Initializes a new proof builder.
///
/// `label` is a mandatory public attribute that should uniquely identify the credential, but as
/// a public attribute it is ignored. It is merely here for symmetry with
/// [`PresentationProofVerifier::with_authenticated_message`].
///
/// `message`, however, is not an attribute and is not part of the original credential; it is
/// merely part of the proof. This could, for example, be used to distinguish multiple proofs
/// that present the same kind of credential.
pub fn with_authenticated_message(label: &[u8], message: &'a [u8]) -> Self {
_ = label;
Self {
core: PresentationProofBuilderCore::with_authenticated_message(message),
}
}
/// Unnecessary: public attributes are passed directly to the verifying server.
#[deprecated = "Unnecessary: public attributes are passed directly to the verifying server."]
pub fn add_public_attribute(self, attr: &dyn PublicAttribute) -> Self {
_ = attr;
self
}
/// Adds an attribute to the proof, which will be encrypted using `key`.
///
/// This is order-sensitive.
pub fn add_attribute(
mut self,
attr: &dyn Attribute,
key: &attributes::KeyPair<impl attributes::Domain>,
) -> Self {
self.core
.add_attribute(&attr.as_points(), Some(AnyKeyPair::from(*key)));
self
}
/// Adds an attribute to the proof, which will be encrypted using `key`.
///
/// This still includes a proof that the attribute was correctly encrypted; however, the
/// verifying server will not be able to check which key performed that encryption.
///
/// This is order-sensitive.
pub fn add_attribute_without_verified_key(
mut self,
attr: &dyn Attribute,
key: &attributes::KeyPair<impl attributes::Domain>,
) -> Self {
self.core.add_attribute(
&attr.as_points(),
Some(AnyKeyPair::from(*key).without_public_key()),
);
self
}
/// Adds an attribute to check against the credential.
///
/// In practice `attr` is ignored in favor of letting the verifying server check the attribute
/// itself, but it's still necessary to call this method to indicate that there *is* an
/// attribute.
///
/// This is order-sensitive.
pub fn add_revealed_attribute(mut self, attr: &dyn RevealedAttribute) -> Self {
// We don't actually need the value! The server will check it for us.
_ = attr;
self.core.add_attribute(&[RistrettoPoint::identity()], None);
self
}
/// Generates the presentation of `credential` using the server-provided `public_key`.
///
/// Note that this does not consume `credential`; indeed, it is recommended to use a new
/// presentation every time you want to use a particular credential.
///
/// `randomness` ensures several important properties:
/// - The generated presentation is randomized (non-deterministic).
/// - The presentation proof uses a random nonce.
///
/// It is critical that different randomness is used each time a credential is issued. Failing
/// to do so allows different presentations to be linked to the same credential (and thus the
/// same user), and worse, effectively reveals any hidden Attributes and their encryption keys.
pub fn present(
self,
public_key: &CredentialPublicKey,
credential: &Credential,
randomness: [u8; RANDOMNESS_LEN],
) -> PresentationProof {
let credentials_system = SystemParams::get_hardcoded();
let mut sho = ShoHmacSha256::new(b"Signal_ZKCredential_Presentation_20230410");
sho.absorb_and_ratchet(&randomness);
let z = sho.get_scalar();
debug_assert!(
self.core.attr_points[0] == RistrettoPoint::identity(),
"public attributes are incorporated by the server"
);
// Note that Mn will be the identity element for both the first point and for any
// RevealedAttributes, so this will simply produce `z * G_yn` for those elements as in
// Chase-Perrin-Zaverucha section 3.2.
let C_y = credentials_system
.G_y
.iter()
.zip(&self.core.attr_points)
.map(|(G_yn, Mn)| z * G_yn + Mn)
.collect::<Vec<_>>();
let C_x0 = z * credentials_system.G_x0 + credential.U;
let C_V = z * credentials_system.G_V + credential.V;
let C_x1 = z * credentials_system.G_x1 + credential.t * credential.U;
let commitments = PresentationProofCommitments {
C_x0,
C_x1,
C_V,
C_y,
};
let z0 = -z * credential.t;
let I = public_key.I(self.core.attr_points.len());
let Z = z * I;
let mut scalar_args = poksho::ScalarArgs::new();
scalar_args.add("z", z);
scalar_args.add("t", credential.t);
scalar_args.add("z0", z0);
for key in &self.core.encryption_keys {
let key_id = key.id();
scalar_args.add(format!("a1_{key_id}"), key.a1);
scalar_args.add(format!("a2_{key_id}"), key.a2);
scalar_args.add(format!("z1_{key_id}"), -z * key.a1);
}
let mut point_args = self.core.prepare_non_attribute_point_args(I, &commitments);
point_args.add("Z", Z);
for attr in &self.core.attributes {
let &AttributeRef {
key_index,
first_point_index,
second_point_index,
} = attr;
point_args.add(
format!("C_y{first_point_index}"),
commitments.C_y[first_point_index],
);
if let Some(key_index) = key_index {
let key = &self.core.encryption_keys[key_index];
let E_A1 = key.a1 * self.core.attr_points[first_point_index];
let E_A2 = key.a2 * E_A1 + self.core.attr_points[second_point_index];
point_args.add(format!("E_A{first_point_index}"), E_A1);
point_args.add(format!("-E_A{first_point_index}"), -E_A1);
point_args.add(
format!("C_y{second_point_index}-E_A{second_point_index}"),
commitments.C_y[second_point_index] - E_A2,
);
} else {
debug_assert!(
self.core.attr_points[first_point_index] == RistrettoPoint::identity(),
"revealed attributes are incorporated by the server"
);
}
}
let poksho_proof = self
.core
.get_poksho_statement()
.prove(
&scalar_args,
&point_args,
self.core.authenticated_message,
&sho.squeeze_and_ratchet_as_array::<RANDOMNESS_LEN>(),
)
.expect("valid proof");
PresentationProof {
commitments,
poksho_proof,
}
}
}
impl<'a> PresentationProofVerifier<'a> {
/// Initializes a new proof verifier.
///
/// `label` is a mandatory public attribute that should uniquely identify the credential.
pub fn new(label: &[u8]) -> Self {
Self::with_authenticated_message(label, &[])
}
/// Initializes a new proof verifier.
///
/// `label` is a mandatory public attribute that should uniquely identify the credential.
/// `message`, however, is not an attribute and is not part of the original credential; it is
/// merely part of the proof. This could, for example, be used to distinguish multiple proofs
/// that present the same kind of credential.
pub fn with_authenticated_message(label: &[u8], message: &'a [u8]) -> Self {
Self {
core: PresentationProofBuilderCore::with_authenticated_message(message),
public_attrs: ShoHmacSha256::new(label),
}
}
/// Adds a public attribute to check against the credential.
///
/// This is order-sensitive.
pub fn add_public_attribute(mut self, attr: &dyn PublicAttribute) -> Self {
attr.hash_into(&mut self.public_attrs);
self.public_attrs.ratchet();
self
}
/// Adds an encrypted attribute to check against the credential, along with the public key for
/// the key it was encrypted with.
///
/// This is order-sensitive.
pub fn add_attribute(
mut self,
attr: &dyn Attribute,
key: &attributes::PublicKey<impl attributes::Domain>,
) -> Self {
self.core.add_attribute(
&attr.as_points(),
Some(PublicKeyOrId::PublicKey(AnyPublicKey::from(*key))),
);
self
}
/// Adds an encrypted attribute to check against the credential, omitting the key it was
/// encrypted with.
///
/// This still checks that the attribute was correctly encrypted; it just can't enforce which
/// key did so.
///
/// This is order-sensitive.
pub fn add_attribute_without_verified_key(
mut self,
attr: &dyn Attribute,
key_id: &'static str,
) -> Self {
self.core
.add_attribute(&attr.as_points(), Some(PublicKeyOrId::Id(key_id)));
self
}
/// Adds an attribute to check against the credential, unencrypted.
///
/// This should only be used when the attribute is blinded from the issuing server, but visible
/// to the verifying server. Use public attributes when the value doesn't need to be hidden at
/// all.
///
/// This is order-sensitive.
pub fn add_revealed_attribute(mut self, attr: &dyn RevealedAttribute) -> Self {
self.core.add_attribute(&[attr.as_point()], None);
self
}
fn finalize_public_attrs(&mut self) {
debug_assert!(self.core.attr_points[0] == RistrettoPoint::identity());
self.core.attr_points[0] = self.public_attrs.get_point();
}
/// Verifies the given `proof` over the accrued attributes using the given `key_pair`.
pub fn verify(
mut self,
key_pair: &CredentialKeyPair,
proof: &PresentationProof,
) -> Result<(), VerificationFailure> {
self.finalize_public_attrs();
let PresentationProofCommitments {
C_x0,
C_x1,
C_V,
C_y,
} = &proof.commitments;
if C_y.len() != self.core.attr_points.len() {
return Err(VerificationFailure);
}
let CredentialPrivateKey { W, x0, x1, y, .. } = key_pair.private_key();
let mut Z = C_V - W - x0 * C_x0 - x1 * C_x1;
for (yn, C_yn) in y.iter().zip(C_y.iter()) {
Z -= yn * C_yn;
}
// Incorporate public attributes here so the server can check they haven't changed.
Z -= y[0] * self.core.attr_points[0];
let public_key = key_pair.public_key();
let I = public_key.I(self.core.attr_points.len());
let mut point_args = self
.core
.prepare_non_attribute_point_args(I, &proof.commitments);
for attr in &self.core.attributes {
let &AttributeRef {
first_point_index,
second_point_index,
key_index,
} = attr;
point_args.add(format!("C_y{first_point_index}"), C_y[first_point_index]);
if key_index.is_some() {
point_args.add(
format!("E_A{first_point_index}"),
self.core.attr_points[first_point_index],
);
point_args.add(
format!("-E_A{first_point_index}"),
-self.core.attr_points[first_point_index],
);
point_args.add(
format!("C_y{second_point_index}-E_A{second_point_index}"),
C_y[second_point_index] - self.core.attr_points[second_point_index],
);
} else {
// Check that the revealed attributes match the original issuance.
Z -= y[first_point_index] * self.core.attr_points[first_point_index];
}
}
point_args.add("Z", Z);
match self.core.get_poksho_statement().verify_proof(
&proof.poksho_proof,
&point_args,
self.core.authenticated_message,
) {
Err(_) => Err(VerificationFailure),
Ok(_) => Ok(()),
}
}
}
+29
View File
@@ -0,0 +1,29 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
//! Additional utilities for poksho's [`ShoApi`] types.
use curve25519_dalek::ristretto::RistrettoPoint;
use curve25519_dalek::scalar::Scalar;
use poksho::ShoApi;
/// Extends [`ShoApi`] with convenience methods for generating Ristretto group elements.
pub trait ShoExt: ShoApi {
/// Uses [`ShoApi::squeeze_and_ratchet_into`] to generate a pseudorandom point.
fn get_point(&mut self) -> RistrettoPoint {
let mut point_bytes = [0u8; 64];
self.squeeze_and_ratchet_into(&mut point_bytes);
RistrettoPoint::from_uniform_bytes(&point_bytes)
}
/// Uses [`ShoApi::squeeze_and_ratchet_into`] to generate a pseudorandom scalar.
fn get_scalar(&mut self) -> Scalar {
let mut scalar_bytes = [0u8; 64];
self.squeeze_and_ratchet_into(&mut scalar_bytes);
Scalar::from_bytes_mod_order_wide(&scalar_bytes)
}
}
impl<T: ShoApi + ?Sized> ShoExt for T {}
@@ -0,0 +1,73 @@
use partial_default::PartialDefault;
use serde::{Serialize, Serializer};
use crate::api;
use crate::auth::AuthCredentialWithPniZkcPresentation;
use crate::common::constants::*;
use crate::common::errors::*;
use crate::common::simple_types::*;
#[derive(derive_more::From)]
pub enum AnyAuthCredentialPresentation {
V4(AuthCredentialWithPniZkcPresentation),
}
#[repr(u8)]
#[derive(Copy, Clone, Debug, PartialDefault, derive_more::TryFrom)]
#[try_from(repr)]
enum PresentationVersion {
// V1-V3 are no longer supported.
#[partial_default]
V4 = PRESENTATION_VERSION_4,
}
impl From<PresentationVersion> for u8 {
fn from(value: PresentationVersion) -> Self {
value as u8
}
}
impl AnyAuthCredentialPresentation {
pub fn new(presentation_bytes: &[u8]) -> Result<Self, ZkGroupDeserializationFailure> {
let first = *presentation_bytes
.first()
.ok_or(ZkGroupDeserializationFailure::new::<Self>())?;
let version = PresentationVersion::try_from(first)
.map_err(|_| ZkGroupDeserializationFailure::new::<Self>())?;
match version {
PresentationVersion::V4 => Ok(crate::deserialize::<
AuthCredentialWithPniZkcPresentation,
>(presentation_bytes)?
.into()),
}
}
pub fn get_aci_ciphertext(&self) -> api::groups::UuidCiphertext {
match self {
AnyAuthCredentialPresentation::V4(presentation) => presentation.aci_ciphertext(),
}
}
pub fn get_pni_ciphertext(&self) -> api::groups::UuidCiphertext {
match self {
AnyAuthCredentialPresentation::V4(presentation) => presentation.pni_ciphertext(),
}
}
pub fn get_redemption_time(&self) -> Timestamp {
match self {
AnyAuthCredentialPresentation::V4(presentation) => presentation.redemption_time(),
}
}
}
impl Serialize for AnyAuthCredentialPresentation {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
AnyAuthCredentialPresentation::V4(presentation) => presentation.serialize(serializer),
}
}
}
@@ -0,0 +1,121 @@
//
// Copyright 2022 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use libsignal_core::{Aci, Pni};
use partial_default::PartialDefault;
use serde::Serialize;
use crate::auth::AnyAuthCredentialPresentation;
use crate::groups::GroupSecretParams;
use crate::{
RandomnessBytes, ServerPublicParams, ZkGroupDeserializationFailure, ZkGroupVerificationFailure,
};
mod zkc;
pub use zkc::{
AuthCredentialWithPniZkc, AuthCredentialWithPniZkcPresentation,
AuthCredentialWithPniZkcResponse,
};
#[derive(Clone, PartialDefault, derive_more::From)]
pub enum AuthCredentialWithPni {
#[partial_default]
Zkc(AuthCredentialWithPniZkc),
}
#[derive(Clone, PartialDefault, derive_more::From)]
pub enum AuthCredentialWithPniResponse {
#[partial_default]
Zkc(AuthCredentialWithPniZkcResponse),
}
#[repr(u8)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialDefault, derive_more::TryFrom)]
#[try_from(repr)]
pub enum AuthCredentialWithPniVersion {
#[partial_default]
Zkc = 3,
}
impl AuthCredentialWithPni {
pub fn new(bytes: &[u8]) -> Result<Self, ZkGroupDeserializationFailure> {
let first = bytes
.first()
.ok_or_else(ZkGroupDeserializationFailure::new::<Self>)?;
let version = AuthCredentialWithPniVersion::try_from(*first)
.map_err(|_| ZkGroupDeserializationFailure::new::<Self>())?;
match version {
AuthCredentialWithPniVersion::Zkc => {
crate::common::serialization::deserialize(bytes).map(Self::Zkc)
}
}
}
pub fn present(
&self,
public_params: &ServerPublicParams,
group_secret_params: &GroupSecretParams,
randomness: RandomnessBytes,
) -> AnyAuthCredentialPresentation {
match self {
Self::Zkc(credential) => AnyAuthCredentialPresentation::V4(credential.present(
public_params,
group_secret_params,
randomness,
)),
}
}
}
impl AuthCredentialWithPniResponse {
pub fn new(bytes: &[u8]) -> Result<Self, ZkGroupDeserializationFailure> {
let first = bytes
.first()
.ok_or_else(ZkGroupDeserializationFailure::new::<Self>)?;
let version = AuthCredentialWithPniVersion::try_from(*first)
.map_err(|_| ZkGroupDeserializationFailure::new::<Self>())?;
match version {
AuthCredentialWithPniVersion::Zkc => {
crate::common::serialization::deserialize(bytes).map(Self::Zkc)
}
}
}
pub fn receive(
self,
public_params: &ServerPublicParams,
aci: Aci,
pni: Pni,
redemption_time: crate::Timestamp,
) -> Result<AuthCredentialWithPni, ZkGroupVerificationFailure> {
match self {
Self::Zkc(credential) => credential
.receive(aci, pni, redemption_time, public_params)
.map(AuthCredentialWithPni::Zkc),
}
}
}
impl Serialize for AuthCredentialWithPni {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::Zkc(z) => z.serialize(serializer),
}
}
}
impl Serialize for AuthCredentialWithPniResponse {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::Zkc(z) => z.serialize(serializer),
}
}
}
@@ -0,0 +1,267 @@
//
// Copyright 2024 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use libsignal_core::{Aci, Pni};
use partial_default::PartialDefault;
use serde::{Deserialize, Serialize};
use zkcredential::credentials::{CredentialKeyPair, CredentialPublicKey};
use crate::api::auth::auth_credential_with_pni::AuthCredentialWithPniVersion;
use crate::common::constants::PRESENTATION_VERSION_4;
use crate::common::serialization::VersionByte;
use crate::common::simple_types::{RandomnessBytes, Timestamp};
use crate::crypto::uid_encryption;
use crate::crypto::uid_struct::UidStruct;
use crate::groups::{GroupPublicParams, GroupSecretParams, UuidCiphertext};
use crate::{ServerPublicParams, ServerSecretParams, ZkGroupVerificationFailure};
const CREDENTIAL_LABEL: &[u8] = b"20240222_Signal_AuthCredentialZkc";
/// Authentication credential implemented using [`zkcredential`].
///
/// The same credential as [`crate::api::auth::AuthCredentialWithPni`] but
/// implemented using the types and mechanism from the `zkcredential` crate.
#[derive(Clone, Serialize, Deserialize, PartialDefault)]
pub struct AuthCredentialWithPniZkc {
version: VersionByte<{ AuthCredentialWithPniVersion::Zkc as u8 }>,
credential: zkcredential::credentials::Credential,
aci: UidStruct,
pni: UidStruct,
redemption_time: Timestamp,
}
#[derive(Clone, Serialize, Deserialize, PartialDefault)]
pub struct AuthCredentialWithPniZkcResponse {
version: VersionByte<{ AuthCredentialWithPniVersion::Zkc as u8 }>,
proof: zkcredential::issuance::IssuanceProof,
}
#[derive(Clone, Serialize, Deserialize, PartialDefault)]
pub struct AuthCredentialWithPniZkcPresentation {
version: VersionByte<PRESENTATION_VERSION_4>,
proof: zkcredential::presentation::PresentationProof,
aci_ciphertext: uid_encryption::Ciphertext,
pni_ciphertext: uid_encryption::Ciphertext,
redemption_time: Timestamp,
}
impl AuthCredentialWithPniZkcResponse {
pub fn issue_credential(
aci: Aci,
pni: Pni,
redemption_time: Timestamp,
params: &ServerSecretParams,
randomness: RandomnessBytes,
) -> Self {
Self::issue_credential_for_key(
aci,
pni,
redemption_time,
&params.generic_credential_key_pair,
randomness,
)
}
pub fn receive(
self,
aci: Aci,
pni: Pni,
redemption_time: Timestamp,
public_params: &ServerPublicParams,
) -> Result<AuthCredentialWithPniZkc, ZkGroupVerificationFailure> {
self.receive_for_key(
aci,
pni,
redemption_time,
&public_params.generic_credential_public_key,
)
}
pub(crate) fn issue_credential_for_key(
aci: Aci,
pni: Pni,
redemption_time: Timestamp,
credential_key: &CredentialKeyPair,
randomness: RandomnessBytes,
) -> Self {
let proof = zkcredential::issuance::IssuanceProofBuilder::new(CREDENTIAL_LABEL)
.add_attribute(&UidStruct::from_service_id(aci.into()))
.add_attribute(&UidStruct::from_service_id(pni.into()))
.add_public_attribute(&redemption_time)
.issue(credential_key, randomness);
Self {
version: VersionByte,
proof,
}
}
pub(crate) fn receive_for_key(
self,
aci: Aci,
pni: Pni,
redemption_time: Timestamp,
public_key: &CredentialPublicKey,
) -> Result<AuthCredentialWithPniZkc, ZkGroupVerificationFailure> {
if !redemption_time.is_day_aligned() {
return Err(ZkGroupVerificationFailure);
}
let aci = UidStruct::from_service_id(aci.into());
let pni = UidStruct::from_service_id(pni.into());
let raw_credential = zkcredential::issuance::IssuanceProofBuilder::new(CREDENTIAL_LABEL)
.add_attribute(&aci)
.add_attribute(&pni)
.add_public_attribute(&redemption_time)
.verify(public_key, self.proof)?;
Ok(AuthCredentialWithPniZkc {
credential: raw_credential,
version: VersionByte,
aci,
pni,
redemption_time,
})
}
}
impl AuthCredentialWithPniZkc {
pub fn present(
&self,
public_params: &ServerPublicParams,
group_secret_params: &GroupSecretParams,
randomness: RandomnessBytes,
) -> AuthCredentialWithPniZkcPresentation {
self.present_for_key(
&public_params.generic_credential_public_key,
group_secret_params,
randomness,
)
}
pub(crate) fn present_for_key(
&self,
public_key: &CredentialPublicKey,
group_secret_params: &GroupSecretParams,
randomness: RandomnessBytes,
) -> AuthCredentialWithPniZkcPresentation {
let Self {
aci,
credential,
pni,
redemption_time,
version: _,
} = self;
let proof = zkcredential::presentation::PresentationProofBuilder::new(CREDENTIAL_LABEL)
.add_attribute(aci, &group_secret_params.uid_enc_key_pair)
.add_attribute(pni, &group_secret_params.uid_enc_key_pair)
.present(public_key, credential, randomness);
AuthCredentialWithPniZkcPresentation {
aci_ciphertext: group_secret_params.uid_enc_key_pair.encrypt(&self.aci),
pni_ciphertext: group_secret_params.uid_enc_key_pair.encrypt(&self.pni),
proof,
redemption_time: *redemption_time,
version: VersionByte,
}
}
}
impl AuthCredentialWithPniZkcPresentation {
pub fn verify(
&self,
params: &ServerSecretParams,
group_public_params: &GroupPublicParams,
redemption_time: Timestamp,
) -> Result<(), ZkGroupVerificationFailure> {
self.verify_for_key(
&params.generic_credential_key_pair,
group_public_params,
redemption_time,
)
}
pub(crate) fn verify_for_key(
&self,
credential_key: &CredentialKeyPair,
group_public_params: &GroupPublicParams,
redemption_time: Timestamp,
) -> Result<(), ZkGroupVerificationFailure> {
zkcredential::presentation::PresentationProofVerifier::new(CREDENTIAL_LABEL)
.add_attribute(
&self.aci_ciphertext,
&group_public_params.uid_enc_public_key,
)
.add_attribute(
&self.pni_ciphertext,
&group_public_params.uid_enc_public_key,
)
.add_public_attribute(&redemption_time)
.verify(credential_key, &self.proof)
.map_err(|_| ZkGroupVerificationFailure)
}
pub fn aci_ciphertext(&self) -> UuidCiphertext {
UuidCiphertext {
reserved: Default::default(),
ciphertext: self.aci_ciphertext,
}
}
pub fn pni_ciphertext(&self) -> UuidCiphertext {
UuidCiphertext {
reserved: Default::default(),
ciphertext: self.pni_ciphertext,
}
}
pub fn redemption_time(&self) -> Timestamp {
self.redemption_time
}
}
#[cfg(test)]
mod test {
use zkcredential::RANDOMNESS_LEN;
use super::*;
use crate::SECONDS_PER_DAY;
#[test]
fn issue_receive_present() {
const ACI: Aci = Aci::from_uuid_bytes([b'a'; 16]);
const PNI: Pni = Pni::from_uuid_bytes([b'p'; 16]);
const REDEMPTION_TIME: Timestamp = Timestamp::from_epoch_seconds(12345 * SECONDS_PER_DAY);
let credential_key = CredentialKeyPair::generate([1; RANDOMNESS_LEN]);
let public_key = credential_key.public_key();
let group_secret_params = GroupSecretParams::generate([2; RANDOMNESS_LEN]);
let response = AuthCredentialWithPniZkcResponse::issue_credential_for_key(
ACI,
PNI,
REDEMPTION_TIME,
&credential_key,
[3; RANDOMNESS_LEN],
);
let credential = response
.receive_for_key(ACI, PNI, REDEMPTION_TIME, public_key)
.expect("is valid");
let presentation =
credential.present_for_key(public_key, &group_secret_params, [4; RANDOMNESS_LEN]);
presentation
.verify_for_key(
&credential_key,
&group_secret_params.get_public_params(),
REDEMPTION_TIME,
)
.expect("can verify")
}
}
+383
View File
@@ -0,0 +1,383 @@
//
// Copyright 2020 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use aes_gcm_siv::aead::Aead;
use aes_gcm_siv::aead::generic_array::GenericArray;
use aes_gcm_siv::{Aes256GcmSiv, KeyInit};
use partial_default::PartialDefault;
use serde::{Deserialize, Serialize};
use crate::common::constants::*;
use crate::common::errors::*;
use crate::common::serialization::ReservedByte;
use crate::common::sho::*;
use crate::common::simple_types::*;
use crate::crypto::uid_encryption;
use crate::{api, crypto};
#[derive(Copy, Clone, Serialize, Deserialize, Default)]
pub struct GroupMasterKey {
pub(crate) bytes: [u8; GROUP_MASTER_KEY_LEN],
}
#[derive(Copy, Clone, Serialize, Deserialize, PartialDefault)]
pub struct GroupSecretParams {
reserved: ReservedByte,
master_key: GroupMasterKey,
group_id: GroupIdentifierBytes,
blob_key: AesKeyBytes,
pub(crate) uid_enc_key_pair: crypto::uid_encryption::KeyPair,
pub(crate) profile_key_enc_key_pair: crypto::profile_key_encryption::KeyPair,
}
impl AsRef<uid_encryption::KeyPair> for GroupSecretParams {
fn as_ref(&self) -> &uid_encryption::KeyPair {
&self.uid_enc_key_pair
}
}
#[derive(Copy, Clone, Serialize, Deserialize, PartialDefault)]
pub struct GroupPublicParams {
reserved: ReservedByte,
group_id: GroupIdentifierBytes,
pub(crate) uid_enc_public_key: crypto::uid_encryption::PublicKey,
pub(crate) profile_key_enc_public_key: crypto::profile_key_encryption::PublicKey,
}
impl GroupMasterKey {
pub fn new(bytes: [u8; GROUP_MASTER_KEY_LEN]) -> Self {
GroupMasterKey { bytes }
}
}
const ENCRYPTED_BLOB_PADDING_LENGTH_SIZE: usize = std::mem::size_of::<u32>();
impl GroupSecretParams {
pub fn generate(randomness: RandomnessBytes) -> Self {
let mut sho = Sho::new(
b"Signal_ZKGroup_20200424_Random_GroupSecretParams_Generate",
&randomness,
);
let master_key = GroupMasterKey::new(sho.squeeze_as_array());
GroupSecretParams::derive_from_master_key(master_key)
}
pub fn derive_from_master_key(master_key: GroupMasterKey) -> Self {
let mut sho = Sho::new(
b"Signal_ZKGroup_20200424_GroupMasterKey_GroupSecretParams_DeriveFromMasterKey",
&master_key.bytes,
);
let group_id: GroupIdentifierBytes = sho.squeeze_as_array();
let blob_key: AesKeyBytes = sho.squeeze_as_array();
let uid_enc_key_pair = crypto::uid_encryption::KeyPair::derive_from(sho.as_mut());
let profile_key_enc_key_pair =
crypto::profile_key_encryption::KeyPair::derive_from(sho.as_mut());
Self {
reserved: Default::default(),
master_key,
group_id,
blob_key,
uid_enc_key_pair,
profile_key_enc_key_pair,
}
}
pub fn get_master_key(&self) -> GroupMasterKey {
self.master_key
}
pub fn get_group_identifier(&self) -> GroupIdentifierBytes {
self.group_id
}
pub fn get_public_params(&self) -> GroupPublicParams {
GroupPublicParams {
reserved: Default::default(),
uid_enc_public_key: self.uid_enc_key_pair.public_key,
profile_key_enc_public_key: self.profile_key_enc_key_pair.public_key,
group_id: self.group_id,
}
}
pub fn encrypt_service_id(
&self,
service_id: libsignal_core::ServiceId,
) -> api::groups::UuidCiphertext {
let uid = crypto::uid_struct::UidStruct::from_service_id(service_id);
self.encrypt_uid_struct(uid)
}
pub fn encrypt_uid_struct(
&self,
uid: crypto::uid_struct::UidStruct,
) -> api::groups::UuidCiphertext {
let ciphertext = self.uid_enc_key_pair.encrypt(&uid);
api::groups::UuidCiphertext {
reserved: Default::default(),
ciphertext,
}
}
pub fn decrypt_service_id(
&self,
ciphertext: api::groups::UuidCiphertext,
) -> Result<libsignal_core::ServiceId, ZkGroupVerificationFailure> {
crypto::uid_encryption::UidEncryptionDomain::decrypt(
&self.uid_enc_key_pair,
&ciphertext.ciphertext,
)
}
pub fn encrypt_profile_key(
&self,
profile_key: api::profiles::ProfileKey,
user_id: libsignal_core::Aci,
) -> api::groups::ProfileKeyCiphertext {
self.encrypt_profile_key_bytes(profile_key.bytes, user_id)
}
pub fn encrypt_profile_key_bytes(
&self,
profile_key_bytes: ProfileKeyBytes,
user_id: libsignal_core::Aci,
) -> api::groups::ProfileKeyCiphertext {
let profile_key = crypto::profile_key_struct::ProfileKeyStruct::new(
profile_key_bytes,
uuid::Uuid::from(user_id).into_bytes(),
);
let ciphertext = self.profile_key_enc_key_pair.encrypt(&profile_key);
api::groups::ProfileKeyCiphertext {
reserved: Default::default(),
ciphertext,
}
}
pub fn decrypt_profile_key(
&self,
ciphertext: api::groups::ProfileKeyCiphertext,
user_id: libsignal_core::Aci,
) -> Result<api::profiles::ProfileKey, ZkGroupVerificationFailure> {
let profile_key_struct =
crypto::profile_key_encryption::ProfileKeyEncryptionDomain::decrypt(
&self.profile_key_enc_key_pair,
&ciphertext.ciphertext,
uuid::Uuid::from(user_id).into_bytes(),
)?;
Ok(api::profiles::ProfileKey {
bytes: profile_key_struct.bytes,
})
}
pub fn encrypt_blob(&self, randomness: RandomnessBytes, plaintext: &[u8]) -> Vec<u8> {
let mut sho = Sho::new(
b"Signal_ZKGroup_20200424_Random_GroupSecretParams_EncryptBlob",
&randomness,
);
let nonce_vec = sho.squeeze_as_array::<AESGCM_NONCE_LEN>();
let mut ciphertext_vec = self.encrypt_blob_aesgcmsiv(&self.blob_key, &nonce_vec, plaintext);
ciphertext_vec.extend(nonce_vec);
ciphertext_vec.extend([0u8]); // reserved byte
ciphertext_vec
}
pub fn encrypt_blob_with_padding(
&self,
randomness: RandomnessBytes,
plaintext: &[u8],
padding_len: u32,
) -> Vec<u8> {
let full_length =
ENCRYPTED_BLOB_PADDING_LENGTH_SIZE + plaintext.len() + padding_len as usize;
let mut padded_plaintext = Vec::with_capacity(full_length);
padded_plaintext.extend_from_slice(&padding_len.to_be_bytes());
padded_plaintext.extend_from_slice(plaintext);
padded_plaintext.resize(full_length, 0);
self.encrypt_blob(randomness, &padded_plaintext)
}
pub fn decrypt_blob(&self, ciphertext: &[u8]) -> Result<Vec<u8>, ZkGroupVerificationFailure> {
if ciphertext.len() < AESGCM_NONCE_LEN + 1 {
// AESGCM_NONCE_LEN = 12 bytes for IV
return Err(ZkGroupVerificationFailure);
}
let unreserved_len = ciphertext.len() - 1;
let (ciphertext, nonce) = ciphertext[..unreserved_len]
.split_last_chunk::<AESGCM_NONCE_LEN>()
.expect("checked length already");
self.decrypt_blob_aesgcmsiv(&self.blob_key, nonce, ciphertext)
}
pub fn decrypt_blob_with_padding(
&self,
ciphertext: &[u8],
) -> Result<Vec<u8>, ZkGroupVerificationFailure> {
let mut decrypted = self.decrypt_blob(ciphertext)?;
let (padding_len_bytes, plaintext_plus_padding) = decrypted
.split_first_chunk::<ENCRYPTED_BLOB_PADDING_LENGTH_SIZE>()
.ok_or(ZkGroupVerificationFailure)?;
let padding_len = u32::from_be_bytes(*padding_len_bytes);
if plaintext_plus_padding.len() < padding_len as usize {
return Err(ZkGroupVerificationFailure);
}
decrypted.truncate(decrypted.len() - padding_len as usize);
decrypted.drain(..ENCRYPTED_BLOB_PADDING_LENGTH_SIZE);
Ok(decrypted)
}
fn encrypt_blob_aesgcmsiv(&self, key: &[u8], nonce: &[u8], plaintext: &[u8]) -> Vec<u8> {
let key = GenericArray::from_slice(key);
let aead_cipher = Aes256GcmSiv::new(key);
let nonce = GenericArray::from_slice(nonce);
aead_cipher
.encrypt(nonce, plaintext)
.expect("aead encrypt failure")
}
fn decrypt_blob_aesgcmsiv(
&self,
key: &[u8],
nonce: &[u8],
ciphertext: &[u8],
) -> Result<Vec<u8>, ZkGroupVerificationFailure> {
if ciphertext.len() < AESGCM_TAG_LEN {
// AESGCM_TAG_LEN = 16 bytes for tag
return Err(ZkGroupVerificationFailure);
}
let key = GenericArray::from_slice(key);
let aead_cipher = Aes256GcmSiv::new(key);
let nonce = GenericArray::from_slice(nonce);
match aead_cipher.decrypt(nonce, ciphertext) {
Ok(plaintext_vec) => Ok(plaintext_vec),
Err(_) => Err(ZkGroupVerificationFailure),
}
}
}
impl GroupPublicParams {
pub fn get_group_identifier(&self) -> GroupIdentifierBytes {
self.group_id
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_aesgcmsiv_vec1() {
// https://tools.ietf.org/html/rfc8452#appendix-C
let group_secret_params = GroupSecretParams::generate([0u8; RANDOMNESS_LEN]);
let plaintext_vec = vec![
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
];
let key = [
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
];
let nonce = [
0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
let ciphertext = [
0x4a, 0x6a, 0x9d, 0xb4, 0xc8, 0xc6, 0x54, 0x92, 0x01, 0xb9, 0xed, 0xb5, 0x30, 0x06,
0xcb, 0xa8, 0x21, 0xec, 0x9c, 0xf8, 0x50, 0x94, 0x8a, 0x7c, 0x86, 0xc6, 0x8a, 0xc7,
0x53, 0x9d, 0x02, 0x7f, 0xe8, 0x19, 0xe6, 0x3a, 0xbc, 0xd0, 0x20, 0xb0, 0x06, 0xa9,
0x76, 0x39, 0x76, 0x32, 0xeb, 0x5d,
];
let calc_ciphertext =
group_secret_params.encrypt_blob_aesgcmsiv(&key, &nonce, &plaintext_vec);
assert!(calc_ciphertext[..ciphertext.len()] == ciphertext[..]);
let calc_plaintext = group_secret_params
.decrypt_blob_aesgcmsiv(&key, &nonce, &calc_ciphertext)
.unwrap();
assert!(calc_plaintext[..] == plaintext_vec[..]);
}
#[test]
fn test_aesgcmsiv_vec2() {
// https://tools.ietf.org/html/rfc8452#appendix-C
let group_secret_params = GroupSecretParams::generate([0u8; RANDOMNESS_LEN]);
let plaintext_vec = vec![
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x4d, 0xb9, 0x23, 0xdc, 0x79, 0x3e, 0xe6, 0x49, 0x7c, 0x76, 0xdc, 0xc0,
0x3a, 0x98, 0xe1, 0x08,
];
let key = [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
];
let nonce = [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
let ciphertext = [
0xf3, 0xf8, 0x0f, 0x2c, 0xf0, 0xcb, 0x2d, 0xd9, 0xc5, 0x98, 0x4f, 0xcd, 0xa9, 0x08,
0x45, 0x6c, 0xc5, 0x37, 0x70, 0x3b, 0x5b, 0xa7, 0x03, 0x24, 0xa6, 0x79, 0x3a, 0x7b,
0xf2, 0x18, 0xd3, 0xea, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
let calc_ciphertext =
group_secret_params.encrypt_blob_aesgcmsiv(&key, &nonce, &plaintext_vec);
assert!(calc_ciphertext[..ciphertext.len()] == ciphertext[..]);
let calc_plaintext = group_secret_params
.decrypt_blob_aesgcmsiv(&key, &nonce, &calc_ciphertext)
.unwrap();
assert!(calc_plaintext[..] == plaintext_vec[..]);
}
#[test]
fn test_encrypt_with_padding() {
let group_secret_params = GroupSecretParams::generate([0u8; RANDOMNESS_LEN]);
let plaintext = b"secret team";
{
let expected_ciphertext_hex = "3798afe9c65ffb35a63b2c048b16f19dd50ee9acc33cc925667a9abad4d4c6f86675fa8e32243e0831203700";
let calc_ciphertext =
group_secret_params.encrypt_blob_with_padding([0u8; RANDOMNESS_LEN], plaintext, 0);
assert_eq!(hex::encode(&calc_ciphertext), expected_ciphertext_hex);
let calc_plaintext = group_secret_params
.decrypt_blob_with_padding(&calc_ciphertext)
.unwrap();
assert_eq!(calc_plaintext[..], plaintext[..]);
}
{
let expected_ciphertext_hex = "880a70e071b33f81e1219842c8514f34901abb734c191292ac325455d898da000484080099c620f86675fa8e32243e0831203700";
let calc_ciphertext =
group_secret_params.encrypt_blob_with_padding([0u8; RANDOMNESS_LEN], plaintext, 8);
assert_eq!(hex::encode(&calc_ciphertext), expected_ciphertext_hex);
let calc_plaintext = group_secret_params
.decrypt_blob_with_padding(&calc_ciphertext)
.unwrap();
assert_eq!(calc_plaintext[..], plaintext[..]);
}
}
}
@@ -0,0 +1,16 @@
//
// Copyright 2020 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use partial_default::PartialDefault;
use serde::{Deserialize, Serialize};
use crate::common::serialization::ReservedByte;
use crate::crypto;
#[derive(Copy, Clone, Serialize, Deserialize, PartialEq, Eq, PartialDefault)]
pub struct ProfileKeyCiphertext {
pub(crate) reserved: ReservedByte,
pub(crate) ciphertext: crypto::profile_key_encryption::Ciphertext,
}
+597
View File
@@ -0,0 +1,597 @@
//
// Copyright 2020-2022 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use partial_default::PartialDefault;
use serde::{Deserialize, Serialize};
use crate::common::constants::*;
use crate::common::errors::*;
use crate::common::serialization::{ReservedByte, VersionByte};
use crate::common::sho::*;
use crate::common::simple_types::*;
use crate::{api, crypto};
#[derive(Clone, Serialize, Deserialize, PartialDefault)]
pub struct ServerSecretParams {
reserved: ReservedByte,
// Now unused
auth_credentials_key_pair: crypto::credentials::KeyPair<crypto::credentials::AuthCredential>,
// Now unused
pub(crate) profile_key_credentials_key_pair:
crypto::credentials::KeyPair<crypto::credentials::ProfileKeyCredential>,
sig_key_pair: crypto::signature::KeyPair,
receipt_credentials_key_pair:
crypto::credentials::KeyPair<crypto::credentials::ReceiptCredential>,
// Now unused
pni_credentials_key_pair: crypto::credentials::KeyPair<crypto::credentials::PniCredential>,
expiring_profile_key_credentials_key_pair:
crypto::credentials::KeyPair<crypto::credentials::ExpiringProfileKeyCredential>,
// Now unused
auth_credentials_with_pni_key_pair:
crypto::credentials::KeyPair<crypto::credentials::AuthCredentialWithPni>,
pub(crate) generic_credential_key_pair: zkcredential::credentials::CredentialKeyPair,
pub(crate) endorsement_key_pair: zkcredential::endorsements::ServerRootKeyPair,
}
impl AsRef<zkcredential::endorsements::ServerRootKeyPair> for ServerSecretParams {
fn as_ref(&self) -> &zkcredential::endorsements::ServerRootKeyPair {
&self.endorsement_key_pair
}
}
#[derive(Clone, Serialize, Deserialize, PartialDefault)]
pub struct ServerPublicParams {
reserved: ReservedByte,
// Now unused
auth_credentials_public_key: crypto::credentials::PublicKey,
// Now unused
pub(crate) profile_key_credentials_public_key: crypto::credentials::PublicKey,
sig_public_key: crypto::signature::PublicKey,
receipt_credentials_public_key: crypto::credentials::PublicKey,
// Now unused
pni_credentials_public_key: crypto::credentials::PublicKey,
expiring_profile_key_credentials_public_key: crypto::credentials::PublicKey,
// Now unused
auth_credentials_with_pni_public_key: crypto::credentials::PublicKey,
pub(crate) generic_credential_public_key: zkcredential::credentials::CredentialPublicKey,
pub(crate) endorsement_public_key: zkcredential::endorsements::ServerRootPublicKey,
}
impl AsRef<zkcredential::endorsements::ServerRootPublicKey> for ServerPublicParams {
fn as_ref(&self) -> &zkcredential::endorsements::ServerRootPublicKey {
&self.endorsement_public_key
}
}
impl ServerSecretParams {
pub fn generate(randomness: RandomnessBytes) -> Self {
let mut sho = Sho::new(
b"Signal_ZKGroup_20200424_Random_ServerSecretParams_Generate",
&randomness,
);
let auth_credentials_key_pair = crypto::credentials::KeyPair::generate(&mut sho);
let profile_key_credentials_key_pair = crypto::credentials::KeyPair::generate(&mut sho);
let sig_key_pair = crypto::signature::KeyPair::generate(&mut sho);
let receipt_credentials_key_pair = crypto::credentials::KeyPair::generate(&mut sho);
let pni_credentials_key_pair = crypto::credentials::KeyPair::generate(&mut sho);
let expiring_profile_key_credentials_key_pair =
crypto::credentials::KeyPair::generate(&mut sho);
let auth_credentials_with_pni_key_pair = crypto::credentials::KeyPair::generate(&mut sho);
let generic_credential_key_pair =
zkcredential::credentials::CredentialKeyPair::generate(randomness);
let endorsement_key_pair =
zkcredential::endorsements::ServerRootKeyPair::generate(randomness);
Self {
reserved: Default::default(),
auth_credentials_key_pair,
profile_key_credentials_key_pair,
sig_key_pair,
receipt_credentials_key_pair,
pni_credentials_key_pair,
expiring_profile_key_credentials_key_pair,
auth_credentials_with_pni_key_pair,
generic_credential_key_pair,
endorsement_key_pair,
}
}
pub fn get_endorsement_root_key_pair(&self) -> EndorsementServerRootKeyPair {
EndorsementServerRootKeyPair {
reserved: Default::default(),
key_pair: self.endorsement_key_pair.clone(),
}
}
pub fn get_public_params(&self) -> ServerPublicParams {
ServerPublicParams {
reserved: Default::default(),
auth_credentials_public_key: self.auth_credentials_key_pair.get_public_key(),
profile_key_credentials_public_key: self
.profile_key_credentials_key_pair
.get_public_key(),
sig_public_key: self.sig_key_pair.get_public_key(),
receipt_credentials_public_key: self.receipt_credentials_key_pair.get_public_key(),
pni_credentials_public_key: self.pni_credentials_key_pair.get_public_key(),
expiring_profile_key_credentials_public_key: self
.expiring_profile_key_credentials_key_pair
.get_public_key(),
auth_credentials_with_pni_public_key: self
.auth_credentials_with_pni_key_pair
.get_public_key(),
generic_credential_public_key: self.generic_credential_key_pair.public_key().clone(),
endorsement_public_key: self.endorsement_key_pair.public_key().clone(),
}
}
pub fn sign(&self, randomness: RandomnessBytes, message: &[u8]) -> NotarySignatureBytes {
let mut sho = Sho::new(
b"Signal_ZKGroup_20200424_Random_ServerSecretParams_Sign",
&randomness,
);
self.sig_key_pair.sign(message, &mut sho)
}
/// Checks that `current_time` is within the validity window defined by
/// `redemption_time`.
///
/// All times are relative to SystemTime::UNIX_EPOCH,
/// but we don't actually use SystemTime because it's too small on 32-bit Linux.
pub(crate) fn check_auth_credential_redemption_time(
redemption_time: Timestamp,
current_time: Timestamp,
) -> Result<(), ZkGroupVerificationFailure> {
let acceptable_start_time = redemption_time
.checked_sub_seconds(SECONDS_PER_DAY)
.ok_or(ZkGroupVerificationFailure)?;
let acceptable_end_time = redemption_time
.checked_add_seconds(2 * SECONDS_PER_DAY)
.ok_or(ZkGroupVerificationFailure)?;
if !(acceptable_start_time..=acceptable_end_time).contains(&current_time) {
return Err(ZkGroupVerificationFailure);
}
Ok(())
}
pub fn verify_auth_credential_presentation(
&self,
group_public_params: api::groups::GroupPublicParams,
presentation: &api::auth::AnyAuthCredentialPresentation,
current_time: Timestamp,
) -> Result<(), ZkGroupVerificationFailure> {
Self::check_auth_credential_redemption_time(
presentation.get_redemption_time(),
current_time,
)?;
match presentation {
api::auth::AnyAuthCredentialPresentation::V4(presentation) => {
presentation.verify(self, &group_public_params, presentation.redemption_time())
}
}
}
pub fn verify_profile_key_credential_presentation(
&self,
group_public_params: api::groups::GroupPublicParams,
presentation: &api::profiles::AnyProfileKeyCredentialPresentation,
current_time: Timestamp,
) -> Result<(), ZkGroupVerificationFailure> {
match presentation {
api::profiles::AnyProfileKeyCredentialPresentation::V1(_) => {
Err(ZkGroupVerificationFailure)
}
api::profiles::AnyProfileKeyCredentialPresentation::V2(_) => {
Err(ZkGroupVerificationFailure)
}
api::profiles::AnyProfileKeyCredentialPresentation::V3(presentation) => self
.verify_expiring_profile_key_credential_presentation(
group_public_params,
presentation,
current_time,
),
api::profiles::AnyProfileKeyCredentialPresentation::V4(presentation) => self
.verify_expiring_profile_key_credential_presentation(
group_public_params,
presentation,
current_time,
),
}
}
pub fn verify_expiring_profile_key_credential_presentation<const V: u8>(
&self,
group_public_params: api::groups::GroupPublicParams,
presentation: &api::profiles::ExpiringProfileKeyCredentialPresentation<V>,
current_time: Timestamp,
) -> Result<(), ZkGroupVerificationFailure> {
let credentials_key_pair = self.expiring_profile_key_credentials_key_pair;
let uid_enc_public_key = group_public_params.uid_enc_public_key;
let profile_key_enc_public_key = group_public_params.profile_key_enc_public_key;
presentation.proof.verify(
credentials_key_pair,
presentation.uid_enc_ciphertext,
uid_enc_public_key,
presentation.profile_key_enc_ciphertext,
profile_key_enc_public_key,
presentation.credential_expiration_time,
V >= PRESENTATION_VERSION_4,
)?;
if presentation.credential_expiration_time <= current_time {
return Err(ZkGroupVerificationFailure);
}
Ok(())
}
pub fn issue_expiring_profile_key_credential(
&self,
randomness: RandomnessBytes,
request: &api::profiles::ProfileKeyCredentialRequest,
aci: libsignal_core::Aci,
commitment: api::profiles::ProfileKeyCommitment,
credential_expiration_time: Timestamp,
) -> Result<api::profiles::ExpiringProfileKeyCredentialResponse, ZkGroupVerificationFailure>
{
let mut sho = Sho::new(
b"Signal_ZKGroup_20220508_Random_ServerSecretParams_IssueExpiringProfileKeyCredential",
&randomness,
);
request.proof.verify(
request.public_key,
request.ciphertext,
commitment.commitment,
)?;
let uid = crypto::uid_struct::UidStruct::from_service_id(aci.into());
let blinded_credential_with_secret_nonce = self
.expiring_profile_key_credentials_key_pair
.create_blinded_expiring_profile_key_credential(
uid,
request.public_key,
request.ciphertext,
credential_expiration_time,
&mut sho,
);
let proof = crypto::proofs::ExpiringProfileKeyCredentialIssuanceProof::new(
self.expiring_profile_key_credentials_key_pair,
request.public_key,
request.ciphertext,
blinded_credential_with_secret_nonce,
uid,
credential_expiration_time,
&mut sho,
);
Ok(api::profiles::ExpiringProfileKeyCredentialResponse {
reserved: Default::default(),
blinded_credential: blinded_credential_with_secret_nonce
.get_blinded_expiring_profile_key_credential(),
credential_expiration_time,
proof,
})
}
pub fn issue_receipt_credential(
&self,
randomness: RandomnessBytes,
request: &api::receipts::ReceiptCredentialRequest,
receipt_expiration_time: Timestamp,
receipt_level: ReceiptLevel,
) -> api::receipts::ReceiptCredentialResponse {
let mut sho = Sho::new(
b"Signal_ZKGroup_20210919_Random_ServerSecretParams_IssueReceiptCredential",
&randomness,
);
let blinded_credential_with_secret_nonce = self
.receipt_credentials_key_pair
.create_blinded_receipt_credential(
request.public_key,
request.ciphertext,
receipt_expiration_time,
receipt_level,
&mut sho,
);
let proof = crypto::proofs::ReceiptCredentialIssuanceProof::new(
self.receipt_credentials_key_pair,
request.public_key,
request.ciphertext,
blinded_credential_with_secret_nonce,
receipt_expiration_time,
receipt_level,
&mut sho,
);
api::receipts::ReceiptCredentialResponse {
reserved: Default::default(),
receipt_expiration_time,
receipt_level,
blinded_credential: blinded_credential_with_secret_nonce
.get_blinded_receipt_credential(),
proof,
}
}
pub fn verify_receipt_credential_presentation(
&self,
presentation: &api::receipts::ReceiptCredentialPresentation,
) -> Result<(), ZkGroupVerificationFailure> {
presentation.proof.verify(
self.receipt_credentials_key_pair,
presentation.get_receipt_struct(),
)
}
}
impl ServerPublicParams {
pub fn get_endorsement_public_key(&self) -> EndorsementPublicKey {
EndorsementPublicKey {
reserved: Default::default(),
public_key: self.endorsement_public_key.clone(),
}
}
pub fn verify_signature(
&self,
message: &[u8],
signature: NotarySignatureBytes,
) -> Result<(), ZkGroupVerificationFailure> {
self.sig_public_key.verify(message, signature)
}
pub fn create_profile_key_credential_request_context(
&self,
randomness: RandomnessBytes,
aci: libsignal_core::Aci,
profile_key: api::profiles::ProfileKey,
) -> api::profiles::ProfileKeyCredentialRequestContext {
let mut sho = Sho::new(
b"Signal_ZKGroup_20200424_Random_ServerPublicParams_CreateProfileKeyCredentialRequestContext",
&randomness,
);
let uid_bytes = uuid::Uuid::from(aci).into_bytes();
let profile_key_struct =
crypto::profile_key_struct::ProfileKeyStruct::new(profile_key.bytes, uid_bytes);
let commitment_with_secret_nonce =
crypto::profile_key_commitment::CommitmentWithSecretNonce::new(
profile_key_struct,
uid_bytes,
);
let key_pair = crypto::profile_key_credential_request::KeyPair::generate(&mut sho);
let ciphertext_with_secret_nonce = key_pair.encrypt(profile_key_struct, &mut sho);
let proof = crypto::proofs::ProfileKeyCredentialRequestProof::new(
key_pair,
ciphertext_with_secret_nonce,
commitment_with_secret_nonce,
&mut sho,
);
api::profiles::ProfileKeyCredentialRequestContext {
reserved: Default::default(),
aci_bytes: uid_bytes,
profile_key_bytes: profile_key_struct.bytes,
key_pair,
ciphertext_with_secret_nonce,
proof,
}
}
pub fn receive_expiring_profile_key_credential(
&self,
context: &api::profiles::ProfileKeyCredentialRequestContext,
response: &api::profiles::ExpiringProfileKeyCredentialResponse,
current_time: Timestamp,
) -> Result<api::profiles::ExpiringProfileKeyCredential, ZkGroupVerificationFailure> {
response.proof.verify(
self.expiring_profile_key_credentials_public_key,
context.key_pair.get_public_key(),
context.aci_bytes,
context.ciphertext_with_secret_nonce.get_ciphertext(),
response.blinded_credential,
response.credential_expiration_time,
)?;
if !response.credential_expiration_time.is_day_aligned() {
return Err(ZkGroupVerificationFailure);
}
let days_remaining = response
.credential_expiration_time
.saturating_seconds_since(current_time)
/ SECONDS_PER_DAY;
if days_remaining == 0 || days_remaining > 7 {
return Err(ZkGroupVerificationFailure);
}
let credential = context
.key_pair
.decrypt_blinded_expiring_profile_key_credential(response.blinded_credential);
Ok(api::profiles::ExpiringProfileKeyCredential {
reserved: Default::default(),
credential,
aci_bytes: context.aci_bytes,
profile_key_bytes: context.profile_key_bytes,
credential_expiration_time: response.credential_expiration_time,
})
}
pub fn create_expiring_profile_key_credential_presentation<const V: u8>(
&self,
randomness: RandomnessBytes,
group_secret_params: api::groups::GroupSecretParams,
expiring_profile_key_credential: api::profiles::ExpiringProfileKeyCredential,
) -> api::profiles::ExpiringProfileKeyCredentialPresentation<V> {
let mut sho = Sho::new(
b"Signal_ZKGroup_20220508_Random_ServerPublicParams_CreateExpiringProfileKeyCredentialPresentation",
&randomness,
);
let uid_enc_key_pair = group_secret_params.uid_enc_key_pair;
let profile_key_enc_key_pair = group_secret_params.profile_key_enc_key_pair;
let credentials_public_key = self.expiring_profile_key_credentials_public_key;
let uid = expiring_profile_key_credential.aci();
let uuid_ciphertext = group_secret_params.encrypt_service_id(uid.into());
let profile_key_ciphertext = group_secret_params
.encrypt_profile_key_bytes(expiring_profile_key_credential.profile_key_bytes, uid);
let proof = crypto::proofs::ExpiringProfileKeyCredentialPresentationProof::new(
uid_enc_key_pair,
profile_key_enc_key_pair,
credentials_public_key,
expiring_profile_key_credential.credential,
uuid_ciphertext.ciphertext,
profile_key_ciphertext.ciphertext,
expiring_profile_key_credential.aci_bytes,
expiring_profile_key_credential.profile_key_bytes,
V >= PRESENTATION_VERSION_4,
&mut sho,
);
api::profiles::ExpiringProfileKeyCredentialPresentation {
version: VersionByte,
proof,
uid_enc_ciphertext: uuid_ciphertext.ciphertext,
profile_key_enc_ciphertext: profile_key_ciphertext.ciphertext,
credential_expiration_time: expiring_profile_key_credential.credential_expiration_time,
}
}
pub fn create_receipt_credential_request_context(
&self,
randomness: RandomnessBytes,
receipt_serial_bytes: ReceiptSerialBytes,
) -> api::receipts::ReceiptCredentialRequestContext {
let mut sho = Sho::new(
b"Signal_ZKGroup_20210919_Random_ServerPublicParams_CreateReceiptCredentialRequestContext",
&randomness,
);
let key_pair = crypto::receipt_credential_request::KeyPair::generate(&mut sho);
let ciphertext_with_secret_nonce = key_pair.encrypt(receipt_serial_bytes, &mut sho);
api::receipts::ReceiptCredentialRequestContext {
reserved: Default::default(),
receipt_serial_bytes,
key_pair,
ciphertext_with_secret_nonce,
}
}
pub fn receive_receipt_credential(
&self,
context: &api::receipts::ReceiptCredentialRequestContext,
response: &api::receipts::ReceiptCredentialResponse,
) -> Result<api::receipts::ReceiptCredential, ZkGroupVerificationFailure> {
if !response.receipt_expiration_time.is_day_aligned() {
return Err(ZkGroupVerificationFailure);
}
let receipt_struct = crypto::receipt_struct::ReceiptStruct::new(
context.receipt_serial_bytes,
response.receipt_expiration_time,
response.receipt_level,
);
response.proof.verify(
self.receipt_credentials_public_key,
context.key_pair.get_public_key(),
context.ciphertext_with_secret_nonce.get_ciphertext(),
response.blinded_credential,
receipt_struct,
)?;
let credential = context
.key_pair
.decrypt_blinded_receipt_credential(response.blinded_credential);
Ok(api::receipts::ReceiptCredential {
reserved: Default::default(),
credential,
receipt_expiration_time: response.receipt_expiration_time,
receipt_level: response.receipt_level,
receipt_serial_bytes: context.receipt_serial_bytes,
})
}
pub fn create_receipt_credential_presentation(
&self,
randomness: RandomnessBytes,
receipt_credential: &api::receipts::ReceiptCredential,
) -> api::receipts::ReceiptCredentialPresentation {
let mut sho = Sho::new(
b"Signal_ZKGroup_20210919_Random_ServerPublicParams_CreateReceiptCredentialPresentation",
&randomness,
);
let proof = crypto::proofs::ReceiptCredentialPresentationProof::new(
self.receipt_credentials_public_key,
receipt_credential.credential,
&mut sho,
);
api::receipts::ReceiptCredentialPresentation {
reserved: Default::default(),
proof,
receipt_expiration_time: receipt_credential.receipt_expiration_time,
receipt_level: receipt_credential.receipt_level,
receipt_serial_bytes: receipt_credential.receipt_serial_bytes,
}
}
}
#[derive(Clone, Serialize, Deserialize, PartialDefault)]
pub struct EndorsementServerRootKeyPair {
reserved: ReservedByte,
pub(crate) key_pair: zkcredential::endorsements::ServerRootKeyPair,
}
impl AsRef<zkcredential::endorsements::ServerRootKeyPair> for EndorsementServerRootKeyPair {
fn as_ref(&self) -> &zkcredential::endorsements::ServerRootKeyPair {
&self.key_pair
}
}
impl EndorsementServerRootKeyPair {
pub fn public_key(&self) -> EndorsementPublicKey {
EndorsementPublicKey {
reserved: self.reserved,
public_key: self.key_pair.public_key().clone(),
}
}
}
#[derive(Clone, Serialize, Deserialize, PartialDefault)]
pub struct EndorsementPublicKey {
reserved: ReservedByte,
public_key: zkcredential::endorsements::ServerRootPublicKey,
}
impl AsRef<zkcredential::endorsements::ServerRootPublicKey> for EndorsementPublicKey {
fn as_ref(&self) -> &zkcredential::endorsements::ServerRootPublicKey {
&self.public_key
}
}
+16
View File
@@ -0,0 +1,16 @@
//
// Copyright 2020 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use partial_default::PartialDefault;
use serde::{Deserialize, Serialize};
use crate::common::serialization::ReservedByte;
use crate::crypto;
#[derive(Copy, Clone, Serialize, Deserialize, PartialEq, Eq, PartialDefault)]
pub struct UuidCiphertext {
pub(crate) reserved: ReservedByte,
pub(crate) ciphertext: crypto::uid_encryption::Ciphertext,
}
+90
View File
@@ -0,0 +1,90 @@
//
// Copyright 2020-2022 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
pub const NUM_AUTH_CRED_ATTRIBUTES: usize = 3;
pub const NUM_PROFILE_KEY_CRED_ATTRIBUTES: usize = 4;
pub const NUM_RECEIPT_CRED_ATTRIBUTES: usize = 2;
pub const PRESENTATION_VERSION_1: u8 = 0;
pub const PRESENTATION_VERSION_2: u8 = 1;
pub const PRESENTATION_VERSION_3: u8 = 2;
pub const PRESENTATION_VERSION_4: u8 = 3;
pub const AES_KEY_LEN: usize = 32;
pub const AESGCM_NONCE_LEN: usize = 12;
pub const AESGCM_TAG_LEN: usize = 16;
pub const GROUP_MASTER_KEY_LEN: usize = 32;
pub const GROUP_SECRET_PARAMS_LEN: usize = 289;
pub const GROUP_PUBLIC_PARAMS_LEN: usize = 97;
pub const GROUP_IDENTIFIER_LEN: usize = 32;
pub const AUTH_CREDENTIAL_LEN: usize = 181;
pub const AUTH_CREDENTIAL_PRESENTATION_V2_LEN: usize = 461;
pub const AUTH_CREDENTIAL_RESPONSE_LEN: usize = 361;
pub const AUTH_CREDENTIAL_WITH_PNI_LEN: usize = 265;
pub const AUTH_CREDENTIAL_WITH_PNI_RESPONSE_LEN: usize = 425;
pub const PROFILE_KEY_LEN: usize = 32;
pub const PROFILE_KEY_CIPHERTEXT_LEN: usize = 65;
pub const PROFILE_KEY_COMMITMENT_LEN: usize = 97;
pub const EXPIRING_PROFILE_KEY_CREDENTIAL_LEN: usize = 153;
pub(crate) const PROFILE_KEY_CREDENTIAL_PRESENTATION_V1_LEN: usize = 713;
pub const PROFILE_KEY_CREDENTIAL_PRESENTATION_V2_LEN: usize = 713;
pub const PROFILE_KEY_CREDENTIAL_REQUEST_LEN: usize = 329;
pub const PROFILE_KEY_CREDENTIAL_REQUEST_CONTEXT_LEN: usize = 473;
pub const EXPIRING_PROFILE_KEY_CREDENTIAL_RESPONSE_LEN: usize = 497;
pub const PROFILE_KEY_VERSION_LEN: usize = 32;
pub const PROFILE_KEY_VERSION_ENCODED_LEN: usize = 64;
pub const RECEIPT_CREDENTIAL_LEN: usize = 129;
pub const RECEIPT_CREDENTIAL_PRESENTATION_LEN: usize = 329;
pub const RECEIPT_CREDENTIAL_REQUEST_LEN: usize = 97;
pub const RECEIPT_CREDENTIAL_REQUEST_CONTEXT_LEN: usize = 177;
pub const RECEIPT_CREDENTIAL_RESPONSE_LEN: usize = 409;
pub const RECEIPT_SERIAL_LEN: usize = 16;
pub const RESERVED_LEN: usize = 1;
pub const SERVER_SECRET_PARAMS_LEN: usize = 2721;
pub const SERVER_PUBLIC_PARAMS_LEN: usize = 673;
pub const UUID_CIPHERTEXT_LEN: usize = 65;
pub const RANDOMNESS_LEN: usize = 32;
pub const SIGNATURE_LEN: usize = 64;
pub const UUID_LEN: usize = 16;
pub const ACCESS_KEY_LEN: usize = 16;
/// Seconds in a 24-hour cycle (ignoring leap seconds).
pub const SECONDS_PER_DAY: u64 = 86400;
pub const TEST_ARRAY_16: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
pub const TEST_ARRAY_16_1: [u8; 16] = [
100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115,
];
pub const TEST_ARRAY_32: [u8; 32] = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
26, 27, 28, 29, 30, 31,
];
pub const TEST_ARRAY_32_1: [u8; 32] = [
100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118,
119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131,
];
pub const TEST_ARRAY_32_2: [u8; 32] = [
200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218,
219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231,
];
pub const TEST_ARRAY_32_3: [u8; 32] = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
27, 28, 29, 30, 31, 32,
];
pub const TEST_ARRAY_32_4: [u8; 32] = [
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
28, 29, 30, 31, 32, 33,
];
pub const TEST_ARRAY_32_5: [u8; 32] = [
3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
28, 29, 30, 31, 32, 33, 34,
];
+166
View File
@@ -0,0 +1,166 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use bincode::Options;
use partial_default::PartialDefault;
use serde::{Deserialize, Serialize};
use crate::ZkGroupDeserializationFailure;
fn zkgroup_bincode_options() -> impl bincode::Options {
bincode::DefaultOptions::new()
.with_fixint_encoding()
.with_little_endian()
.reject_trailing_bytes()
}
/// Deserializes a type using the standard zkgroup encoding (based on bincode).
///
/// The type must support [`PartialDefault`] to save on code size.
pub fn deserialize<'a, T: Deserialize<'a> + PartialDefault>(
bytes: &'a [u8],
) -> Result<T, ZkGroupDeserializationFailure> {
let mut result = T::partial_default();
// Use the same encoding options as plain bincode::deserialize, which we used historically,
// but also reject trailing bytes.
// See https://docs.rs/bincode/1.3.3/bincode/config/index.html#options-struct-vs-bincode-functions.
T::deserialize_in_place(
&mut bincode::Deserializer::from_slice(bytes, zkgroup_bincode_options()),
&mut result,
)
.map_err(|_| ZkGroupDeserializationFailure::new::<T>())?;
Ok(result)
}
/// Serializes a type using the standard zkgroup encoding (based on bincode).
pub fn serialize<T: Serialize>(value: &T) -> Vec<u8> {
zkgroup_bincode_options()
.serialize(value)
.expect("cannot fail")
}
/// Constant version number `C` as a type.
///
/// Zero-sized type that converts to and from for the value `C` via `Into`,
/// `TryFrom`, [`Serialize`], and [`Deserialize`]. Used for providing a version
/// tag at the beginning of serialized structs.
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub struct VersionByte<const C: u8>;
impl<const C: u8> From<VersionByte<C>> for u8 {
fn from(VersionByte: VersionByte<C>) -> Self {
C
}
}
/// version byte was {found}, not {EXPECTED:?}
#[derive(Copy, Clone, Debug, Eq, PartialEq, displaydoc::Display)]
pub struct VersionMismatchError<const EXPECTED: u8> {
found: u8,
}
impl<const C: u8> TryFrom<u8> for VersionByte<C> {
type Error = VersionMismatchError<C>;
fn try_from(value: u8) -> Result<Self, Self::Error> {
(value == C)
.then_some(VersionByte::<C>)
.ok_or(VersionMismatchError::<C> { found: value })
}
}
impl<const C: u8> Serialize for VersionByte<C> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
u8::serialize(&C, serializer)
}
}
impl<'de, const C: u8> Deserialize<'de> for VersionByte<C> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let v = u8::deserialize(deserializer)?;
v.try_into().map_err(|_| {
<D::Error as serde::de::Error>::invalid_value(
serde::de::Unexpected::Unsigned(v.into()),
&format!("version `{C}`").as_str(),
)
})
}
}
/// Value that always serializes to and from `0u8`.
pub type ReservedByte = VersionByte<0>;
#[cfg(test)]
mod test {
use std::fmt::Debug;
use test_case::test_case;
use super::*;
#[derive(Debug, Serialize, Deserialize, PartialEq, PartialDefault)]
struct WithLeadingByte<T> {
leading: T,
string: String,
}
impl<T: Default> WithLeadingByte<T> {
fn test_value() -> Self {
Self {
leading: T::default(),
string: "a string".to_string(),
}
}
}
type WithReservedByte = WithLeadingByte<ReservedByte>;
type WithVersionByte = WithLeadingByte<VersionByte<42>>;
#[test_case(WithReservedByte::test_value(), 0)]
#[test_case(WithVersionByte::test_value(), 42)]
fn round_trip<T: Serialize + for<'a> Deserialize<'a> + PartialEq + PartialDefault + Debug>(
test_value: T,
expected_first_byte: u8,
) {
let serialized = crate::serialize(&test_value);
assert_eq!(serialized[0], expected_first_byte);
let deserialized: T = crate::deserialize(&serialized).expect("can deserialize");
assert_eq!(deserialized, test_value);
}
#[test_case(WithReservedByte::test_value())]
#[test_case(WithVersionByte::test_value())]
fn version_byte_wrong<
T: Serialize + for<'a> Deserialize<'a> + PartialEq + PartialDefault + Debug,
>(
test_value: T,
) {
let mut serialized = crate::serialize(&test_value);
// perturb the first byte.
serialized[0] += 1;
crate::deserialize::<T>(&serialized).expect_err("invalid version");
}
#[test]
fn version_byte_error_message() {
let mut bincode_serialized =
bincode::serialize(&WithVersionByte::test_value()).expect("should serialize");
bincode_serialized[0] = 41;
let error_message =
bincode::deserialize::<WithVersionByte>(&bincode_serialized).expect_err("should fail");
assert_eq!(
error_message.to_string(),
"invalid value: integer `41`, expected version `42`"
);
}
}
+63
View File
@@ -0,0 +1,63 @@
//
// Copyright 2020 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use curve25519_dalek_signal::ristretto::RistrettoPoint;
use curve25519_dalek_signal::scalar::Scalar;
use poksho::ShoApi;
use poksho::shoapi::ShoApiExt as _;
#[derive(Clone)]
pub struct Sho {
internal_sho: poksho::ShoHmacSha256,
}
impl Sho {
/// Creates a Sho and immediately absorbs `data` and ratchets.
///
/// This is exactly equivalent to calling [`Sho::new_seed`] followed by
/// [`Sho::absorb_and_ratchet`]. Meant for when you're not doing any other absorbing.
pub fn new(label: &[u8], data: &[u8]) -> Self {
let mut sho = Self::new_seed(label);
sho.absorb_and_ratchet(data);
sho
}
pub fn new_seed(label: &[u8]) -> Self {
let sho = poksho::ShoHmacSha256::new(label);
Sho { internal_sho: sho }
}
pub fn absorb_and_ratchet(&mut self, data: &[u8]) {
self.internal_sho.absorb_and_ratchet(data)
}
pub fn squeeze(&mut self, outlen: usize) -> Vec<u8> {
self.internal_sho.squeeze_and_ratchet(outlen)
}
pub fn squeeze_as_array<const N: usize>(&mut self) -> [u8; N] {
self.internal_sho.squeeze_and_ratchet_as_array()
}
pub fn get_point(&mut self) -> RistrettoPoint {
RistrettoPoint::from_uniform_bytes(&self.internal_sho.squeeze_and_ratchet_as_array())
}
pub fn get_point_single_elligator(&mut self) -> RistrettoPoint {
RistrettoPoint::from_uniform_bytes_single_elligator(
&self.internal_sho.squeeze_and_ratchet_as_array(),
)
}
pub fn get_scalar(&mut self) -> Scalar {
Scalar::from_bytes_mod_order_wide(&self.internal_sho.squeeze_and_ratchet_as_array())
}
}
impl AsMut<poksho::ShoHmacSha256> for Sho {
fn as_mut(&mut self) -> &mut poksho::ShoHmacSha256 {
&mut self.internal_sho
}
}
+147
View File
@@ -0,0 +1,147 @@
//
// Copyright 2020 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use curve25519_dalek_signal::scalar::Scalar;
use partial_default::PartialDefault;
use serde::{Deserialize, Serialize};
use zkcredential::attributes::PublicAttribute;
use crate::common::constants::*;
pub type AesKeyBytes = [u8; AES_KEY_LEN];
pub type GroupMasterKeyBytes = [u8; GROUP_MASTER_KEY_LEN];
pub type UidBytes = [u8; UUID_LEN];
pub type ProfileKeyBytes = [u8; PROFILE_KEY_LEN];
pub type RandomnessBytes = [u8; RANDOMNESS_LEN];
pub type SignatureBytes = [u8; SIGNATURE_LEN];
pub type NotarySignatureBytes = [u8; SIGNATURE_LEN];
pub type GroupIdentifierBytes = [u8; GROUP_IDENTIFIER_LEN];
pub type ProfileKeyVersionBytes = [u8; PROFILE_KEY_VERSION_LEN];
// TODO: Use ascii::Char when stable (the "encoding" is hex)
pub type ProfileKeyVersionEncodedBytes = [u8; PROFILE_KEY_VERSION_ENCODED_LEN];
// A random UUID that the receipt issuing server will blind authorize to redeem a given receipt
// level within a certain time frame.
pub type ReceiptSerialBytes = [u8; RECEIPT_SERIAL_LEN];
/// Timestamp measured in seconds past the epoch.
///
/// Clients should only accept round multiples of 86400 to avoid fingerprinting by the server.
/// For expirations, the timestamp should be within a couple of days into the future;
/// for redemption times, it should be within a day of the current date.
#[derive(
Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize, PartialDefault,
)]
#[serde(transparent)]
#[repr(transparent)]
pub struct Timestamp(u64);
impl Timestamp {
#[inline]
pub const fn from_epoch_seconds(seconds: u64) -> Self {
Self(seconds)
}
#[inline]
pub const fn epoch_seconds(&self) -> u64 {
self.0
}
#[inline]
pub const fn add_seconds(&self, seconds: u64) -> Self {
Self(self.0 + seconds)
}
#[inline]
pub const fn sub_seconds(&self, seconds: u64) -> Self {
Self(self.0 - seconds)
}
#[inline]
pub fn checked_add_seconds(&self, seconds: u64) -> Option<Self> {
self.0.checked_add(seconds).map(Self)
}
#[inline]
pub fn checked_sub_seconds(&self, seconds: u64) -> Option<Self> {
self.0.checked_sub(seconds).map(Self)
}
#[inline]
pub const fn is_day_aligned(&self) -> bool {
self.0 % SECONDS_PER_DAY == 0
}
#[inline]
pub fn to_be_bytes(self) -> [u8; 8] {
self.0.to_be_bytes()
}
/// Number of seconds that `self` is after `before`.
///
/// Returns `0` if `self` is equal to or earlier than `before`.
pub(crate) fn saturating_seconds_since(&self, before: Timestamp) -> u64 {
self.0.saturating_sub(before.0)
}
}
impl From<Timestamp> for std::time::SystemTime {
fn from(Timestamp(seconds): Timestamp) -> Self {
std::time::UNIX_EPOCH + std::time::Duration::from_secs(seconds)
}
}
impl From<std::time::SystemTime> for Timestamp {
fn from(timestamp: std::time::SystemTime) -> Self {
Self::from_epoch_seconds(
timestamp
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
)
}
}
impl rand::distr::Distribution<Timestamp> for rand::distr::StandardUniform {
fn sample<R: rand::prelude::Rng + ?Sized>(&self, rng: &mut R) -> Timestamp {
Timestamp(Self::sample(self, rng))
}
}
impl PublicAttribute for Timestamp {
fn hash_into(&self, sho: &mut dyn poksho::ShoApi) {
self.0.hash_into(sho)
}
}
// Used to tell the server handling receipt redemptions what to redeem the receipt for. Clients
// should validate this matches their expectations.
pub type ReceiptLevel = u64;
pub fn encode_redemption_time(redemption_time: u32) -> Scalar {
let mut scalar_bytes: [u8; 32] = Default::default();
scalar_bytes[0..4].copy_from_slice(&redemption_time.to_be_bytes());
Scalar::from_bytes_mod_order(scalar_bytes)
}
pub fn encode_receipt_serial_bytes(receipt_serial_bytes: ReceiptSerialBytes) -> Scalar {
let mut scalar_bytes: [u8; 32] = Default::default();
scalar_bytes[0..16].copy_from_slice(&receipt_serial_bytes[..]);
Scalar::from_bytes_mod_order(scalar_bytes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_scalar() {
let s_bytes = [0xFF; 32];
match bincode::deserialize::<Scalar>(&s_bytes) {
Err(_) => (),
Ok(_) => unreachable!(),
}
}
}
+509
View File
@@ -0,0 +1,509 @@
//
// Copyright 2020-2022 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
#![allow(non_snake_case)]
use std::sync::LazyLock;
use const_str::hex;
use curve25519_dalek_signal::constants::RISTRETTO_BASEPOINT_POINT;
use curve25519_dalek_signal::ristretto::RistrettoPoint;
use curve25519_dalek_signal::scalar::Scalar;
use derive_where::derive_where;
use partial_default::PartialDefault;
use serde::{Deserialize, Serialize};
use crate::common::array_utils::{ArrayLike, OneBased};
use crate::common::sho::*;
use crate::common::simple_types::*;
use crate::crypto::receipt_struct::ReceiptStruct;
use crate::crypto::timestamp_struct::TimestampStruct;
use crate::crypto::{
profile_key_credential_request, receipt_credential_request, receipt_struct, uid_struct,
};
use crate::{
NUM_AUTH_CRED_ATTRIBUTES, NUM_PROFILE_KEY_CRED_ATTRIBUTES, NUM_RECEIPT_CRED_ATTRIBUTES,
};
static SYSTEM_PARAMS: LazyLock<SystemParams> = LazyLock::new(|| {
crate::deserialize(SystemParams::SYSTEM_HARDCODED).expect("valid hardcoded params")
});
const NUM_SUPPORTED_ATTRS: usize = 6;
#[derive(Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SystemParams {
pub(crate) G_w: RistrettoPoint,
pub(crate) G_wprime: RistrettoPoint,
pub(crate) G_x0: RistrettoPoint,
pub(crate) G_x1: RistrettoPoint,
pub(crate) G_y: OneBased<[RistrettoPoint; NUM_SUPPORTED_ATTRS]>,
pub(crate) G_m1: RistrettoPoint,
pub(crate) G_m2: RistrettoPoint,
pub(crate) G_m3: RistrettoPoint,
pub(crate) G_m4: RistrettoPoint,
pub(crate) G_m5: RistrettoPoint,
pub(crate) G_V: RistrettoPoint,
pub(crate) G_z: RistrettoPoint,
}
/// Used to specialize a [`KeyPair<S>`] to support a certain number of attributes.
///
/// The only required member is `Storage`, which should be a fixed-size array of [`Scalar`], one for
/// each attribute. However, for backwards compatibility some systems support fewer attributes than
/// are actually stored, and in this case the `NUM_ATTRS` member can be set to a custom value. Note
/// that `NUM_ATTRS` must always be less than or equal to the number of elements in `Storage`.
pub trait AttrScalars {
/// The storage (should be a fixed-size array of Scalar).
type Storage: ArrayLike<Scalar> + Copy + Eq + Serialize + for<'a> Deserialize<'a>;
/// The number of attributes supported in this system.
///
/// Defaults to the full set stored in `Self::Storage`.
const NUM_ATTRS: usize = Self::Storage::LEN;
}
impl AttrScalars for AuthCredential {
// Store four scalars for backwards compatibility.
type Storage = [Scalar; 4];
const NUM_ATTRS: usize = NUM_AUTH_CRED_ATTRIBUTES;
}
impl AttrScalars for AuthCredentialWithPni {
type Storage = [Scalar; 5];
}
impl AttrScalars for ProfileKeyCredential {
// Store four scalars for backwards compatibility.
type Storage = [Scalar; 4];
const NUM_ATTRS: usize = NUM_PROFILE_KEY_CRED_ATTRIBUTES;
}
impl AttrScalars for ExpiringProfileKeyCredential {
type Storage = [Scalar; 5];
}
impl AttrScalars for ReceiptCredential {
// Store four scalars for backwards compatibility.
type Storage = [Scalar; 4];
const NUM_ATTRS: usize = NUM_RECEIPT_CRED_ATTRIBUTES;
}
impl AttrScalars for PniCredential {
type Storage = [Scalar; 6];
}
#[derive(Serialize, Deserialize, PartialDefault)]
#[partial_default(bound = "S::Storage: Default")]
#[derive_where(Clone, Copy, PartialEq, Eq; S: AttrScalars)]
pub struct KeyPair<S: AttrScalars> {
// private
pub(crate) w: Scalar,
pub(crate) wprime: Scalar,
pub(crate) W: RistrettoPoint,
pub(crate) x0: Scalar,
pub(crate) x1: Scalar,
pub(crate) y: OneBased<S::Storage>,
// public
pub(crate) C_W: RistrettoPoint,
pub(crate) I: RistrettoPoint,
}
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialDefault)]
pub struct PublicKey {
pub(crate) C_W: RistrettoPoint,
pub(crate) I: RistrettoPoint,
}
/// Unused, kept only because ServerSecretParams contains a `KeyPair<AuthCredential>`.
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialDefault)]
pub(crate) struct AuthCredential {
pub(crate) t: Scalar,
pub(crate) U: RistrettoPoint,
pub(crate) V: RistrettoPoint,
}
/// Unused, kept only because ServerSecretParams contains a `KeyPair<AuthCredentialWithPni>`.
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialDefault)]
pub(crate) struct AuthCredentialWithPni {
pub(crate) t: Scalar,
pub(crate) U: RistrettoPoint,
pub(crate) V: RistrettoPoint,
}
/// Unused, kept only because ServerSecretParams contains a `KeyPair<ProfileKeyCredential>`.
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProfileKeyCredential {
pub(crate) t: Scalar,
pub(crate) U: RistrettoPoint,
pub(crate) V: RistrettoPoint,
}
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialDefault)]
pub struct ExpiringProfileKeyCredential {
pub(crate) t: Scalar,
pub(crate) U: RistrettoPoint,
pub(crate) V: RistrettoPoint,
}
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BlindedExpiringProfileKeyCredentialWithSecretNonce {
pub(crate) rprime: Scalar,
pub(crate) t: Scalar,
pub(crate) U: RistrettoPoint,
pub(crate) S1: RistrettoPoint,
pub(crate) S2: RistrettoPoint,
}
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialDefault)]
pub struct BlindedExpiringProfileKeyCredential {
pub(crate) t: Scalar,
pub(crate) U: RistrettoPoint,
pub(crate) S1: RistrettoPoint,
pub(crate) S2: RistrettoPoint,
}
/// Unused, kept only because ServerSecretParams contains a `KeyPair<PniCredential>`.
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PniCredential {
pub(crate) t: Scalar,
pub(crate) U: RistrettoPoint,
pub(crate) V: RistrettoPoint,
}
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialDefault)]
pub struct ReceiptCredential {
pub(crate) t: Scalar,
pub(crate) U: RistrettoPoint,
pub(crate) V: RistrettoPoint,
}
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BlindedReceiptCredentialWithSecretNonce {
pub(crate) rprime: Scalar,
pub(crate) t: Scalar,
pub(crate) U: RistrettoPoint,
pub(crate) S1: RistrettoPoint,
pub(crate) S2: RistrettoPoint,
}
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialDefault)]
pub struct BlindedReceiptCredential {
pub(crate) t: Scalar,
pub(crate) U: RistrettoPoint,
pub(crate) S1: RistrettoPoint,
pub(crate) S2: RistrettoPoint,
}
pub(crate) fn convert_to_points_receipt_struct(
receipt: receipt_struct::ReceiptStruct,
) -> Vec<RistrettoPoint> {
let system = SystemParams::get_hardcoded();
let m1 = receipt.calc_m1();
let receipt_serial_scalar = encode_receipt_serial_bytes(receipt.receipt_serial_bytes);
vec![m1 * system.G_m1, receipt_serial_scalar * system.G_m2]
}
pub(crate) fn convert_to_point_M2_receipt_serial_bytes(
receipt_serial_bytes: ReceiptSerialBytes,
) -> RistrettoPoint {
let system = SystemParams::get_hardcoded();
let receipt_serial_scalar = encode_receipt_serial_bytes(receipt_serial_bytes);
receipt_serial_scalar * system.G_m2
}
impl SystemParams {
#[cfg(test)]
fn generate() -> Self {
let mut sho = Sho::new(
b"Signal_ZKGroup_20200424_Constant_Credentials_SystemParams_Generate",
b"",
);
let G_w = sho.get_point();
let G_wprime = sho.get_point();
let G_x0 = sho.get_point();
let G_x1 = sho.get_point();
let G_y1 = sho.get_point();
let G_y2 = sho.get_point();
let G_y3 = sho.get_point();
let G_y4 = sho.get_point();
let G_m1 = sho.get_point();
let G_m2 = sho.get_point();
let G_m3 = sho.get_point();
let G_m4 = sho.get_point();
let G_V = sho.get_point();
let G_z = sho.get_point();
// We don't ever want to use existing generator points in new ways,
// so new points have to be added at the end.
let G_y5 = sho.get_point();
let G_y6 = sho.get_point();
let G_m5 = sho.get_point();
SystemParams {
G_w,
G_wprime,
G_x0,
G_x1,
G_y: OneBased([G_y1, G_y2, G_y3, G_y4, G_y5, G_y6]),
G_m1,
G_m2,
G_m3,
G_m4,
G_m5,
G_V,
G_z,
}
}
pub fn get_hardcoded() -> SystemParams {
*SYSTEM_PARAMS
}
const SYSTEM_HARDCODED: &'static [u8] = &hex!(
"9ae7c8e5ed779b114ae7708aa2f794670adda324987b659913122c35505b105e6ca31025d2d76be7fd34944f98f7fa0e37babb2c8b98bbbdbd3dd1bf130cca2c8a9a3bdfaaa2b6b322d46b93eca7b0d51c86a3c839e11466358258a6c10c577fc2bffd34cd99164c9a6cd29fab55d91ff9269322ec3458603cc96a0d47f704058288f62ee0acedb8aa23242121d98965a9bb2991250c11758095ece0fd2b33285286fe1fcb056103b6081744b975f550d08521568dd3d8618f25c140375a0f4024c3aa23bdfffb27fbd982208d3ecd1fd3bcb7ac0c3a14b109804fc748d7fa456cffb4934f980b6e09a248a60f44a6150ae6c13d7e3c06261d7e4eed37f39f60b04dd9d607fd357012274d3c63dbb38e7378599c9e97dfbb28842694891d5f0ddc729919b798b4131503408cc57a9c532f4427632c88f54cea53861a5bc44c61cc6037dc31c2e8d4474fb519587a448693182ad9d6d86b535957858f547b9340127da75f8074caee944ac36c0ac662d38c9b3ccce03a093fcd9644047398b86b6e83372ff14fb8bb0dea65531252ac70d58a4a0810d682a0e709c9227b30ef6c8e17c5915d527221bb00da8175cd6489aa8aa492a500f9abee5690b9dfca8855dc0bd02a7f277add240f639ac16801e81574afb4683edff63b9a01e93dbd867a04b616c706c80c756c11a3016bbfb60977f4648b5f2395a4b428b7211940813e3afde2b87aa9c2c37bf716e2578f95656df12c2fb6f5d0631f6f71e2c3193f6d"
);
}
impl<S: AttrScalars> KeyPair<S> {
pub fn generate(sho: &mut Sho) -> Self {
assert!(S::NUM_ATTRS >= 1, "at least one attribute required");
assert!(
S::NUM_ATTRS <= NUM_SUPPORTED_ATTRS,
"more than {NUM_SUPPORTED_ATTRS} attributes not supported"
);
assert!(
S::NUM_ATTRS <= S::Storage::LEN,
"more attributes than storage",
);
let system = SystemParams::get_hardcoded();
let w = sho.get_scalar();
let W = w * system.G_w;
let wprime = sho.get_scalar();
let x0 = sho.get_scalar();
let x1 = sho.get_scalar();
let y = OneBased::<S::Storage>::create(|| sho.get_scalar());
let C_W = (w * system.G_w) + (wprime * system.G_wprime);
let mut I = system.G_V - (x0 * system.G_x0) - (x1 * system.G_x1);
for (yn, G_yn) in y.iter().zip(system.G_y.iter()).take(S::NUM_ATTRS) {
I -= yn * G_yn;
}
KeyPair {
w,
wprime,
W,
x0,
x1,
y,
C_W,
I,
}
}
pub fn get_public_key(&self) -> PublicKey {
PublicKey {
C_W: self.C_W,
I: self.I,
}
}
fn credential_core(
&self,
M: &[RistrettoPoint],
sho: &mut Sho,
) -> (Scalar, RistrettoPoint, RistrettoPoint) {
assert!(
M.len() <= S::NUM_ATTRS,
"more than {} attributes not supported",
S::NUM_ATTRS
);
let t = sho.get_scalar();
let U = sho.get_point();
let mut V = self.W + (self.x0 + self.x1 * t) * U;
for (yn, Mn) in self.y.iter().zip(M) {
V += yn * Mn;
}
(t, U, V)
}
}
impl KeyPair<ExpiringProfileKeyCredential> {
pub fn create_blinded_expiring_profile_key_credential(
&self,
uid: uid_struct::UidStruct,
public_key: profile_key_credential_request::PublicKey,
ciphertext: profile_key_credential_request::Ciphertext,
credential_expiration_time: Timestamp,
sho: &mut Sho,
) -> BlindedExpiringProfileKeyCredentialWithSecretNonce {
let M = [uid.M1, uid.M2];
let (t, U, Vprime) = self.credential_core(&M, sho);
let params = SystemParams::get_hardcoded();
let m5 = TimestampStruct::calc_m_from(credential_expiration_time);
let M5 = m5 * params.G_m5;
let Vprime_with_expiration = Vprime + (self.y[5] * M5);
let rprime = sho.get_scalar();
let R1 = rprime * RISTRETTO_BASEPOINT_POINT;
let R2 = rprime * public_key.Y + Vprime_with_expiration;
let S1 = R1 + (self.y[3] * ciphertext.D1) + (self.y[4] * ciphertext.E1);
let S2 = R2 + (self.y[3] * ciphertext.D2) + (self.y[4] * ciphertext.E2);
BlindedExpiringProfileKeyCredentialWithSecretNonce {
rprime,
t,
U,
S1,
S2,
}
}
}
impl KeyPair<ReceiptCredential> {
pub fn create_blinded_receipt_credential(
&self,
public_key: receipt_credential_request::PublicKey,
ciphertext: receipt_credential_request::Ciphertext,
receipt_expiration_time: Timestamp,
receipt_level: ReceiptLevel,
sho: &mut Sho,
) -> BlindedReceiptCredentialWithSecretNonce {
let params = SystemParams::get_hardcoded();
let m1 = ReceiptStruct::calc_m1_from(receipt_expiration_time, receipt_level);
let M = [m1 * params.G_m1];
let (t, U, Vprime) = self.credential_core(&M, sho);
let rprime = sho.get_scalar();
let R1 = rprime * RISTRETTO_BASEPOINT_POINT;
let R2 = rprime * public_key.Y + Vprime;
let S1 = self.y[2] * ciphertext.D1 + R1;
let S2 = self.y[2] * ciphertext.D2 + R2;
BlindedReceiptCredentialWithSecretNonce {
rprime,
t,
U,
S1,
S2,
}
}
}
impl BlindedExpiringProfileKeyCredentialWithSecretNonce {
pub fn get_blinded_expiring_profile_key_credential(
&self,
) -> BlindedExpiringProfileKeyCredential {
BlindedExpiringProfileKeyCredential {
t: self.t,
U: self.U,
S1: self.S1,
S2: self.S2,
}
}
}
impl BlindedReceiptCredentialWithSecretNonce {
pub fn get_blinded_receipt_credential(&self) -> BlindedReceiptCredential {
BlindedReceiptCredential {
t: self.t,
U: self.U,
S1: self.S1,
S2: self.S2,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::constants::*;
use crate::crypto::proofs;
#[test]
fn test_system() {
let params = SystemParams::generate();
println!("PARAMS = {:#x?}", bincode::serialize(&params));
assert!(SystemParams::generate() == SystemParams::get_hardcoded());
}
#[test]
fn test_mac() {
// It doesn't really matter *which* credential we test here, we just want to generally know
// we've set things up correctly. (Also, the credentials hardcoded here in zkgroup may
// eventually all be superseded by implementations using zkcredential, at which point this
// test can be deleted.)
let mut sho = Sho::new(b"Test_Credentials", b"");
let keypair = KeyPair::<ExpiringProfileKeyCredential>::generate(&mut sho);
let uid_bytes = TEST_ARRAY_16;
let redemption_time = Timestamp::from_epoch_seconds(37 * SECONDS_PER_DAY);
let aci = libsignal_core::Aci::from_uuid_bytes(uid_bytes);
let aci_struct = uid_struct::UidStruct::from_service_id(aci.into());
let profile_key_struct = crate::crypto::profile_key_struct::ProfileKeyStruct::new(
[1; PROFILE_KEY_LEN],
uid_bytes,
);
let request_key_pair =
crate::crypto::profile_key_credential_request::KeyPair::generate(&mut sho);
let ciphertext = request_key_pair
.encrypt(profile_key_struct, &mut sho)
.get_ciphertext();
let credential = keypair.create_blinded_expiring_profile_key_credential(
aci_struct,
request_key_pair.get_public_key(),
ciphertext,
redemption_time,
&mut sho,
);
let proof = proofs::ExpiringProfileKeyCredentialIssuanceProof::new(
keypair,
request_key_pair.get_public_key(),
ciphertext,
credential,
aci_struct,
redemption_time,
&mut sho,
);
let public_key = keypair.get_public_key();
proof
.verify(
public_key,
request_key_pair.get_public_key(),
uid_bytes,
ciphertext,
credential.get_blinded_expiring_profile_key_credential(),
redemption_time,
)
.unwrap();
let keypair_bytes = bincode::serialize(&keypair).unwrap();
let keypair2 = bincode::deserialize(&keypair_bytes).unwrap();
assert!(keypair == keypair2);
let public_key_bytes = bincode::serialize(&public_key).unwrap();
let public_key2 = bincode::deserialize(&public_key_bytes).unwrap();
assert!(public_key == public_key2);
let mac_bytes = bincode::serialize(&credential).unwrap();
println!("mac_bytes = {}", hex::encode(&mac_bytes));
assert_eq!(
mac_bytes,
hex!(
"ef47110715831160100f14d1936f4349c45b80ccaacd4edd9f949375d2d90a090888d81f8b0ed313
808b5ff7ec1957ed4e8b3d9c195b3a5abdbdd3d972c29809100a7f8dc2354be7a1d44452cbadd87e
4851ae05ebeb2586b856d35af765883a94473ad855df8583be2930e4e1d5756175a9091f2be1d8d0
21280446a7611841d6b4f2eb165267a9d1d7a800f19c2077a4ef7df721b160fe200181be3c455f1c"
)
);
}
}
@@ -0,0 +1,215 @@
//
// Copyright 2020 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
#![allow(non_snake_case)]
use std::sync::LazyLock;
use curve25519_dalek_signal::ristretto::RistrettoPoint;
use partial_default::PartialDefault;
use serde::{Deserialize, Serialize};
use subtle::{Choice, ConditionallySelectable, ConstantTimeEq};
use zkcredential::attributes::Attribute;
use crate::common::errors::*;
use crate::common::sho::*;
use crate::common::simple_types::*;
use crate::crypto::profile_key_struct;
static SYSTEM_PARAMS: LazyLock<SystemParams> = LazyLock::new(|| {
crate::deserialize(&SystemParams::SYSTEM_HARDCODED).expect("valid hardcoded params")
});
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialDefault)]
pub struct SystemParams {
pub(crate) G_b1: RistrettoPoint,
pub(crate) G_b2: RistrettoPoint,
}
pub type KeyPair = zkcredential::attributes::KeyPair<ProfileKeyEncryptionDomain>;
pub type PublicKey = zkcredential::attributes::PublicKey<ProfileKeyEncryptionDomain>;
pub type Ciphertext = zkcredential::attributes::Ciphertext<ProfileKeyEncryptionDomain>;
impl SystemParams {
pub fn generate() -> Self {
let mut sho = Sho::new(
b"Signal_ZKGroup_20200424_Constant_ProfileKeyEncryption_SystemParams_Generate",
b"",
);
let G_b1 = sho.get_point();
let G_b2 = sho.get_point();
SystemParams { G_b1, G_b2 }
}
pub fn get_hardcoded() -> SystemParams {
*SYSTEM_PARAMS
}
const SYSTEM_HARDCODED: [u8; 64] = [
0xf6, 0xba, 0xa3, 0x17, 0xce, 0x18, 0x39, 0xc9, 0x3d, 0x61, 0x7e, 0xc, 0xd8, 0x37, 0xd1,
0x9d, 0xa9, 0xc8, 0xa4, 0xc5, 0x20, 0xbf, 0x7c, 0x51, 0xb1, 0xe6, 0xc2, 0xcb, 0x2a, 0x4,
0x9c, 0x61, 0x2e, 0x1, 0x75, 0x89, 0x4c, 0x87, 0x30, 0xb2, 0x3, 0xab, 0x3b, 0xd9, 0x8e,
0xcb, 0x2d, 0x81, 0xab, 0xac, 0xb6, 0x5f, 0x8a, 0x61, 0x24, 0xf4, 0x97, 0x71, 0xd1, 0x4a,
0x98, 0x52, 0x12, 0xc,
];
}
pub struct ProfileKeyEncryptionDomain;
impl zkcredential::attributes::Domain for ProfileKeyEncryptionDomain {
type Attribute = profile_key_struct::ProfileKeyStruct;
const ID: &'static str = "Signal_ZKGroup_20231011_ProfileKeyEncryption";
fn G_a() -> [RistrettoPoint; 2] {
let system = SystemParams::get_hardcoded();
[system.G_b1, system.G_b2]
}
}
impl ProfileKeyEncryptionDomain {
pub(crate) fn decrypt(
key_pair: &KeyPair,
ciphertext: &Ciphertext,
uid_bytes: UidBytes,
) -> Result<profile_key_struct::ProfileKeyStruct, ZkGroupVerificationFailure> {
let M4 = key_pair
.decrypt_to_second_point(ciphertext)
.map_err(|_| ZkGroupVerificationFailure)?;
let (mask, candidates) = M4.decode_253_bits();
let target_M3 = key_pair.a1.invert() * ciphertext.as_points()[0];
let seed_sho = profile_key_struct::ProfileKeyStruct::seed_M3();
let mut retval: profile_key_struct::ProfileKeyStruct = PartialDefault::partial_default();
let mut n_found = 0;
#[allow(clippy::needless_range_loop)]
for i in 0..8 {
let is_valid_fe = Choice::from((mask >> i) & 1);
let profile_key_bytes: ProfileKeyBytes = candidates[i];
for j in 0..8 {
let mut pk = profile_key_bytes;
if ((j >> 2) & 1) == 1 {
pk[0] |= 0x01;
}
if ((j >> 1) & 1) == 1 {
pk[31] |= 0x80;
}
if (j & 1) == 1 {
pk[31] |= 0x40;
}
let M3 =
profile_key_struct::ProfileKeyStruct::calc_M3(seed_sho.clone(), pk, uid_bytes);
let candidate_retval = profile_key_struct::ProfileKeyStruct { bytes: pk, M3, M4 };
let found = M3.ct_eq(&target_M3) & is_valid_fe;
retval.conditional_assign(&candidate_retval, found);
n_found += found.unwrap_u8();
}
}
if n_found == 1 {
Ok(retval)
} else {
Err(ZkGroupVerificationFailure)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::constants::*;
#[test]
fn test_profile_key_encryption() {
let master_key = TEST_ARRAY_32_1;
let mut sho = Sho::new(b"Test_Profile_Key_Encryption", &master_key);
//let system = SystemParams::generate();
//println!("PARAMS = {:#x?}", bincode::serialize(&system));
assert!(SystemParams::generate() == SystemParams::get_hardcoded());
let key_pair = KeyPair::derive_from(sho.as_mut());
// Test serialize of key_pair
let key_pair_bytes = bincode::serialize(&key_pair).unwrap();
match bincode::deserialize::<KeyPair>(&key_pair_bytes[0..key_pair_bytes.len() - 1]) {
Err(_) => (),
_ => unreachable!(),
};
let key_pair2: KeyPair = bincode::deserialize(&key_pair_bytes).unwrap();
assert!(key_pair == key_pair2);
let profile_key_bytes = TEST_ARRAY_32_1;
let uid_bytes = TEST_ARRAY_16_1;
let profile_key = profile_key_struct::ProfileKeyStruct::new(profile_key_bytes, uid_bytes);
let ciphertext = key_pair.encrypt(&profile_key);
// Test serialize / deserialize of Ciphertext
let ciphertext_bytes = bincode::serialize(&ciphertext).unwrap();
assert!(ciphertext_bytes.len() == 64);
let ciphertext2: Ciphertext = bincode::deserialize(&ciphertext_bytes).unwrap();
assert!(ciphertext == ciphertext2);
println!("ciphertext_bytes = {ciphertext_bytes:#x?}");
assert!(
ciphertext_bytes
== vec![
0x56, 0x18, 0xcb, 0x4c, 0x7d, 0x72, 0x1e, 0x1, 0x2b, 0x22, 0xf0, 0x77, 0xef,
0x12, 0x64, 0xf6, 0xb1, 0x43, 0xbb, 0x59, 0x7a, 0x1d, 0x66, 0x5a, 0x70, 0xaa,
0x84, 0x24, 0x5f, 0x24, 0x6d, 0x20, 0xba, 0xdb, 0x97, 0x47, 0x4a, 0x56, 0xf4,
0xb5, 0x36, 0x1a, 0xec, 0xa9, 0xd1, 0x18, 0xb7, 0x0, 0x4e, 0x14, 0x9, 0x71,
0x99, 0xa, 0xab, 0x2a, 0xf2, 0x43, 0x2d, 0x3f, 0x8f, 0x7d, 0x21, 0x3a,
]
);
let plaintext =
ProfileKeyEncryptionDomain::decrypt(&key_pair, &ciphertext2, uid_bytes).unwrap();
assert!(plaintext == profile_key);
let mut sho = Sho::new(b"Test_Repeated_ProfileKeyEnc/Dec", b"seed");
for _ in 0..100 {
let uid_bytes: UidBytes = sho.squeeze_as_array();
let profile_key_bytes: ProfileKeyBytes = sho.squeeze_as_array();
let profile_key =
profile_key_struct::ProfileKeyStruct::new(profile_key_bytes, uid_bytes);
let ciphertext = key_pair.encrypt(&profile_key);
assert!(
ProfileKeyEncryptionDomain::decrypt(&key_pair, &ciphertext, uid_bytes).unwrap()
== profile_key
);
}
let uid_bytes = TEST_ARRAY_16;
let profile_key = profile_key_struct::ProfileKeyStruct::new(TEST_ARRAY_32, TEST_ARRAY_16);
let ciphertext = key_pair.encrypt(&profile_key);
assert!(
ProfileKeyEncryptionDomain::decrypt(&key_pair, &ciphertext, uid_bytes).unwrap()
== profile_key
);
let uid_bytes = TEST_ARRAY_16;
let profile_key = profile_key_struct::ProfileKeyStruct::new(TEST_ARRAY_32_2, TEST_ARRAY_16);
let ciphertext = key_pair.encrypt(&profile_key);
assert!(
ProfileKeyEncryptionDomain::decrypt(&key_pair, &ciphertext, uid_bytes).unwrap()
== profile_key
);
let uid_bytes = TEST_ARRAY_16;
let profile_key = profile_key_struct::ProfileKeyStruct::new(TEST_ARRAY_32_3, TEST_ARRAY_16);
let ciphertext = key_pair.encrypt(&profile_key);
assert!(
ProfileKeyEncryptionDomain::decrypt(&key_pair, &ciphertext, uid_bytes).unwrap()
== profile_key
);
let uid_bytes = TEST_ARRAY_16;
let profile_key = profile_key_struct::ProfileKeyStruct::new(TEST_ARRAY_32_4, TEST_ARRAY_16);
let ciphertext = key_pair.encrypt(&profile_key);
assert!(
ProfileKeyEncryptionDomain::decrypt(&key_pair, &ciphertext, uid_bytes).unwrap()
== profile_key
);
}
}
@@ -0,0 +1,87 @@
//
// Copyright 2020 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
#![allow(non_snake_case)]
use curve25519_dalek_signal::ristretto::RistrettoPoint;
use partial_default::PartialDefault;
use serde::{Deserialize, Serialize};
use subtle::{Choice, ConditionallySelectable};
use crate::common::constants::*;
use crate::common::sho::*;
use crate::common::simple_types::*;
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialDefault)]
pub struct ProfileKeyStruct {
pub(crate) bytes: ProfileKeyBytes,
pub(crate) M3: RistrettoPoint,
pub(crate) M4: RistrettoPoint,
}
impl ProfileKeyStruct {
pub fn new(profile_key_bytes: ProfileKeyBytes, uid_bytes: UidBytes) -> Self {
let mut encoded_profile_key = profile_key_bytes;
encoded_profile_key[0] &= 254;
encoded_profile_key[31] &= 63;
let M3 = Self::calc_M3(Self::seed_M3(), profile_key_bytes, uid_bytes);
let M4 = RistrettoPoint::from_uniform_bytes_single_elligator(&encoded_profile_key);
ProfileKeyStruct {
bytes: profile_key_bytes,
M3,
M4,
}
}
pub(crate) fn seed_M3() -> Sho {
Sho::new_seed(b"Signal_ZKGroup_20200424_ProfileKeyAndUid_ProfileKey_CalcM3")
}
pub(crate) fn calc_M3(
mut seed: Sho,
profile_key_bytes: ProfileKeyBytes,
uid_bytes: UidBytes,
) -> RistrettoPoint {
let mut combined_array = [0u8; PROFILE_KEY_LEN + UUID_LEN];
combined_array[..PROFILE_KEY_LEN].copy_from_slice(&profile_key_bytes);
combined_array[PROFILE_KEY_LEN..].copy_from_slice(&uid_bytes);
seed.absorb_and_ratchet(&combined_array);
seed.get_point_single_elligator()
}
pub fn to_bytes(&self) -> ProfileKeyBytes {
self.bytes
}
}
impl ConditionallySelectable for ProfileKeyStruct {
#[expect(
clippy::needless_range_loop,
reason = "an explicit loop makes it more clear that this runs in constant time"
)]
fn conditional_select(
a: &ProfileKeyStruct,
b: &ProfileKeyStruct,
choice: Choice,
) -> ProfileKeyStruct {
let mut bytes: ProfileKeyBytes = [0u8; PROFILE_KEY_LEN];
for i in 0..PROFILE_KEY_LEN {
bytes[i] = u8::conditional_select(&a.bytes[i], &b.bytes[i], choice);
}
ProfileKeyStruct {
bytes,
M3: RistrettoPoint::conditional_select(&a.M3, &b.M3, choice),
M4: RistrettoPoint::conditional_select(&a.M4, &b.M4, choice),
}
}
}
impl zkcredential::attributes::Attribute for ProfileKeyStruct {
fn as_points(&self) -> [RistrettoPoint; 2] {
[self.M3, self.M4]
}
}
+181
View File
@@ -0,0 +1,181 @@
//
// Copyright 2020 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
#![allow(non_snake_case)]
use std::sync::LazyLock;
use curve25519_dalek_signal::ristretto::RistrettoPoint;
use partial_default::PartialDefault;
use serde::{Deserialize, Serialize};
use subtle::{ConditionallySelectable, ConstantTimeEq};
use zkcredential::attributes::Attribute;
use crate::common::errors::*;
use crate::common::sho::*;
use crate::crypto::uid_struct;
static SYSTEM_PARAMS: LazyLock<SystemParams> = LazyLock::new(|| {
crate::deserialize(&SystemParams::SYSTEM_HARDCODED).expect("valid hardcoded params")
});
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialDefault)]
pub struct SystemParams {
pub(crate) G_a1: RistrettoPoint,
pub(crate) G_a2: RistrettoPoint,
}
pub type KeyPair = zkcredential::attributes::KeyPair<UidEncryptionDomain>;
pub type PublicKey = zkcredential::attributes::PublicKey<UidEncryptionDomain>;
pub type Ciphertext = zkcredential::attributes::Ciphertext<UidEncryptionDomain>;
impl SystemParams {
pub fn generate() -> Self {
let mut sho = Sho::new(
b"Signal_ZKGroup_20200424_Constant_UidEncryption_SystemParams_Generate",
b"",
);
let G_a1 = sho.get_point();
let G_a2 = sho.get_point();
SystemParams { G_a1, G_a2 }
}
pub fn get_hardcoded() -> SystemParams {
*SYSTEM_PARAMS
}
const SYSTEM_HARDCODED: [u8; 64] = [
0xa6, 0x32, 0x4c, 0x36, 0x8d, 0xf7, 0x34, 0x69, 0x11, 0x47, 0x98, 0x13, 0x48, 0xb6, 0xe7,
0xeb, 0x42, 0xc3, 0x30, 0x7e, 0x71, 0x1b, 0x6c, 0x7e, 0xcc, 0xd3, 0x3, 0x2d, 0x45, 0x69,
0x3f, 0x5a, 0x4, 0x80, 0x13, 0x52, 0x5b, 0x76, 0x12, 0x4b, 0xf2, 0x64, 0xc, 0x5e, 0x93,
0x69, 0xc7, 0x6e, 0xfb, 0xe8, 0xa, 0xba, 0x2a, 0x24, 0xaa, 0x5d, 0x8e, 0x18, 0xa9, 0x8e,
0xba, 0x14, 0xf8, 0x37,
];
}
pub struct UidEncryptionDomain;
impl zkcredential::attributes::Domain for UidEncryptionDomain {
type Attribute = uid_struct::UidStruct;
const ID: &'static str = "Signal_ZKGroup_20230419_UidEncryption";
fn G_a() -> [RistrettoPoint; 2] {
let system = SystemParams::get_hardcoded();
[system.G_a1, system.G_a2]
}
}
impl UidEncryptionDomain {
pub(crate) fn decrypt(
key_pair: &KeyPair,
ciphertext: &Ciphertext,
) -> Result<libsignal_core::ServiceId, ZkGroupVerificationFailure> {
let M2 = key_pair
.decrypt_to_second_point(ciphertext)
.map_err(|_| ZkGroupVerificationFailure)?;
match M2.lizard_decode::<sha2::Sha256>() {
None => Err(ZkGroupVerificationFailure),
Some(bytes) => {
// We want to do a constant-time choice between the ACI and the PNI possibilities.
// Only at the end do we do a normal branch to see if decryption succeeded,
// and even then we don't want to expose whether we picked the ACI or the PNI.
// So we store them both in an array, and index into it at the very end.
// This isn't fully "data-oblivious"; only one service ID gets loaded from memory at
// the end, and which one is data-dependent. But it is constant-time.
let decoded_uuid = uuid::Uuid::from_bytes(bytes);
let decoded_service_ids = [
libsignal_core::Aci::from(decoded_uuid).into(),
libsignal_core::Pni::from(decoded_uuid).into(),
];
let decoded_aci = &decoded_service_ids[0];
let decoded_pni = &decoded_service_ids[1];
let sho_seed = uid_struct::UidStruct::seed_M1();
let aci_M1 = uid_struct::UidStruct::calc_M1(sho_seed.clone(), *decoded_aci);
let pni_M1 = uid_struct::UidStruct::calc_M1(sho_seed, *decoded_pni);
debug_assert!(aci_M1 != pni_M1);
let decrypted_M1 = key_pair.a1.invert() * ciphertext.as_points()[0];
let mut index = u8::MAX;
index.conditional_assign(&0, decrypted_M1.ct_eq(&aci_M1));
index.conditional_assign(&1, decrypted_M1.ct_eq(&pni_M1));
decoded_service_ids
.get(index as usize)
.copied()
.ok_or(ZkGroupVerificationFailure)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::constants::*;
#[test]
fn test_uid_encryption() {
let master_key = TEST_ARRAY_32;
let mut sho = Sho::new(b"Test_Uid_Encryption", &master_key);
//let system = SystemParams::generate();
//println!("PARAMS = {:#x?}", bincode::serialize(&system));
assert!(SystemParams::generate() == SystemParams::get_hardcoded());
let key_pair = KeyPair::derive_from(sho.as_mut());
// Test serialize of key_pair
let key_pair_bytes = bincode::serialize(&key_pair).unwrap();
match bincode::deserialize::<KeyPair>(&key_pair_bytes[0..key_pair_bytes.len() - 1]) {
Err(_) => (),
_ => unreachable!(),
};
let key_pair2: KeyPair = bincode::deserialize(&key_pair_bytes).unwrap();
assert!(key_pair == key_pair2);
let aci = libsignal_core::Aci::from_uuid_bytes(TEST_ARRAY_16);
let uid = uid_struct::UidStruct::from_service_id(aci.into());
let ciphertext = key_pair.encrypt(&uid);
// Test serialize / deserialize of Ciphertext
let ciphertext_bytes = bincode::serialize(&ciphertext).unwrap();
assert!(ciphertext_bytes.len() == 64);
let ciphertext2: Ciphertext = bincode::deserialize(&ciphertext_bytes).unwrap();
assert!(ciphertext == ciphertext2);
//println!("ciphertext_bytes = {:#x?}", ciphertext_bytes);
assert!(
ciphertext_bytes
== vec![
0xf8, 0x9e, 0xe7, 0x70, 0x5a, 0x66, 0x3, 0x6b, 0x90, 0x8d, 0xb8, 0x84, 0x21,
0x1b, 0x77, 0x3a, 0xc5, 0x43, 0xee, 0x35, 0xc4, 0xa3, 0x8, 0x62, 0x20, 0xfc,
0x3e, 0x1e, 0x35, 0xb4, 0x23, 0x4c, 0xfa, 0x1d, 0x2e, 0xea, 0x2c, 0xc2, 0xf4,
0xb4, 0xc4, 0x2c, 0xff, 0x39, 0xa9, 0xdc, 0xeb, 0x57, 0x29, 0x3b, 0x5f, 0x87,
0x70, 0xca, 0x60, 0xf9, 0xe9, 0xb7, 0x44, 0x47, 0xbf, 0xd3, 0xbd, 0x3d,
]
);
let plaintext = UidEncryptionDomain::decrypt(&key_pair, &ciphertext2).unwrap();
assert!(matches!(plaintext, libsignal_core::ServiceId::Aci(_)));
assert!(uid_struct::UidStruct::from_service_id(plaintext) == uid);
}
#[test]
fn test_pni_encryption() {
let mut sho = Sho::new(b"Test_Pni_Encryption", &[]);
let key_pair = KeyPair::derive_from(sho.as_mut());
let pni = libsignal_core::Pni::from_uuid_bytes(TEST_ARRAY_16);
let uid = uid_struct::UidStruct::from_service_id(pni.into());
let ciphertext = key_pair.encrypt(&uid);
// Test serialize / deserialize of Ciphertext
let ciphertext_bytes = bincode::serialize(&ciphertext).unwrap();
assert!(ciphertext_bytes.len() == 64);
let ciphertext2: Ciphertext = bincode::deserialize(&ciphertext_bytes).unwrap();
assert!(ciphertext == ciphertext2);
let plaintext = UidEncryptionDomain::decrypt(&key_pair, &ciphertext2).unwrap();
assert!(matches!(plaintext, libsignal_core::ServiceId::Pni(_)));
assert!(uid_struct::UidStruct::from_service_id(plaintext) == uid);
}
}
+59
View File
@@ -0,0 +1,59 @@
//
// Copyright 2020 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
#![allow(non_snake_case)]
use curve25519_dalek_signal::ristretto::RistrettoPoint;
use libsignal_core::ServiceId;
use partial_default::PartialDefault;
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use crate::common::sho::*;
use crate::common::simple_types::*;
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, PartialDefault)]
pub struct UidStruct {
// Currently unused. It would be possible to convert this back to the correct kind of ServiceId
// using the same technique as decryption: comparing possible M1 points and seeing which one
// matches. But we don't have a need for that, and therefore it's better if that operation
// remains part of decryption, so that you're guaranteed to get a valid result or an error in
// one step.
//
// At the same time, we can't just remove the field: it's serialized as part of AuthCredential
// and AuthCredentialWithPni, which clients store locally.
#[serde(rename = "bytes")]
raw_uuid_bytes: UidBytes,
pub(crate) M1: RistrettoPoint,
pub(crate) M2: RistrettoPoint,
}
impl UidStruct {
pub fn from_service_id(service_id: ServiceId) -> Self {
let M1 = Self::calc_M1(Self::seed_M1(), service_id);
let raw_uuid_bytes = service_id.raw_uuid().into_bytes();
let M2 = RistrettoPoint::lizard_encode::<Sha256>(&raw_uuid_bytes);
UidStruct {
raw_uuid_bytes,
M1,
M2,
}
}
pub(crate) fn seed_M1() -> Sho {
Sho::new_seed(b"Signal_ZKGroup_20200424_UID_CalcM1")
}
pub(crate) fn calc_M1(mut seed: Sho, service_id: ServiceId) -> RistrettoPoint {
seed.absorb_and_ratchet(&service_id.service_id_binary());
seed.get_point()
}
}
impl zkcredential::attributes::Attribute for UidStruct {
fn as_points(&self) -> [RistrettoPoint; 2] {
[self.M1, self.M2]
}
}
@@ -0,0 +1,472 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
//! Examples of using zkcredential with existing zkgroup types.
//!
//! Has to live in zkgroup because they implement zkcredential traits on zkgroup types.
use curve25519_dalek_signal::ristretto::RistrettoPoint;
use poksho::shoapi::ShoApiExt as _;
use poksho::{ShoApi, ShoSha256};
use serde::{Deserialize, Serialize};
use zkcredential::attributes::{Attribute, Domain, RevealedAttribute};
use zkcredential::credentials::CredentialKeyPair;
use zkcredential::issuance::IssuanceProofBuilder;
use zkcredential::issuance::blind::{
BlindedAttribute, BlindedPoint, BlindingKeyPair, BlindingPublicKey, WithoutNonce,
};
use zkcredential::presentation::{
PresentationProof, PresentationProofBuilder, PresentationProofVerifier,
};
use zkcredential::sho::ShoExt;
use crate::common::sho::*;
use crate::crypto::profile_key_struct::ProfileKeyStruct;
use crate::crypto::uid_struct::UidStruct;
use crate::crypto::{profile_key_encryption, uid_encryption};
use crate::{RANDOMNESS_LEN, TEST_ARRAY_16, TEST_ARRAY_32};
#[test]
fn test_mac_generic() {
let mut sho = ShoSha256::new(b"Test_Credentials");
let keypair = CredentialKeyPair::generate(sho.squeeze_and_ratchet_as_array());
let label = b"20221221_AuthCredentialLike";
let uid_bytes = TEST_ARRAY_16;
let aci = libsignal_core::Aci::from_uuid_bytes(uid_bytes);
let uid = UidStruct::from_service_id(aci.into());
let proof = IssuanceProofBuilder::new(label)
.add_attribute(&uid)
.add_public_attribute(&[1, 2, 3])
.issue(&keypair, sho.squeeze_and_ratchet_as_array());
let credential = IssuanceProofBuilder::new(label)
.add_attribute(&uid)
.add_public_attribute(&[1, 2, 3])
.verify(keypair.public_key(), proof)
.unwrap();
let uid_encryption_key = uid_encryption::KeyPair::derive_from(Sho::new(b"test", b"").as_mut());
let uid_encryption_public_key = uid_encryption_key.public_key;
let proof = PresentationProofBuilder::new(label)
.add_attribute(&uid, &uid_encryption_key)
.present(
keypair.public_key(),
&credential,
sho.squeeze_and_ratchet_as_array(),
);
PresentationProofVerifier::new(label)
.add_public_attribute(&[1, 2, 3])
.add_attribute(
&uid_encryption_key.encrypt(&uid),
&uid_encryption_public_key,
)
.verify(&keypair, &proof)
.unwrap()
}
#[test]
fn test_mac_generic_without_verifying_encryption_key() {
let mut sho = ShoSha256::new(b"Test_Credentials");
let keypair = CredentialKeyPair::generate(sho.squeeze_and_ratchet_as_array());
let label = b"20221221_AuthCredentialLike";
let uid_bytes = TEST_ARRAY_16;
let aci = libsignal_core::Aci::from_uuid_bytes(uid_bytes);
let uid = UidStruct::from_service_id(aci.into());
let proof = IssuanceProofBuilder::new(label)
.add_attribute(&uid)
.add_public_attribute(&[1, 2, 3])
.issue(&keypair, sho.squeeze_and_ratchet_as_array());
let credential = IssuanceProofBuilder::new(label)
.add_attribute(&uid)
.add_public_attribute(&[1, 2, 3])
.verify(keypair.public_key(), proof)
.unwrap();
let uid_encryption_key = uid_encryption::KeyPair::derive_from(Sho::new(b"test", b"").as_mut());
let proof = PresentationProofBuilder::new(label)
.add_attribute_without_verified_key(&uid, &uid_encryption_key)
.present(
keypair.public_key(),
&credential,
sho.squeeze_and_ratchet_as_array(),
);
PresentationProofVerifier::new(label)
.add_public_attribute(&[1, 2, 3])
.add_attribute_without_verified_key(
&uid_encryption_key.encrypt(&uid),
uid_encryption::UidEncryptionDomain::ID,
)
.verify(&keypair, &proof)
.unwrap()
}
#[test]
fn test_profile_key_credential() {
let mut sho = ShoSha256::new(b"Test_Credentials");
let keypair = CredentialKeyPair::generate(sho.squeeze_and_ratchet_as_array());
let blinding_keypair = BlindingKeyPair::generate(&mut sho);
let label = b"20221221_ProfileKeyCredentialLike";
let aci = libsignal_core::Aci::from_uuid_bytes(TEST_ARRAY_16);
let uid = UidStruct::from_service_id(aci.into());
let profile_key = ProfileKeyStruct::new(TEST_ARRAY_32, TEST_ARRAY_16);
let encrypted_profile_key = blinding_keypair.encrypt(&profile_key, &mut sho).into();
#[derive(Serialize, Deserialize)]
struct Request {
uid: UidStruct,
encrypted_profile_key: BlindedAttribute,
blinding_public_key: BlindingPublicKey,
}
// Client
let request_serialized = bincode::serialize(&Request {
uid,
encrypted_profile_key,
blinding_public_key: *blinding_keypair.public_key(),
})
.unwrap();
// Issuing server
let request: Request = bincode::deserialize(&request_serialized).unwrap();
let proof = IssuanceProofBuilder::with_authenticated_message(label, b"abc")
.add_attribute(&request.uid)
.add_blinded_attribute(&request.encrypted_profile_key)
.issue(
&keypair,
&request.blinding_public_key,
sho.squeeze_and_ratchet_as_array(),
);
let proof_serialized = bincode::serialize(&proof).unwrap();
// Client
let credential = IssuanceProofBuilder::with_authenticated_message(label, b"abc")
.add_attribute(&uid)
.add_blinded_attribute(&encrypted_profile_key)
.verify(
keypair.public_key(),
&blinding_keypair,
bincode::deserialize(&proof_serialized).unwrap(),
)
.unwrap();
let mut zkgroup_sho = Sho::new(b"test", b"");
let uid_encryption_key = uid_encryption::KeyPair::derive_from(zkgroup_sho.as_mut());
let profile_key_encryption_key =
profile_key_encryption::KeyPair::derive_from(zkgroup_sho.as_mut());
let proof = PresentationProofBuilder::with_authenticated_message(label, b"v1")
.add_attribute(&uid, &uid_encryption_key)
.add_attribute(&profile_key, &profile_key_encryption_key)
.present(
keypair.public_key(),
&credential,
sho.squeeze_and_ratchet_as_array(),
);
#[derive(Serialize, Deserialize)]
struct Presentation {
proof: PresentationProof,
encrypted_uid: uid_encryption::Ciphertext,
encrypted_profile_key: profile_key_encryption::Ciphertext,
uid_encryption_public_key: uid_encryption::PublicKey,
profile_key_encryption_public_key: profile_key_encryption::PublicKey,
}
let presentation_serialized = bincode::serialize(&Presentation {
proof,
encrypted_uid: uid_encryption_key.encrypt(&uid),
encrypted_profile_key: profile_key_encryption_key.encrypt(&profile_key),
uid_encryption_public_key: uid_encryption_key.public_key,
profile_key_encryption_public_key: profile_key_encryption_key.public_key,
})
.unwrap();
// Verifying server
let presentation: Presentation = bincode::deserialize(&presentation_serialized).unwrap();
PresentationProofVerifier::with_authenticated_message(label, b"v1")
.add_attribute(
&presentation.encrypted_uid,
&presentation.uid_encryption_public_key,
)
.add_attribute(
&presentation.encrypted_profile_key,
&presentation.profile_key_encryption_public_key,
)
.verify(&keypair, &presentation.proof)
.unwrap();
}
#[test]
fn test_profile_key_credential_only_verifying_one_encryption_key() {
let mut sho = ShoSha256::new(b"Test_Credentials");
let keypair = CredentialKeyPair::generate(sho.squeeze_and_ratchet_as_array());
let blinding_keypair = BlindingKeyPair::generate(&mut sho);
let label = b"20221221_ProfileKeyCredentialLike";
let aci = libsignal_core::Aci::from_uuid_bytes(TEST_ARRAY_16);
let uid = UidStruct::from_service_id(aci.into());
let profile_key = ProfileKeyStruct::new(TEST_ARRAY_32, TEST_ARRAY_16);
let encrypted_profile_key = blinding_keypair.encrypt(&profile_key, &mut sho).into();
#[derive(Serialize, Deserialize)]
struct Request {
uid: UidStruct,
encrypted_profile_key: BlindedAttribute,
blinding_public_key: BlindingPublicKey,
}
// Client
let request_serialized = bincode::serialize(&Request {
uid,
encrypted_profile_key,
blinding_public_key: *blinding_keypair.public_key(),
})
.unwrap();
// Issuing server
let request: Request = bincode::deserialize(&request_serialized).unwrap();
let proof = IssuanceProofBuilder::with_authenticated_message(label, b"abc")
.add_attribute(&request.uid)
.add_blinded_attribute(&request.encrypted_profile_key)
.issue(
&keypair,
&request.blinding_public_key,
sho.squeeze_and_ratchet_as_array(),
);
let proof_serialized = bincode::serialize(&proof).unwrap();
// Client
let credential = IssuanceProofBuilder::with_authenticated_message(label, b"abc")
.add_attribute(&uid)
.add_blinded_attribute(&encrypted_profile_key)
.verify(
keypair.public_key(),
&blinding_keypair,
bincode::deserialize(&proof_serialized).unwrap(),
)
.unwrap();
let mut zkgroup_sho = Sho::new(b"test", b"");
let uid_encryption_key = uid_encryption::KeyPair::derive_from(zkgroup_sho.as_mut());
let profile_key_encryption_key =
profile_key_encryption::KeyPair::derive_from(zkgroup_sho.as_mut());
let proof = PresentationProofBuilder::with_authenticated_message(label, b"v1")
.add_attribute_without_verified_key(&uid, &uid_encryption_key)
.add_attribute(&profile_key, &profile_key_encryption_key)
.present(
keypair.public_key(),
&credential,
sho.squeeze_and_ratchet_as_array(),
);
#[derive(Serialize, Deserialize)]
struct Presentation {
proof: PresentationProof,
encrypted_uid: uid_encryption::Ciphertext,
encrypted_profile_key: profile_key_encryption::Ciphertext,
uid_encryption_public_key: uid_encryption::PublicKey,
profile_key_encryption_public_key: profile_key_encryption::PublicKey,
}
let presentation_serialized = bincode::serialize(&Presentation {
proof,
encrypted_uid: uid_encryption_key.encrypt(&uid),
encrypted_profile_key: profile_key_encryption_key.encrypt(&profile_key),
uid_encryption_public_key: uid_encryption_key.public_key,
profile_key_encryption_public_key: profile_key_encryption_key.public_key,
})
.unwrap();
// Verifying server
let presentation: Presentation = bincode::deserialize(&presentation_serialized).unwrap();
PresentationProofVerifier::with_authenticated_message(label, b"v1")
.add_attribute_without_verified_key(
&presentation.encrypted_uid,
uid_encryption::UidEncryptionDomain::ID,
)
.add_attribute(
&presentation.encrypted_profile_key,
&presentation.profile_key_encryption_public_key,
)
.verify(&keypair, &presentation.proof)
.unwrap();
}
#[test]
fn test_room_credential() {
let mut sho = ShoSha256::new(b"RoomCredential");
let keypair = CredentialKeyPair::generate(sho.squeeze_and_ratchet_as_array());
let blinding_keypair = BlindingKeyPair::generate(&mut sho);
let label = b"20230330_RoomCredential";
let request_label = b"20230330_RoomCredential_Request";
#[derive(Serialize, Deserialize)]
struct RoomId {
opaque_id: RistrettoPoint,
}
impl RevealedAttribute for RoomId {
fn as_point(&self) -> RistrettoPoint {
self.opaque_id
}
}
let room_id = RoomId {
opaque_id: sho.get_point(),
};
let blinded_room_id = blinding_keypair.blind(&room_id, &mut sho);
// Generate a request proof.
let mut request_proof_statement = poksho::Statement::new();
request_proof_statement.add("Y", &[("y", "G")]);
request_proof_statement.add("D1", &[("r1", "G")]);
// For most credentials we'd want to constraint D2 as well, but room IDs are unconstrained.
// So we leave it out; if the client passes a wild D2, well, they won't get a credential back.
let mut request_scalar_args = poksho::ScalarArgs::new();
request_scalar_args.add("y", blinding_keypair.private_key().y);
request_scalar_args.add("r1", blinded_room_id.r.0);
let mut request_point_args = poksho::PointArgs::new();
request_point_args.add("Y", blinding_keypair.public_key().Y);
request_point_args.add("D1", blinded_room_id.D1);
let proof = request_proof_statement
.prove(
&request_scalar_args,
&request_point_args,
request_label,
&sho.squeeze_and_ratchet_as_array::<RANDOMNESS_LEN>(),
)
.expect("valid");
let blinded_room_id: BlindedPoint<WithoutNonce> = blinded_room_id.into();
#[derive(Serialize, Deserialize)]
struct Request {
blinded_room_id: BlindedPoint,
blinding_public_key: BlindingPublicKey,
proof: Vec<u8>,
}
// Client
let request_serialized = bincode::serialize(&Request {
blinded_room_id,
blinding_public_key: *blinding_keypair.public_key(),
proof,
})
.unwrap();
// Issuing server
let request: Request = bincode::deserialize(&request_serialized).unwrap();
let mut request_verifying_point_args = poksho::PointArgs::new();
request_verifying_point_args.add("Y", request.blinding_public_key.Y);
request_verifying_point_args.add("D1", request.blinded_room_id.D1);
request_proof_statement
.verify_proof(&request.proof, &request_verifying_point_args, request_label)
.expect("valid");
let expiration = 1680220000u32;
let proof = IssuanceProofBuilder::new(label)
.add_public_attribute(&expiration)
.add_blinded_revealed_attribute(&request.blinded_room_id)
.issue(
&keypair,
&request.blinding_public_key,
sho.squeeze_and_ratchet_as_array(),
);
let proof_serialized = bincode::serialize(&proof).unwrap();
// Client
let credential = IssuanceProofBuilder::new(label)
.add_public_attribute(&expiration)
.add_blinded_revealed_attribute(&blinded_room_id)
.verify(
keypair.public_key(),
&blinding_keypair,
bincode::deserialize(&proof_serialized).unwrap(),
)
.unwrap();
let proof = PresentationProofBuilder::new(label)
.add_revealed_attribute(&room_id)
.present(
keypair.public_key(),
&credential,
sho.squeeze_and_ratchet_as_array(),
);
#[derive(Serialize, Deserialize)]
struct Presentation {
proof: PresentationProof,
room_id: RoomId,
expiration: u32,
}
let presentation_serialized = bincode::serialize(&Presentation {
proof,
room_id,
expiration,
})
.unwrap();
// Verifying server
let presentation: Presentation = bincode::deserialize(&presentation_serialized).unwrap();
PresentationProofVerifier::new(label)
.add_public_attribute(&presentation.expiration)
.add_revealed_attribute(&presentation.room_id)
.verify(&keypair, &presentation.proof)
.unwrap();
}
struct InverseUidDecryptionKey;
impl zkcredential::attributes::Domain for InverseUidDecryptionKey {
type Attribute = uid_encryption::Ciphertext;
const ID: &'static str = "InverseUidEncryptionDomain_20231011";
fn G_a() -> [curve25519_dalek_signal::RistrettoPoint; 2] {
static STORAGE: std::sync::OnceLock<[curve25519_dalek_signal::RistrettoPoint; 2]> =
std::sync::OnceLock::new();
*zkcredential::attributes::derive_default_generator_points::<Self>(&STORAGE)
}
}
#[test]
fn test_inverse_key() {
let aci = libsignal_core::Aci::from_uuid_bytes(TEST_ARRAY_16);
let uid = UidStruct::from_service_id(aci.into());
let mut sho = Sho::new(b"test_inverse_key", b"");
let uid_encryption_key = uid_encryption::KeyPair::derive_from(sho.as_mut());
let encrypted = uid_encryption_key.encrypt(&uid);
let inverse = zkcredential::attributes::KeyPair::<InverseUidDecryptionKey>::inverse_of(
&uid_encryption_key,
);
#[allow(non_snake_case)]
let [E_A1_prime, E_A2_prime] = inverse.encrypt(&encrypted).as_points();
assert_eq!(uid.M1, E_A1_prime);
assert_eq!(uid.M2, E_A2_prime);
}