Add project files.
This commit is contained in:
@@ -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
@@ -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()
|
||||
);
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) * γ
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
]);
|
||||
@@ -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,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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[..]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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(¶ms).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);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
¶ms.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(
|
||||
¶ms.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")
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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(¤t_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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
@@ -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`"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(¶ms));
|
||||
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]
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user