Skip to main content

dcrypt_sign/bls/
mod.rs

1//! BLS signatures over BLS12-381 with minimum-size public keys.
2//!
3//! This module implements the Basic, Message Augmentation, and Proof of
4//! Possession schemes from `draft-irtf-cfrg-bls-signature-07`. Public keys are
5//! 48-byte compressed G1 points and signatures are 96-byte compressed G2
6//! points. [`Eth2Bls12381G2PopV4`] is a deliberately separate adapter for the
7//! legacy draft-v4 profile retained by Ethereum consensus.
8//!
9//! The standard profile is pinned to
10//! [`draft-irtf-cfrg-bls-signature-07`](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bls-signature-07),
11//! published 6 July 2026. That draft's Appendix B still marks minimum-public-
12//! key vectors as TBA; dcrypt therefore combines published EIP-2333 v4 KeyGen
13//! vectors with byte-for-byte tests against an independent BLS12-381 oracle in
14//! the excluded verification workspace.
15//!
16//! ```
17//! use dcrypt_sign::bls::{Bls12381G2Basic, Bls12381SecretKey};
18//!
19//! // Production IKM must be at least 32 unpredictable bytes. Draft-07 also
20//! // requires the caller to choose the salt explicitly.
21//! let ikm = [7u8; 32];
22//! let secret = Bls12381SecretKey::key_gen(&ikm, b"example application salt")?;
23//! let public = secret.public_key()?;
24//! let signature = Bls12381G2Basic::sign(&secret, b"message")?;
25//! Bls12381G2Basic::verify(&public, b"message", &signature)?;
26//! # Ok::<(), dcrypt_api::Error>(())
27//! ```
28//!
29//! Secret keys are non-`Copy`, non-`Clone`, exact-width clearing owners. They
30//! never expose a plain byte-array serialization. The arithmetic bridge uses a
31//! fixed 256-bit scalar-multiplication schedule and explicitly clears scalar
32//! and byte temporaries; target-specific compiler inspection remains necessary
33//! for a concrete side-channel claim.
34
35#![forbid(unsafe_code)]
36
37use alloc::vec::Vec;
38use core::fmt;
39
40use dcrypt_algorithms::ec::bls12_381::{
41    pairing, Bls12_381Scalar, G1Affine, G1Projective, G2Affine, G2Projective, Gt,
42};
43use dcrypt_algorithms::hash::{sha2::Sha256, HashFunction};
44use dcrypt_algorithms::kdf::Hkdf;
45use dcrypt_api::{Error as ApiError, Result as ApiResult};
46use dcrypt_internal::{
47    random::try_fill_bytes_zeroing_on_error, CryptoRng, RngCore, Zeroize, ZeroizeOnDrop, Zeroizing,
48};
49
50/// Size of a canonical BLS12-381 secret scalar encoding.
51pub const BLS_SECRET_KEY_SIZE: usize = 32;
52/// Size of a compressed minimum-size BLS12-381 public key in G1.
53pub const BLS_PUBLIC_KEY_SIZE: usize = 48;
54/// Size of a compressed minimum-public-key BLS12-381 signature in G2.
55pub const BLS_SIGNATURE_SIZE: usize = 96;
56
57/// Draft-07 minimum-public-key Basic ciphersuite domain separation tag.
58pub const BLS_BASIC_G2_DST: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_";
59/// Draft-07 minimum-public-key Message Augmentation domain separation tag.
60pub const BLS_AUG_G2_DST: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_AUG_";
61/// Draft-07 minimum-public-key Proof of Possession signature tag.
62pub const BLS_POP_G2_DST: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_";
63/// Draft-07 minimum-public-key proof-generation tag, distinct from signatures.
64pub const BLS_POP_PROOF_G2_DST: &[u8] = b"BLS_POP_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_";
65
66const KEYGEN_V4_SALT_TAG: &[u8] = b"BLS-SIG-KEYGEN-SALT-";
67const KEYGEN_L: usize = 48;
68
69fn invalid_key(context: &'static str, _message: &'static str) -> ApiError {
70    ApiError::InvalidKey {
71        context,
72        #[cfg(feature = "std")]
73        message: _message.into(),
74    }
75}
76
77fn invalid_signature(context: &'static str, _message: &'static str) -> ApiError {
78    ApiError::InvalidSignature {
79        context,
80        #[cfg(feature = "std")]
81        message: _message.into(),
82    }
83}
84
85fn invalid_parameter(context: &'static str, _message: &'static str) -> ApiError {
86    ApiError::InvalidParameter {
87        context,
88        #[cfg(feature = "std")]
89        message: _message.into(),
90    }
91}
92
93/// Protected canonical nonzero BLS12-381 secret scalar.
94///
95/// This type deliberately implements neither `Copy` nor `Clone` and exposes no
96/// shared or mutable byte-slice trait. Use [`Self::to_bytes_zeroizing`] only
97/// when an explicit protected export is required.
98pub struct Bls12381SecretKey {
99    bytes: Zeroizing<[u8; BLS_SECRET_KEY_SIZE]>,
100}
101
102impl fmt::Debug for Bls12381SecretKey {
103    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
104        formatter.write_str("Bls12381SecretKey([REDACTED])")
105    }
106}
107
108impl Zeroize for Bls12381SecretKey {
109    fn zeroize(&mut self) {
110        self.bytes.zeroize();
111    }
112}
113
114impl Drop for Bls12381SecretKey {
115    fn drop(&mut self) {
116        self.zeroize();
117    }
118}
119
120impl ZeroizeOnDrop for Bls12381SecretKey {}
121
122impl Bls12381SecretKey {
123    /// Import a canonical 32-byte big-endian scalar in the range `1..r`.
124    pub fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
125        if bytes.len() != BLS_SECRET_KEY_SIZE {
126            return Err(ApiError::InvalidLength {
127                context: "Bls12381SecretKey::from_bytes",
128                expected: BLS_SECRET_KEY_SIZE,
129                actual: bytes.len(),
130            });
131        }
132
133        let mut protected = Zeroizing::new([0u8; BLS_SECRET_KEY_SIZE]);
134        protected.copy_from_slice(bytes);
135        if !bool::from(Bls12_381Scalar::secret_key_bytes_are_valid(&protected)) {
136            return Err(invalid_key(
137                "Bls12381SecretKey::from_bytes",
138                "secret scalar must be canonical and nonzero",
139            ));
140        }
141        Ok(Self { bytes: protected })
142    }
143
144    /// Draft-07 KeyGen with the optional `key_info` value defaulting to empty.
145    ///
146    /// `ikm` must contain at least 32 unpredictable bytes. Draft-07 requires
147    /// callers to choose and supply `salt`; an empty salt is permitted by HKDF,
148    /// but it is still an explicit caller choice.
149    pub fn key_gen(ikm: &[u8], salt: &[u8]) -> ApiResult<Self> {
150        Self::key_gen_with_info(ikm, salt, &[])
151    }
152
153    /// Draft-07 KeyGen with explicit caller-required `salt` and `key_info`.
154    pub fn key_gen_with_info(ikm: &[u8], salt: &[u8], key_info: &[u8]) -> ApiResult<Self> {
155        Self::key_gen_inner(ikm, salt, key_info, |current_salt, protected_ikm, info| {
156            Hkdf::<Sha256>::derive(Some(current_salt), protected_ikm, Some(info), KEYGEN_L)
157                .map_err(ApiError::from)
158        })
159    }
160
161    /// Generate 32 bytes of IKM from a caller-supplied cryptographic RNG, then
162    /// apply draft-07 KeyGen with an empty `key_info` value.
163    pub fn generate<R: CryptoRng + RngCore>(rng: &mut R, salt: &[u8]) -> ApiResult<Self> {
164        Self::generate_with_info(rng, salt, &[])
165    }
166
167    /// Generate IKM from a caller-supplied RNG and apply draft-07 KeyGen.
168    pub fn generate_with_info<R: CryptoRng + RngCore>(
169        rng: &mut R,
170        salt: &[u8],
171        key_info: &[u8],
172    ) -> ApiResult<Self> {
173        let mut ikm = Zeroizing::new([0u8; 32]);
174        try_fill_bytes_zeroing_on_error(rng, &mut *ikm).map_err(|_| {
175            ApiError::RandomGenerationError {
176                context: "Bls12381SecretKey::generate",
177                #[cfg(feature = "std")]
178                message: "caller-provided randomness source failed".into(),
179            }
180        })?;
181        Self::key_gen_with_info(&*ikm, salt, key_info)
182    }
183
184    fn key_gen_inner<F>(ikm: &[u8], salt: &[u8], key_info: &[u8], mut derive: F) -> ApiResult<Self>
185    where
186        F: FnMut(&[u8], &[u8], &[u8]) -> ApiResult<dcrypt_internal::zeroing::ZeroizingBytes>,
187    {
188        if ikm.len() < 32 {
189            return Err(ApiError::InvalidLength {
190                context: "Bls12381SecretKey::key_gen IKM",
191                expected: 32,
192                actual: ikm.len(),
193            });
194        }
195
196        let ikm_len = ikm.len().checked_add(1).ok_or_else(|| {
197            invalid_parameter("Bls12381SecretKey::key_gen", "IKM length overflow")
198        })?;
199        let mut protected_ikm =
200            Zeroizing::new(dcrypt_internal::zeroing::boxed_bytes_zeroed(ikm_len));
201        protected_ikm[..ikm.len()].copy_from_slice(ikm);
202
203        let info_len = key_info.len().checked_add(2).ok_or_else(|| {
204            invalid_parameter("Bls12381SecretKey::key_gen", "key_info length overflow")
205        })?;
206        let mut info = Vec::with_capacity(info_len);
207        info.extend_from_slice(key_info);
208        info.extend_from_slice(&(KEYGEN_L as u16).to_be_bytes());
209
210        let mut current_salt = salt.to_vec();
211        loop {
212            let okm = derive(&current_salt, &protected_ikm, &info)?;
213            if okm.len() != KEYGEN_L {
214                return Err(invalid_parameter(
215                    "Bls12381SecretKey::key_gen",
216                    "HKDF returned an unexpected output length",
217                ));
218            }
219
220            let mut scalar =
221                Bls12_381Scalar::from_be_bytes_mod_order(&okm).map_err(ApiError::from)?;
222            if !bool::from(scalar.is_zero()) {
223                let bytes = scalar.to_be_bytes_zeroizing();
224                scalar.zeroize();
225                return Ok(Self { bytes });
226            }
227            scalar.zeroize();
228
229            let mut digest = Sha256::digest(&current_salt).map_err(ApiError::from)?;
230            current_salt.clear();
231            current_salt.extend_from_slice(digest.as_ref());
232            digest.zeroize();
233        }
234    }
235
236    /// Derive the canonical minimum-size public key in G1.
237    pub fn public_key(&self) -> ApiResult<Bls12381PublicKey> {
238        let point = G1Projective::generator()
239            .multiply_secret_be_bytes(&self.bytes)
240            .map_err(ApiError::from)?;
241        Bls12381PublicKey::from_point(point)
242    }
243
244    /// Export the canonical big-endian scalar in a clearing fixed-width owner.
245    pub fn to_bytes_zeroizing(&self) -> Zeroizing<[u8; BLS_SECRET_KEY_SIZE]> {
246        self.bytes.clone()
247    }
248
249    fn bytes(&self) -> &[u8; BLS_SECRET_KEY_SIZE] {
250        &self.bytes
251    }
252}
253
254/// Canonical, nonidentity, prime-subgroup minimum-size public key in G1.
255#[derive(Clone, Eq, PartialEq)]
256pub struct Bls12381PublicKey {
257    bytes: [u8; BLS_PUBLIC_KEY_SIZE],
258}
259
260impl fmt::Debug for Bls12381PublicKey {
261    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
262        formatter
263            .debug_struct("Bls12381PublicKey")
264            .field("length", &BLS_PUBLIC_KEY_SIZE)
265            .finish()
266    }
267}
268
269impl Bls12381PublicKey {
270    /// Parse a canonical, nonidentity G1 point in the prime-order subgroup.
271    pub fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
272        if bytes.len() != BLS_PUBLIC_KEY_SIZE {
273            return Err(ApiError::InvalidLength {
274                context: "Bls12381PublicKey::from_bytes",
275                expected: BLS_PUBLIC_KEY_SIZE,
276                actual: bytes.len(),
277            });
278        }
279        G1Projective::from_bytes_validated(bytes).map_err(|_| {
280            invalid_key(
281                "Bls12381PublicKey::from_bytes",
282                "invalid encoding, identity, or point outside the subgroup",
283            )
284        })?;
285        let encoded: [u8; BLS_PUBLIC_KEY_SIZE] = bytes.try_into().map_err(|_| {
286            invalid_key(
287                "Bls12381PublicKey::from_bytes",
288                "validated public-key length changed unexpectedly",
289            )
290        })?;
291        Ok(Self { bytes: encoded })
292    }
293
294    /// Return the canonical compressed encoding.
295    pub fn to_bytes(&self) -> [u8; BLS_PUBLIC_KEY_SIZE] {
296        self.bytes
297    }
298
299    /// Validate an external public-key encoding without retaining it.
300    pub fn key_validate(bytes: &[u8]) -> bool {
301        Self::from_bytes(bytes).is_ok()
302    }
303
304    /// Aggregate one or more validated public keys, rejecting an identity sum.
305    pub fn aggregate(public_keys: &[Self]) -> ApiResult<Self> {
306        let first = public_keys.first().ok_or_else(|| {
307            invalid_parameter(
308                "Bls12381PublicKey::aggregate",
309                "at least one public key is required",
310            )
311        })?;
312        let mut aggregate = first.point()?;
313        for public_key in &public_keys[1..] {
314            aggregate += public_key.point()?;
315        }
316        Self::from_point(aggregate)
317    }
318
319    fn point(&self) -> ApiResult<G1Projective> {
320        G1Projective::from_bytes_validated(&self.bytes).map_err(|_| {
321            invalid_key(
322                "Bls12381PublicKey",
323                "stored public-key invariant was violated",
324            )
325        })
326    }
327
328    fn from_point(point: G1Projective) -> ApiResult<Self> {
329        if bool::from(point.is_identity()) {
330            return Err(invalid_key(
331                "Bls12381PublicKey",
332                "aggregate public key is the identity",
333            ));
334        }
335        Ok(Self {
336            bytes: point.to_bytes(),
337        })
338    }
339}
340
341/// Canonical prime-subgroup signature in G2.
342///
343/// The canonical identity encoding is accepted, as required by the draft's
344/// `signature_to_point` contract. Ordinary verification rejects it through the
345/// pairing equation. The Ethereum adapter exposes its one specified empty-set
346/// exception explicitly.
347#[derive(Clone, Eq, PartialEq)]
348pub struct Bls12381Signature {
349    bytes: [u8; BLS_SIGNATURE_SIZE],
350}
351
352impl fmt::Debug for Bls12381Signature {
353    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
354        formatter
355            .debug_struct("Bls12381Signature")
356            .field("length", &BLS_SIGNATURE_SIZE)
357            .finish()
358    }
359}
360
361impl Bls12381Signature {
362    /// Parse a canonical G2 point and enforce prime-order subgroup membership.
363    pub fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
364        let encoded: [u8; BLS_SIGNATURE_SIZE] =
365            bytes.try_into().map_err(|_| ApiError::InvalidLength {
366                context: "Bls12381Signature::from_bytes",
367                expected: BLS_SIGNATURE_SIZE,
368                actual: bytes.len(),
369            })?;
370        G2Projective::from_bytes(&encoded)
371            .into_option()
372            .ok_or_else(|| {
373                invalid_signature(
374                    "Bls12381Signature::from_bytes",
375                    "invalid encoding or point outside the subgroup",
376                )
377            })?;
378        Ok(Self { bytes: encoded })
379    }
380
381    /// Return the canonical compressed encoding.
382    pub fn to_bytes(&self) -> [u8; BLS_SIGNATURE_SIZE] {
383        self.bytes
384    }
385
386    /// Return whether this is the canonical G2 identity.
387    pub fn is_identity(&self) -> bool {
388        self.point()
389            .map(|point| bool::from(point.is_identity()))
390            .unwrap_or(false)
391    }
392
393    /// Aggregate one or more canonical subgroup signatures.
394    pub fn aggregate(signatures: &[Self]) -> ApiResult<Self> {
395        let first = signatures.first().ok_or_else(|| {
396            invalid_parameter(
397                "Bls12381Signature::aggregate",
398                "at least one signature is required",
399            )
400        })?;
401        let mut aggregate = first.point()?;
402        for signature in &signatures[1..] {
403            aggregate += signature.point()?;
404        }
405        Ok(Self::from_point(aggregate))
406    }
407
408    fn point(&self) -> ApiResult<G2Projective> {
409        G2Projective::from_bytes(&self.bytes)
410            .into_option()
411            .ok_or_else(|| {
412                invalid_signature(
413                    "Bls12381Signature",
414                    "stored signature invariant was violated",
415                )
416            })
417    }
418
419    fn from_point(point: G2Projective) -> Self {
420        Self {
421            bytes: point.to_bytes(),
422        }
423    }
424}
425
426/// Canonical prime-subgroup proof of possession in G2.
427#[derive(Clone, Eq, PartialEq)]
428pub struct Bls12381ProofOfPossession {
429    bytes: [u8; BLS_SIGNATURE_SIZE],
430}
431
432impl fmt::Debug for Bls12381ProofOfPossession {
433    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
434        formatter
435            .debug_struct("Bls12381ProofOfPossession")
436            .field("length", &BLS_SIGNATURE_SIZE)
437            .finish()
438    }
439}
440
441impl Bls12381ProofOfPossession {
442    /// Parse a canonical G2 proof and enforce subgroup membership.
443    pub fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
444        let signature = Bls12381Signature::from_bytes(bytes)?;
445        Ok(Self {
446            bytes: signature.bytes,
447        })
448    }
449
450    /// Return the canonical compressed encoding.
451    pub fn to_bytes(&self) -> [u8; BLS_SIGNATURE_SIZE] {
452        self.bytes
453    }
454
455    fn signature(&self) -> Bls12381Signature {
456        Bls12381Signature { bytes: self.bytes }
457    }
458}
459
460#[derive(Clone, Copy)]
461enum MessageMode {
462    Raw,
463    Augmented,
464}
465
466fn message_equal(
467    mode: MessageMode,
468    public_keys: &[Bls12381PublicKey],
469    messages: &[&[u8]],
470    left: usize,
471    right: usize,
472) -> bool {
473    match mode {
474        MessageMode::Raw => messages[left] == messages[right],
475        MessageMode::Augmented => {
476            public_keys[left] == public_keys[right] && messages[left] == messages[right]
477        }
478    }
479}
480
481fn hash_message(
482    mode: MessageMode,
483    public_key: &Bls12381PublicKey,
484    message: &[u8],
485    dst: &[u8],
486) -> ApiResult<G2Projective> {
487    match mode {
488        MessageMode::Raw => G2Projective::hash_to_curve(message, dst).map_err(ApiError::from),
489        MessageMode::Augmented => {
490            let length = BLS_PUBLIC_KEY_SIZE
491                .checked_add(message.len())
492                .ok_or_else(|| {
493                    invalid_parameter("BLS message augmentation", "message length overflow")
494                })?;
495            let mut augmented = Vec::with_capacity(length);
496            augmented.extend_from_slice(&public_key.bytes);
497            augmented.extend_from_slice(message);
498            G2Projective::hash_to_curve(&augmented, dst).map_err(ApiError::from)
499        }
500    }
501}
502
503fn core_sign(
504    secret_key: &Bls12381SecretKey,
505    public_key: &Bls12381PublicKey,
506    message: &[u8],
507    dst: &[u8],
508    mode: MessageMode,
509) -> ApiResult<Bls12381Signature> {
510    let message_point = hash_message(mode, public_key, message, dst)?;
511    let signature = message_point
512        .multiply_secret_be_bytes(secret_key.bytes())
513        .map_err(ApiError::from)?;
514    Ok(Bls12381Signature::from_point(signature))
515}
516
517fn core_verify(
518    public_key: &Bls12381PublicKey,
519    message: &[u8],
520    signature: &Bls12381Signature,
521    dst: &[u8],
522    mode: MessageMode,
523) -> ApiResult<()> {
524    let public_key_point = G1Affine::from(public_key.point()?);
525    let signature_point = G2Affine::from(signature.point()?);
526    let message_point = G2Affine::from(hash_message(mode, public_key, message, dst)?);
527
528    if pairing(&public_key_point, &message_point)
529        == pairing(&G1Affine::generator(), &signature_point)
530    {
531        Ok(())
532    } else {
533        Err(invalid_signature(
534            "BLS verification",
535            "pairing equation failed",
536        ))
537    }
538}
539
540fn validate_parallel_inputs(
541    context: &'static str,
542    public_keys: &[Bls12381PublicKey],
543    messages: &[&[u8]],
544) -> ApiResult<()> {
545    if public_keys.is_empty() {
546        return Err(invalid_parameter(
547            context,
548            "at least one public key and message is required",
549        ));
550    }
551    if public_keys.len() != messages.len() {
552        return Err(invalid_parameter(
553            context,
554            "public-key and message counts differ",
555        ));
556    }
557    Ok(())
558}
559
560fn core_aggregate_verify(
561    public_keys: &[Bls12381PublicKey],
562    messages: &[&[u8]],
563    signature: &Bls12381Signature,
564    dst: &[u8],
565    mode: MessageMode,
566) -> ApiResult<()> {
567    validate_parallel_inputs("BLS aggregate verification", public_keys, messages)?;
568
569    let mut product = Gt::identity();
570    for index in 0..public_keys.len() {
571        if (0..index).any(|previous| message_equal(mode, public_keys, messages, previous, index)) {
572            continue;
573        }
574
575        let mut aggregate_public_key = public_keys[index].point()?;
576        let mut group_len = 1usize;
577        for next in (index + 1)..public_keys.len() {
578            if message_equal(mode, public_keys, messages, index, next) {
579                aggregate_public_key += public_keys[next].point()?;
580                group_len += 1;
581            }
582        }
583
584        // Draft-07 validates each grouped aggregate public key. This prevents
585        // splitting-zero groups even though every component key is valid.
586        if group_len > 1 && bool::from(aggregate_public_key.is_identity()) {
587            return Err(invalid_key(
588                "BLS aggregate verification",
589                "same-message aggregate public key is the identity",
590            ));
591        }
592
593        let message_point = G2Affine::from(hash_message(
594            mode,
595            &public_keys[index],
596            messages[index],
597            dst,
598        )?);
599        product += pairing(&G1Affine::from(aggregate_public_key), &message_point);
600    }
601
602    let signature_point = G2Affine::from(signature.point()?);
603    let expected = pairing(&G1Affine::generator(), &signature_point);
604    if product == expected {
605        Ok(())
606    } else {
607        Err(invalid_signature(
608            "BLS aggregate verification",
609            "aggregate pairing equation failed",
610        ))
611    }
612}
613
614/// Draft-07 minimum-public-key Basic scheme.
615pub struct Bls12381G2Basic;
616
617impl Bls12381G2Basic {
618    /// Deterministically sign with the Basic ciphersuite.
619    pub fn sign(secret_key: &Bls12381SecretKey, message: &[u8]) -> ApiResult<Bls12381Signature> {
620        let public_key = secret_key.public_key()?;
621        core_sign(
622            secret_key,
623            &public_key,
624            message,
625            BLS_BASIC_G2_DST,
626            MessageMode::Raw,
627        )
628    }
629
630    /// Verify one Basic signature.
631    pub fn verify(
632        public_key: &Bls12381PublicKey,
633        message: &[u8],
634        signature: &Bls12381Signature,
635    ) -> ApiResult<()> {
636        core_verify(
637            public_key,
638            message,
639            signature,
640            BLS_BASIC_G2_DST,
641            MessageMode::Raw,
642        )
643    }
644
645    /// Aggregate one or more signatures.
646    pub fn aggregate(signatures: &[Bls12381Signature]) -> ApiResult<Bls12381Signature> {
647        Bls12381Signature::aggregate(signatures)
648    }
649
650    /// Verify a Basic aggregate, rejecting every repeated message.
651    pub fn aggregate_verify(
652        public_keys: &[Bls12381PublicKey],
653        messages: &[&[u8]],
654        signature: &Bls12381Signature,
655    ) -> ApiResult<()> {
656        validate_parallel_inputs("BLS Basic aggregate verification", public_keys, messages)?;
657        for left in 0..messages.len() {
658            for right in (left + 1)..messages.len() {
659                if messages[left] == messages[right] {
660                    return Err(invalid_parameter(
661                        "BLS Basic aggregate verification",
662                        "Basic scheme messages must be distinct",
663                    ));
664                }
665            }
666        }
667        core_aggregate_verify(
668            public_keys,
669            messages,
670            signature,
671            BLS_BASIC_G2_DST,
672            MessageMode::Raw,
673        )
674    }
675}
676
677/// Draft-07 minimum-public-key Message Augmentation scheme.
678pub struct Bls12381G2MessageAugmentation;
679
680impl Bls12381G2MessageAugmentation {
681    /// Sign `PK || message` with the Message Augmentation ciphersuite.
682    pub fn sign(secret_key: &Bls12381SecretKey, message: &[u8]) -> ApiResult<Bls12381Signature> {
683        let public_key = secret_key.public_key()?;
684        core_sign(
685            secret_key,
686            &public_key,
687            message,
688            BLS_AUG_G2_DST,
689            MessageMode::Augmented,
690        )
691    }
692
693    /// Verify a Message Augmentation signature over `PK || message`.
694    pub fn verify(
695        public_key: &Bls12381PublicKey,
696        message: &[u8],
697        signature: &Bls12381Signature,
698    ) -> ApiResult<()> {
699        core_verify(
700            public_key,
701            message,
702            signature,
703            BLS_AUG_G2_DST,
704            MessageMode::Augmented,
705        )
706    }
707
708    /// Aggregate one or more signatures.
709    pub fn aggregate(signatures: &[Bls12381Signature]) -> ApiResult<Bls12381Signature> {
710        Bls12381Signature::aggregate(signatures)
711    }
712
713    /// Verify an augmented aggregate. Duplicate raw messages are permitted
714    /// because the signed input includes each public-key encoding.
715    pub fn aggregate_verify(
716        public_keys: &[Bls12381PublicKey],
717        messages: &[&[u8]],
718        signature: &Bls12381Signature,
719    ) -> ApiResult<()> {
720        core_aggregate_verify(
721            public_keys,
722            messages,
723            signature,
724            BLS_AUG_G2_DST,
725            MessageMode::Augmented,
726        )
727    }
728}
729
730/// Draft-07 minimum-public-key Proof of Possession scheme.
731pub struct Bls12381G2ProofOfPossession;
732
733impl Bls12381G2ProofOfPossession {
734    /// Sign with the Proof of Possession signature ciphersuite.
735    pub fn sign(secret_key: &Bls12381SecretKey, message: &[u8]) -> ApiResult<Bls12381Signature> {
736        let public_key = secret_key.public_key()?;
737        core_sign(
738            secret_key,
739            &public_key,
740            message,
741            BLS_POP_G2_DST,
742            MessageMode::Raw,
743        )
744    }
745
746    /// Produce a proof over the canonical public-key encoding using the
747    /// distinct `BLS_POP_...` proof domain.
748    pub fn pop_prove(secret_key: &Bls12381SecretKey) -> ApiResult<Bls12381ProofOfPossession> {
749        let public_key = secret_key.public_key()?;
750        let message_point = G2Projective::hash_to_curve(&public_key.bytes, BLS_POP_PROOF_G2_DST)
751            .map_err(ApiError::from)?;
752        let proof = message_point
753            .multiply_secret_be_bytes(secret_key.bytes())
754            .map_err(ApiError::from)?;
755        Ok(Bls12381ProofOfPossession {
756            bytes: proof.to_bytes(),
757        })
758    }
759
760    /// Validate a proof of possession for a public key.
761    pub fn pop_verify(
762        public_key: &Bls12381PublicKey,
763        proof: &Bls12381ProofOfPossession,
764    ) -> ApiResult<()> {
765        core_verify(
766            public_key,
767            &public_key.bytes,
768            &proof.signature(),
769            BLS_POP_PROOF_G2_DST,
770            MessageMode::Raw,
771        )
772    }
773
774    /// Verify a PoP signature after validating the accompanying proof.
775    pub fn verify(
776        public_key: &Bls12381PublicKey,
777        proof: &Bls12381ProofOfPossession,
778        message: &[u8],
779        signature: &Bls12381Signature,
780    ) -> ApiResult<()> {
781        Self::pop_verify(public_key, proof)?;
782        core_verify(
783            public_key,
784            message,
785            signature,
786            BLS_POP_G2_DST,
787            MessageMode::Raw,
788        )
789    }
790
791    /// Aggregate one or more signatures.
792    pub fn aggregate(signatures: &[Bls12381Signature]) -> ApiResult<Bls12381Signature> {
793        Bls12381Signature::aggregate(signatures)
794    }
795
796    /// Verify an aggregate after validating one proof per public key.
797    pub fn aggregate_verify(
798        public_keys: &[Bls12381PublicKey],
799        proofs: &[Bls12381ProofOfPossession],
800        messages: &[&[u8]],
801        signature: &Bls12381Signature,
802    ) -> ApiResult<()> {
803        validate_parallel_inputs("BLS PoP aggregate verification", public_keys, messages)?;
804        if public_keys.len() != proofs.len() {
805            return Err(invalid_parameter(
806                "BLS PoP aggregate verification",
807                "public-key and proof counts differ",
808            ));
809        }
810        for (public_key, proof) in public_keys.iter().zip(proofs) {
811            Self::pop_verify(public_key, proof)?;
812        }
813        core_aggregate_verify(
814            public_keys,
815            messages,
816            signature,
817            BLS_POP_G2_DST,
818            MessageMode::Raw,
819        )
820    }
821
822    /// Verify an aggregate over one shared message after validating every
823    /// proof of possession. The empty input is rejected by draft-07.
824    pub fn fast_aggregate_verify(
825        public_keys: &[Bls12381PublicKey],
826        proofs: &[Bls12381ProofOfPossession],
827        message: &[u8],
828        signature: &Bls12381Signature,
829    ) -> ApiResult<()> {
830        if public_keys.is_empty() {
831            return Err(invalid_parameter(
832                "BLS PoP fast aggregate verification",
833                "at least one public key is required",
834            ));
835        }
836        if public_keys.len() != proofs.len() {
837            return Err(invalid_parameter(
838                "BLS PoP fast aggregate verification",
839                "public-key and proof counts differ",
840            ));
841        }
842        for (public_key, proof) in public_keys.iter().zip(proofs) {
843            Self::pop_verify(public_key, proof)?;
844        }
845        let aggregate_public_key = Bls12381PublicKey::aggregate(public_keys)?;
846        core_verify(
847            &aggregate_public_key,
848            message,
849            signature,
850            BLS_POP_G2_DST,
851            MessageMode::Raw,
852        )
853    }
854}
855
856/// Ethereum consensus adapter for the minimum-public-key draft-v4 PoP profile.
857///
858/// This adapter keeps Ethereum's POP signature DST and v4-compatible KeyGen
859/// salt behavior. Unlike the draft-07 PoP API, verification methods do not take
860/// proofs: Ethereum relies on protocol registration to establish possession.
861/// Callers outside that context should use [`Bls12381G2ProofOfPossession`].
862pub struct Eth2Bls12381G2PopV4;
863
864impl Eth2Bls12381G2PopV4 {
865    /// Draft-v4-compatible KeyGen, using `SHA-256("BLS-SIG-KEYGEN-SALT-")`
866    /// as the first HKDF salt and an empty `key_info` value.
867    pub fn key_gen(ikm: &[u8]) -> ApiResult<Bls12381SecretKey> {
868        Self::key_gen_with_info(ikm, &[])
869    }
870
871    /// Draft-v4-compatible KeyGen with explicit `key_info`.
872    pub fn key_gen_with_info(ikm: &[u8], key_info: &[u8]) -> ApiResult<Bls12381SecretKey> {
873        let mut salt = Sha256::digest(KEYGEN_V4_SALT_TAG).map_err(ApiError::from)?;
874        let result = Bls12381SecretKey::key_gen_with_info(ikm, salt.as_ref(), key_info);
875        salt.zeroize();
876        result
877    }
878
879    /// Generate v4-compatible IKM from a caller-provided cryptographic RNG.
880    pub fn generate<R: CryptoRng + RngCore>(rng: &mut R) -> ApiResult<Bls12381SecretKey> {
881        let mut ikm = Zeroizing::new([0u8; 32]);
882        try_fill_bytes_zeroing_on_error(rng, &mut *ikm).map_err(|_| {
883            ApiError::RandomGenerationError {
884                context: "Eth2Bls12381G2PopV4::generate",
885                #[cfg(feature = "std")]
886                message: "caller-provided randomness source failed".into(),
887            }
888        })?;
889        Self::key_gen(&*ikm)
890    }
891
892    /// Sign using Ethereum's draft-v4 POP signature DST.
893    pub fn sign(secret_key: &Bls12381SecretKey, message: &[u8]) -> ApiResult<Bls12381Signature> {
894        Bls12381G2ProofOfPossession::sign(secret_key, message)
895    }
896
897    /// Verify under the Ethereum registration precondition.
898    pub fn verify(
899        public_key: &Bls12381PublicKey,
900        message: &[u8],
901        signature: &Bls12381Signature,
902    ) -> ApiResult<()> {
903        core_verify(
904            public_key,
905            message,
906            signature,
907            BLS_POP_G2_DST,
908            MessageMode::Raw,
909        )
910    }
911
912    /// Aggregate one or more Ethereum-profile signatures.
913    pub fn aggregate(signatures: &[Bls12381Signature]) -> ApiResult<Bls12381Signature> {
914        Bls12381Signature::aggregate(signatures)
915    }
916
917    /// Verify a draft-v4 POP aggregate under Ethereum's registration
918    /// precondition.
919    pub fn aggregate_verify(
920        public_keys: &[Bls12381PublicKey],
921        messages: &[&[u8]],
922        signature: &Bls12381Signature,
923    ) -> ApiResult<()> {
924        core_aggregate_verify(
925            public_keys,
926            messages,
927            signature,
928            BLS_POP_G2_DST,
929            MessageMode::Raw,
930        )
931    }
932
933    /// Ethereum's `AggregatePKs` extension. Inputs must be nonempty and the
934    /// aggregate must remain a valid nonidentity public key.
935    pub fn aggregate_public_keys(
936        public_keys: &[Bls12381PublicKey],
937    ) -> ApiResult<Bls12381PublicKey> {
938        Bls12381PublicKey::aggregate(public_keys)
939    }
940
941    /// Ethereum's `eth_fast_aggregate_verify` wrapper.
942    ///
943    /// Its sole empty-set exception accepts exactly the canonical G2 identity.
944    /// Every nonempty call follows draft-v4 FastAggregateVerify and assumes the
945    /// keys were possession-validated during protocol registration.
946    pub fn fast_aggregate_verify(
947        public_keys: &[Bls12381PublicKey],
948        message: &[u8; 32],
949        signature: &Bls12381Signature,
950    ) -> ApiResult<()> {
951        if public_keys.is_empty() {
952            return if signature.is_identity() {
953                Ok(())
954            } else {
955                Err(invalid_signature(
956                    "Eth2 fast aggregate verification",
957                    "empty public-key input requires the G2 identity signature",
958                ))
959            };
960        }
961
962        let aggregate_public_key = Bls12381PublicKey::aggregate(public_keys)?;
963        core_verify(
964            &aggregate_public_key,
965            message,
966            signature,
967            BLS_POP_G2_DST,
968            MessageMode::Raw,
969        )
970    }
971}
972
973#[cfg(test)]
974mod tests;