Skip to main content

zkgroup/api/
zk_credential_key.rs

1//
2// Copyright 2026 Signal Messenger, LLC.
3// SPDX-License-Identifier: AGPL-3.0-only
4//
5
6//! Long-term Ristretto key pair owned by an account, used as a binding identity
7//! across ZK credentials issued to that account (currently
8//! [`crate::avatars::AvatarUploadCredential`]).
9//!
10//! Distinct from the account's curve25519 identity key. The public key is a wire
11//! type stored by the server; the secret key is a wire type that must be synced
12//! to linked devices.
13
14// We use upper-case variable names for curve points by convention.
15#![allow(non_snake_case)]
16
17use curve25519_dalek_signal::ristretto::RistrettoPoint;
18use curve25519_dalek_signal::scalar::Scalar;
19use partial_default::PartialDefault;
20use poksho::ShoApi;
21use serde::{Deserialize, Serialize};
22use zkcredential::sho::ShoExt as _;
23
24use crate::RandomnessBytes;
25use crate::common::serialization::ReservedByte;
26
27#[derive(Clone, Deserialize, PartialDefault)]
28#[serde(from = "ZkCredentialPrivateKey")]
29pub struct ZkCredentialKeyPair {
30    secret: Scalar,
31    public: RistrettoPoint,
32}
33
34/// The serialized form of [`ZkCredentialKeyPair`].
35///
36/// This stores only the secret scalar. The public key is derived when the key
37/// pair is loaded, matching the surrounding key-pair types in `zkcredential`.
38#[derive(Clone, Serialize, Deserialize, PartialDefault)]
39struct ZkCredentialPrivateKey {
40    reserved: ReservedByte,
41    secret: Scalar,
42}
43
44impl ZkCredentialKeyPair {
45    pub fn generate(randomness: RandomnessBytes) -> Self {
46        let mut sho = poksho::ShoHmacSha256::new(b"20260520_Signal_ZkCredentialKeyPair_Generate");
47        sho.absorb_and_ratchet(&randomness);
48        let secret = sho.get_scalar();
49        let public = RistrettoPoint::mul_base(&secret);
50        Self { secret, public }
51    }
52
53    pub fn public_key(&self) -> ZkCredentialPublicKey {
54        ZkCredentialPublicKey {
55            reserved: Default::default(),
56            public: self.public,
57        }
58    }
59
60    pub(crate) fn secret(&self) -> Scalar {
61        self.secret
62    }
63}
64
65impl From<ZkCredentialPrivateKey> for ZkCredentialKeyPair {
66    fn from(value: ZkCredentialPrivateKey) -> Self {
67        let ZkCredentialPrivateKey {
68            reserved: _,
69            secret,
70        } = value;
71        let public = RistrettoPoint::mul_base(&secret);
72        Self { secret, public }
73    }
74}
75
76impl Serialize for ZkCredentialKeyPair {
77    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
78    where
79        S: serde::Serializer,
80    {
81        ZkCredentialPrivateKey {
82            reserved: Default::default(),
83            secret: self.secret,
84        }
85        .serialize(serializer)
86    }
87}
88
89/// The public half of a [`ZkCredentialKeyPair`].
90///
91/// Serialized wire format: reserved byte + 32-byte compressed Ristretto point.
92#[derive(Clone, Copy, Serialize, Deserialize, PartialDefault)]
93pub struct ZkCredentialPublicKey {
94    reserved: ReservedByte,
95    public: RistrettoPoint,
96}
97
98impl ZkCredentialPublicKey {
99    pub(crate) fn point(&self) -> RistrettoPoint {
100        self.public
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use crate::RANDOMNESS_LEN;
108
109    #[test]
110    fn generate_is_deterministic() {
111        let r: RandomnessBytes = [0x7Au8; RANDOMNESS_LEN];
112        let a = ZkCredentialKeyPair::generate(r);
113        let b = ZkCredentialKeyPair::generate(r);
114        assert_eq!(a.secret, b.secret);
115        assert_eq!(a.public, b.public);
116    }
117
118    #[test]
119    fn roundtrip_keypair() {
120        let r: RandomnessBytes = [0x11u8; RANDOMNESS_LEN];
121        let kp = ZkCredentialKeyPair::generate(r);
122        let bytes = crate::serialize(&kp);
123        let parsed: ZkCredentialKeyPair = crate::deserialize(&bytes).expect("roundtrip");
124        assert_eq!(kp.secret, parsed.secret);
125        assert_eq!(kp.public, parsed.public);
126    }
127
128    #[test]
129    fn roundtrip_public_key() {
130        let r: RandomnessBytes = [0x22u8; RANDOMNESS_LEN];
131        let pk = ZkCredentialKeyPair::generate(r).public_key();
132        let bytes = crate::serialize(&pk);
133        let parsed: ZkCredentialPublicKey = crate::deserialize(&bytes).expect("roundtrip");
134        assert_eq!(pk.public, parsed.public);
135    }
136
137    #[test]
138    fn public_key_derives_from_secret() {
139        let r: RandomnessBytes = [0x33u8; RANDOMNESS_LEN];
140        let kp = ZkCredentialKeyPair::generate(r);
141        let pk = kp.public_key();
142        assert_eq!(RistrettoPoint::mul_base(&kp.secret), pk.public);
143    }
144}