1use rcgen::{CertificateParams, CustomExtension, KeyPair};
39
40use crate::bls::{g1_subgroup_check, public_key_bytes, sign_message, verify_signature, SecretKey};
41
42pub const DIG_BLS_BINDING_OID: &[u64] = &[1, 3, 6, 1, 4, 1, 58968, 1, 1];
49
50pub const DIG_BLS_BINDING_OID_STR: &str = "1.3.6.1.4.1.58968.1.1";
52
53pub const BINDING_VERSION_V1: u8 = 1;
56
57const BINDING_V1_LEN: usize = 1 + 48 + 96;
59
60const BINDING_SIG_CONTEXT: &[u8] = b"dig-nat/cert-bls-binding/v1";
65
66pub fn binding_message(spki_der: &[u8]) -> Vec<u8> {
69 let mut msg = Vec::with_capacity(BINDING_SIG_CONTEXT.len() + spki_der.len());
70 msg.extend_from_slice(BINDING_SIG_CONTEXT);
71 msg.extend_from_slice(spki_der);
72 msg
73}
74
75pub fn encode_binding_extension_value(bls_pub: &[u8; 48], bls_sig: &[u8; 96]) -> Vec<u8> {
77 let mut value = Vec::with_capacity(BINDING_V1_LEN);
78 value.push(BINDING_VERSION_V1);
79 value.extend_from_slice(bls_pub);
80 value.extend_from_slice(bls_sig);
81 value
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct CertBlsBinding {
87 pub bls_pub: [u8; 48],
89 pub bls_sig: [u8; 96],
91}
92
93pub fn parse_binding_extension_value(value: &[u8]) -> Option<CertBlsBinding> {
97 if value.first().copied()? != BINDING_VERSION_V1 || value.len() != BINDING_V1_LEN {
98 return None;
99 }
100 let mut bls_pub = [0u8; 48];
101 let mut bls_sig = [0u8; 96];
102 bls_pub.copy_from_slice(&value[1..49]);
103 bls_sig.copy_from_slice(&value[49..145]);
104 Some(CertBlsBinding { bls_pub, bls_sig })
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
109pub enum BindingOutcome {
110 Bound {
112 bls_pub: [u8; 48],
114 },
115 Absent,
117 Invalid(&'static str),
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
124pub enum BindingPolicy {
125 Off,
127 #[default]
129 Opportunistic,
130 Required,
132}
133
134pub fn evaluate(
140 outcome: &BindingOutcome,
141 policy: BindingPolicy,
142) -> std::result::Result<Option<[u8; 48]>, &'static str> {
143 match (policy, outcome) {
144 (BindingPolicy::Off, _) => Ok(None),
146
147 (_, BindingOutcome::Bound { bls_pub }) => Ok(Some(*bls_pub)),
149
150 (BindingPolicy::Opportunistic, BindingOutcome::Absent) => Ok(None),
152 (BindingPolicy::Opportunistic, BindingOutcome::Invalid(reason)) => Err(reason),
153
154 (BindingPolicy::Required, BindingOutcome::Absent) => {
156 Err("cert BLS binding required but absent (possible downgrade)")
157 }
158 (BindingPolicy::Required, BindingOutcome::Invalid(reason)) => Err(reason),
159 }
160}
161
162pub fn verify_binding_from_leaf_cert(cert_der: &[u8]) -> BindingOutcome {
170 let Ok((_, x509)) = x509_parser::parse_x509_certificate(cert_der) else {
171 return BindingOutcome::Invalid("leaf certificate could not be parsed as X.509");
172 };
173 let spki_der = x509.tbs_certificate.subject_pki.raw;
174
175 let mut binding_value: Option<&[u8]> = None;
176 for ext in x509.extensions() {
177 if ext.oid.to_id_string() == DIG_BLS_BINDING_OID_STR {
178 binding_value = Some(ext.value);
179 break;
180 }
181 }
182 let Some(value) = binding_value else {
183 return BindingOutcome::Absent;
184 };
185
186 let Some(binding) = parse_binding_extension_value(value) else {
187 return BindingOutcome::Invalid("binding extension malformed or unknown version");
188 };
189 if !g1_subgroup_check(&binding.bls_pub) {
191 return BindingOutcome::Invalid("binding BLS pubkey failed the G1 subgroup check");
192 }
193 if !verify_signature(
195 &binding.bls_pub,
196 &binding_message(spki_der),
197 &binding.bls_sig,
198 ) {
199 return BindingOutcome::Invalid(
200 "binding BLS self-attestation did not verify over the SPKI",
201 );
202 }
203 BindingOutcome::Bound {
204 bls_pub: binding.bls_pub,
205 }
206}
207
208pub(crate) fn attach_binding(
212 params: &mut CertificateParams,
213 key_pair: &KeyPair,
214 bls_sk: &SecretKey,
215) {
216 let spki_der = key_pair.public_key_der();
217 let bls_pub = public_key_bytes(bls_sk);
218 let bls_sig = sign_message(bls_sk, &binding_message(&spki_der));
219 let ext_value = encode_binding_extension_value(&bls_pub, &bls_sig);
220 params
221 .custom_extensions
222 .push(CustomExtension::from_oid_content(
223 DIG_BLS_BINDING_OID,
224 ext_value,
225 ));
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use sha2::{Digest, Sha256};
232
233 fn node_bls_sk(label: &str) -> SecretKey {
235 let seed: [u8; 32] = Sha256::digest(label.as_bytes()).into();
236 SecretKey::from_seed(&seed)
237 }
238
239 fn tls_key_pair() -> KeyPair {
240 KeyPair::generate().expect("generate TLS key pair")
241 }
242
243 fn bound_self_signed(kp: &KeyPair, bls_sk: &SecretKey) -> Vec<u8> {
246 let mut params = CertificateParams::new(vec!["peer.dig".into()]).unwrap();
247 attach_binding(&mut params, kp, bls_sk);
248 params.self_signed(kp).unwrap().der().to_vec()
249 }
250
251 #[test]
252 fn oid_arc_and_string_forms_agree() {
253 let dotted = DIG_BLS_BINDING_OID
254 .iter()
255 .map(|n| n.to_string())
256 .collect::<Vec<_>>()
257 .join(".");
258 assert_eq!(dotted, DIG_BLS_BINDING_OID_STR);
259 }
260
261 #[test]
262 fn extension_value_round_trips() {
263 let value = encode_binding_extension_value(&[7u8; 48], &[9u8; 96]);
264 assert_eq!(value.len(), BINDING_V1_LEN);
265 let parsed = parse_binding_extension_value(&value).expect("parses");
266 assert_eq!(parsed.bls_pub, [7u8; 48]);
267 assert_eq!(parsed.bls_sig, [9u8; 96]);
268 }
269
270 #[test]
271 fn parse_rejects_bad_length_and_unknown_version() {
272 assert_eq!(parse_binding_extension_value(&[]), None);
273 assert_eq!(
274 parse_binding_extension_value(&[BINDING_VERSION_V1; 10]),
275 None
276 );
277 let mut wrong = vec![0u8; BINDING_V1_LEN];
278 wrong[0] = 0xFE;
279 assert_eq!(parse_binding_extension_value(&wrong), None);
280 }
281
282 #[test]
283 fn valid_bound_cert_verifies() {
284 let kp = tls_key_pair();
285 let bls_sk = node_bls_sk("binding/valid");
286 let cert = bound_self_signed(&kp, &bls_sk);
287 match verify_binding_from_leaf_cert(&cert) {
288 BindingOutcome::Bound { bls_pub } => {
289 assert_eq!(
290 bls_pub,
291 public_key_bytes(&bls_sk),
292 "verified pubkey is the signer's"
293 );
294 }
295 other => panic!("expected Bound, got {other:?}"),
296 }
297 }
298
299 #[test]
300 fn cert_without_extension_is_absent() {
301 let c = rcgen::generate_simple_self_signed(vec!["peer.dig".into()]).unwrap();
302 assert_eq!(
303 verify_binding_from_leaf_cert(c.cert.der()),
304 BindingOutcome::Absent
305 );
306 }
307
308 #[test]
309 fn anti_substitution_wrong_bls_key_rejected() {
310 let kp = tls_key_pair();
312 let victim_sk = node_bls_sk("binding/victim");
313 let attacker_pub = public_key_bytes(&node_bls_sk("binding/attacker"));
314 let spki = kp.public_key_der();
315 let sig = sign_message(&victim_sk, &binding_message(&spki));
316 let ext = encode_binding_extension_value(&attacker_pub, &sig);
317 let mut params = CertificateParams::new(vec!["peer.dig".into()]).unwrap();
318 params
319 .custom_extensions
320 .push(CustomExtension::from_oid_content(DIG_BLS_BINDING_OID, ext));
321 let cert = params.self_signed(&kp).unwrap().der().to_vec();
322 assert!(matches!(
323 verify_binding_from_leaf_cert(&cert),
324 BindingOutcome::Invalid(_)
325 ));
326 }
327
328 #[test]
329 fn anti_substitution_binding_replayed_on_other_cert_rejected() {
330 let kp_a = tls_key_pair();
331 let bls_sk = node_bls_sk("binding/replay");
332 let sig = sign_message(&bls_sk, &binding_message(&kp_a.public_key_der()));
333 let ext = encode_binding_extension_value(&public_key_bytes(&bls_sk), &sig);
334 let kp_b = tls_key_pair();
336 let mut params = CertificateParams::new(vec!["peer.dig".into()]).unwrap();
337 params
338 .custom_extensions
339 .push(CustomExtension::from_oid_content(DIG_BLS_BINDING_OID, ext));
340 let cert_b = params.self_signed(&kp_b).unwrap().der().to_vec();
341 assert!(matches!(
342 verify_binding_from_leaf_cert(&cert_b),
343 BindingOutcome::Invalid(_)
344 ));
345 }
346
347 #[test]
348 fn subgroup_check_rejects_bad_g1_point() {
349 let kp = tls_key_pair();
350 let bls_sk = node_bls_sk("binding/subgroup");
351 let sig = sign_message(&bls_sk, &binding_message(&kp.public_key_der()));
352 let ext = encode_binding_extension_value(&[0xFFu8; 48], &sig);
353 let mut params = CertificateParams::new(vec!["peer.dig".into()]).unwrap();
354 params
355 .custom_extensions
356 .push(CustomExtension::from_oid_content(DIG_BLS_BINDING_OID, ext));
357 let cert = params.self_signed(&kp).unwrap().der().to_vec();
358 assert!(matches!(
359 verify_binding_from_leaf_cert(&cert),
360 BindingOutcome::Invalid(_)
361 ));
362 }
363
364 #[test]
365 fn policy_off_accepts_everything() {
366 let pk = [1u8; 48];
367 assert_eq!(
368 evaluate(&BindingOutcome::Absent, BindingPolicy::Off),
369 Ok(None)
370 );
371 assert_eq!(
372 evaluate(&BindingOutcome::Invalid("x"), BindingPolicy::Off),
373 Ok(None)
374 );
375 assert_eq!(
376 evaluate(&BindingOutcome::Bound { bls_pub: pk }, BindingPolicy::Off),
377 Ok(None)
378 );
379 }
380
381 #[test]
382 fn policy_opportunistic_accepts_absent_rejects_invalid() {
383 let pk = [2u8; 48];
384 assert_eq!(
385 evaluate(&BindingOutcome::Absent, BindingPolicy::Opportunistic),
386 Ok(None)
387 );
388 assert!(evaluate(
389 &BindingOutcome::Invalid("bad"),
390 BindingPolicy::Opportunistic
391 )
392 .is_err());
393 assert_eq!(
394 evaluate(
395 &BindingOutcome::Bound { bls_pub: pk },
396 BindingPolicy::Opportunistic
397 ),
398 Ok(Some(pk))
399 );
400 }
401
402 #[test]
403 fn policy_required_rejects_absent_and_invalid() {
404 let pk = [3u8; 48];
405 assert!(
406 evaluate(&BindingOutcome::Absent, BindingPolicy::Required).is_err(),
407 "anti-downgrade: a stripped extension is rejected in Required mode"
408 );
409 assert!(evaluate(&BindingOutcome::Invalid("bad"), BindingPolicy::Required).is_err());
410 assert_eq!(
411 evaluate(
412 &BindingOutcome::Bound { bls_pub: pk },
413 BindingPolicy::Required
414 ),
415 Ok(Some(pk))
416 );
417 }
418
419 #[test]
420 fn default_policy_is_opportunistic() {
421 assert_eq!(BindingPolicy::default(), BindingPolicy::Opportunistic);
422 }
423}