1use frost_ed25519 as frost;
2use serde::{Deserialize, Serialize};
3
4pub mod serde_bytes_64 {
6 use serde::de::Error;
7 use serde::{Deserializer, Serializer};
8
9 pub fn serialize<S>(bytes: &[u8; 64], serializer: S) -> Result<S::Ok, S::Error>
10 where
11 S: Serializer,
12 {
13 serializer.serialize_bytes(bytes)
14 }
15
16 pub fn deserialize<'de, D>(deserializer: D) -> Result<[u8; 64], D::Error>
17 where
18 D: Deserializer<'de>,
19 {
20 let slice: &[u8] = serde::Deserialize::deserialize(deserializer)?;
21 slice
22 .try_into()
23 .map_err(|_| D::Error::custom("expected 64 bytes"))
24 }
25}
26
27pub const ENROLL_POP_CONTEXT: &[u8] = b"YIP-CA-ENROLL-POP-V1";
29
30#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
33pub struct CertTbs {
34 pub version: u8,
35 pub member_pubkey: [u8; 32],
36 pub member_sign_pubkey: [u8; 32],
37 pub network_id: [u8; 16],
38 pub not_before: u64,
39 pub not_after: u64,
40 pub tags: Vec<(u8, Vec<u8>)>,
41}
42
43impl CertTbs {
44 pub fn to_canonical_bytes(&self) -> Vec<u8> {
45 let mut out = Vec::new();
46 out.push(self.version);
47 out.extend_from_slice(&self.member_pubkey);
48 out.extend_from_slice(&self.member_sign_pubkey);
49 out.extend_from_slice(&self.network_id);
50 out.extend_from_slice(&self.not_before.to_be_bytes());
51 out.extend_from_slice(&self.not_after.to_be_bytes());
52 let tag_count = u16::try_from(self.tags.len()).expect("tags fit in u16");
53 out.extend_from_slice(&tag_count.to_be_bytes());
54 for (tag, bytes) in &self.tags {
55 out.push(*tag);
56 let len = u16::try_from(bytes.len()).expect("tag value fits in u16");
57 out.extend_from_slice(&len.to_be_bytes());
58 out.extend_from_slice(bytes);
59 }
60 out
61 }
62}
63
64#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
66pub enum EnrollmentCredential {
67 BootstrapToken {
69 token_id: [u8; 16],
70 allowed_node_id: [u8; 32],
71 assigned_ipv4: u32,
72 assigned_ipv6: [u8; 16],
73 allowed_flags: u32,
74 not_after_unix: u64,
75 #[serde(with = "serde_bytes_64")]
76 admin_signature: [u8; 64],
77 },
78 RenewalCert {
80 existing_cert_bytes: Vec<u8>,
81 #[serde(with = "serde_bytes_64")]
82 renewal_signature: [u8; 64],
83 },
84}
85
86#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
88pub struct SubmitEnrollmentPayload {
89 pub challenge_nonce: [u8; 32],
90 pub csr_tbs: CertTbs,
91 pub credential: EnrollmentCredential,
92 #[serde(with = "serde_bytes_64")]
93 pub pop_signature: [u8; 64],
94}
95
96#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
98pub enum ClientRpcRequest {
99 GetChallenge,
100 SubmitEnrollment(Box<SubmitEnrollmentPayload>),
101}
102
103#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
105pub enum ClientRpcResponse {
106 Challenge {
107 challenge_nonce: [u8; 32],
108 expires_at_unix: u64,
109 },
110 Enrolled {
111 cert_bytes: Vec<u8>,
113 ca_public_key: [u8; 32],
115 },
116 Rejected {
117 reason: String,
118 },
119}
120
121#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
123pub struct NonceSlotId {
124 pub boot_epoch_id: u128,
125 pub sequence_idx: u64,
126}
127
128#[derive(Clone, Debug, Serialize, Deserialize)]
130pub struct SignCertRequestPayload {
131 pub session_id: u128,
132 pub slot_id: NonceSlotId,
133 pub signing_package: frost::SigningPackage,
134 pub cert_tbs: CertTbs,
135 pub credential: EnrollmentCredential,
136 pub challenge_nonce: [u8; 32],
137 #[serde(with = "serde_bytes_64")]
138 pub pop_signature: [u8; 64],
139}
140
141#[derive(Clone, Debug, Serialize, Deserialize)]
143pub struct SignCertResponsePayload {
144 pub session_id: u128,
145 pub signer_id: frost::Identifier,
146 pub signature_share: frost::round2::SignatureShare,
147 pub replenish_commitment: Option<(NonceSlotId, frost::round1::SigningCommitments)>,
148}
149
150#[derive(Clone, Debug, Serialize, Deserialize)]
152pub enum QuorumRpcMessage {
153 PublishCommitments {
155 signer_id: frost::Identifier,
156 boot_epoch_id: u128,
157 commitments: Vec<(NonceSlotId, frost::round1::SigningCommitments)>,
158 },
159 SignCertRequest(Box<SignCertRequestPayload>),
161 SignCertResponse(Box<SignCertResponsePayload>),
163 SignCertReject {
165 session_id: u128,
166 signer_id: frost::Identifier,
167 reason: String,
168 },
169}
170
171#[derive(Clone, Debug, Serialize, Deserialize)]
173pub enum CaWireMessage {
174 Quorum(QuorumRpcMessage),
175 ClientReq(ClientRpcRequest),
176 ClientResp(ClientRpcResponse),
177}
178
179pub fn verify_enrollment_pop(
181 pop_sig_bytes: &[u8; 64],
182 signer_pubkey_bytes: &[u8; 32],
183 challenge_nonce: &[u8; 32],
184 csr_tbs: &CertTbs,
185) -> Result<(), &'static str> {
186 use ed25519_dalek::{Signature, VerifyingKey};
187 let vk =
188 VerifyingKey::from_bytes(signer_pubkey_bytes).map_err(|_| "invalid signer public key")?;
189 let sig = Signature::from_bytes(pop_sig_bytes);
190 let mut pop_msg = Vec::new();
191 pop_msg.extend_from_slice(ENROLL_POP_CONTEXT);
192 pop_msg.extend_from_slice(challenge_nonce);
193 pop_msg.extend_from_slice(&csr_tbs.to_canonical_bytes());
194 vk.verify_strict(&pop_msg, &sig)
195 .map_err(|_| "invalid Proof-of-Possession signature")
196}