reach_signatures/
lib.rs

1// SPDX-FileCopyrightText: 2024-2025 eaon <eaon@posteo.net>
2// SPDX-License-Identifier: EUPL-1.2
3
4#![cfg_attr(docsrs, feature(doc_auto_cfg))]
5#![allow(clippy::needless_doctest_main)]
6#![doc = include_str!("../README.md")]
7#![warn(missing_docs)]
8
9use std::ops::Deref;
10
11use ed25519_dalek::{Context, DigestSigner, DigestVerifier};
12use fn_dsa_sign::SigningKey;
13use fn_dsa_vrfy::VerifyingKey;
14use rand_core::CryptoRngCore;
15use sha3::digest::core_api::CoreWrapper;
16use sha3::{Digest, Sha3_512, Sha3_512Core, digest::consts::U64};
17
18use reach_aliases::*;
19
20mod aliases;
21pub use aliases::*;
22
23/// Convert data structures to SHA3-512 digests.
24///
25/// This trait provides a standardized way to convert any data structure into
26/// a cryptographic digest suitable for signing or verification operations.
27pub trait Digestible {
28    /// Return a vector of byte references that represent this data structure.
29    ///
30    /// The returned bytes will be hashed in order to create the digest.
31    fn hashable_bytes(&self) -> Vec<Box<dyn AsRef<[u8]> + '_>>;
32
33    /// Generate a SHA3-512 digest from the hashable bytes.
34    fn digest(&self) -> CoreWrapper<Sha3_512Core> {
35        let mut hasher = <Sha3_512 as Digest>::new();
36        for bytes in self.hashable_bytes() {
37            hasher.update(bytes.deref());
38        }
39
40        hasher
41    }
42
43    /// Generate a finalized SHA3-512 digest as a 64-byte array.
44    fn finalized_digest(&self) -> [u8; 64] {
45        self.digest().finalize().into()
46    }
47}
48
49impl Digestible for &str {
50    fn hashable_bytes(&self) -> Vec<Box<dyn AsRef<[u8]> + '_>> {
51        vec![Box::new(self)]
52    }
53}
54
55impl<T> Digestible for [T]
56where
57    T: Digestible,
58{
59    fn hashable_bytes(&self) -> Vec<Box<dyn AsRef<[u8]> + '_>> {
60        self.iter().flat_map(Digestible::hashable_bytes).collect()
61    }
62}
63
64/// Sign and convert to the respective signed variant.
65///
66/// This trait extends [`Digestible`] to provide signing functionality, allowing
67/// a type to be converted into a signed version of itself.
68pub trait Signable: Digestible {
69    /// The type that results from signing this data structure.
70    type SignedType: Signatures;
71
72    /// Return the signing context for domain separation.
73    ///
74    /// Uses the type name of the signed type to ensure different data
75    /// structures have different signing domains.
76    fn context() -> &'static [u8] {
77        std::any::type_name::<Self::SignedType>().as_bytes()
78    }
79
80    /// Combine this with signatures to create the signed variant.
81    fn with_signature(
82        self,
83        ec_signature: Ed25519Signature,
84        pq_signature: FnDsaSignature,
85    ) -> Self::SignedType;
86}
87
88/// Signing key operations.
89///
90/// This trait provides access to both Ed25519 and FN-DSA signing keys and
91/// implements a dual-signature scheme.
92pub trait Sign {
93    /// Access to the Ed25519 signing key.
94    fn ec_signing_key(&self) -> &Ed25519Signing;
95    /// Access to the FN-DSA signing key.
96    fn pq_signing_key(&self) -> &FnDsaSigning;
97
98    /// Create an Ed25519 signing context with domain separation.
99    fn ec_signing_context<'a, 'b>(&'a self, context: &'b [u8]) -> Context<'a, 'b, Ed25519Signing> {
100        self.ec_signing_key()
101            .with_context(context)
102            .expect("Failed to set Ed25519 signing context")
103    }
104
105    /// Sign a digest using Ed25519 with the given context.
106    fn ec_sign_digest<D>(&self, context: &[u8], digest: D) -> Ed25519Signature
107    where
108        D: Digest<OutputSize = U64>,
109    {
110        self.ec_signing_context(context).sign_digest(digest)
111    }
112
113    /// Sign a digest using FN-DSA with the given context.
114    fn pq_sign_digest(
115        &self,
116        context: &FnDsaDomainContext<'_>,
117        digest: &[u8],
118        csprng: &mut impl CryptoRngCore,
119    ) -> FnDsaSignature {
120        let mut pq_signing_key =
121            FnDsaSigningDecoded::decode(self.pq_signing_key().inner.as_slice())
122                .expect("Failed to decode FN-DSA Signing Key");
123        let mut pq_signature = FN_DSA_SIGNATURE_EMPTY;
124
125        pq_signing_key.sign(csprng, context, &FN_DSA_HASH_ID, digest, &mut pq_signature);
126
127        pq_signature
128    }
129
130    /// Generate both Ed25519 and FN-DSA signatures for a digest.
131    fn sign_digest<D>(
132        &self,
133        context: &[u8],
134        digest: D,
135        csprng: &mut impl CryptoRngCore,
136    ) -> (Ed25519Signature, FnDsaSignature)
137    where
138        D: Digest<OutputSize = U64> + Clone,
139    {
140        let digest_bytes = digest.clone().finalize();
141        let ec_signature = self.ec_sign_digest(context, digest);
142        let pq_signature =
143            self.pq_sign_digest(&FnDsaDomainContext { 0: context }, &digest_bytes, csprng);
144
145        (ec_signature, pq_signature)
146    }
147
148    /// Sign a [`Signable`] data structure.
149    ///
150    /// This is the main signing interface that combines the source type with
151    /// both Ed25519 and FN-DSA signatures.
152    fn sign<S, V>(&self, signable: S, csprng: &mut impl CryptoRngCore) -> S::SignedType
153    where
154        S: Signable + Digestible,
155        V: Verifies<S::SignedType> + Verifier,
156        Self: VerifyingKeys<V>,
157    {
158        let (ec_signature, pq_signature) =
159            self.sign_digest(S::context(), signable.digest(), csprng);
160
161        signable.with_signature(ec_signature, pq_signature)
162    }
163}
164
165/// Carries signatures.
166pub trait Signatures {
167    /// Access to the Ed25519 signature.
168    fn ec_signature(&self) -> &Ed25519Signature;
169    /// Access to the FN-DSA signature.
170    fn pq_signature(&self) -> &FnDsaSignature;
171
172    /// Type name string as bytes, used for domain separation.
173    fn context() -> &'static [u8] {
174        std::any::type_name::<Self>().as_bytes()
175    }
176}
177
178/// Links signing keys to their corresponding verifying keys.
179pub trait VerifyingKeys<V: Verifier> {}
180
181/// Indicating which types can be verified by a given verifier.
182pub trait Verifies<V> {}
183
184/// Provides verifying keys for signature verification operations.
185pub trait Verifier {
186    /// Access to the Ed25519 verifying key.
187    fn ec_verifying_key(&self) -> &Ed25519Verifying;
188    /// Access to the FN-DSA verifying key.
189    fn pq_verifying_key(&self) -> &FnDsaVerifying;
190}
191
192fn ec_verifying_context<'a, 'b, V>(
193    verifier: &'a V,
194    context: &'b [u8],
195) -> Context<'a, 'b, Ed25519Verifying>
196where
197    V: Verifier + ?Sized,
198{
199    verifier
200        .ec_verifying_key()
201        .with_context(context)
202        .expect("Failed to set Ed25519 verifying context")
203}
204
205fn ec_verify_digest<V, D>(
206    verifier: &V,
207    context: &[u8],
208    digest: D,
209    signature: &Ed25519Signature,
210) -> bool
211where
212    V: Verifier + ?Sized,
213    D: Digest<OutputSize = U64>,
214{
215    ec_verifying_context(verifier, context)
216        .verify_digest(digest, signature)
217        .is_ok()
218}
219
220fn pq_verify_digest<V>(
221    verifier: &V,
222    context: &FnDsaDomainContext<'_>,
223    digest: &[u8],
224    signature: &FnDsaSignature,
225) -> bool
226where
227    V: Verifier + ?Sized,
228{
229    let pq_verifying_key = FnDsaVerifyingDecoded::decode(verifier.pq_verifying_key().as_ref())
230        .expect("Decoding of FN-DSA Verifying Key failed");
231
232    pq_verifying_key.verify(signature, context, &FN_DSA_HASH_ID, digest)
233}
234
235/// Verify both Ed25519 and FN-DSA signatures with a digest.
236///
237/// Both signatures need to be valid for the verification to succeed.
238pub fn verify_digest<V, D>(
239    verifier: &V,
240    context: &[u8],
241    digest: D,
242    ec_signature: &Ed25519Signature,
243    pq_signature: &FnDsaSignature,
244) -> bool
245where
246    V: Verifier + ?Sized,
247    D: Digest<OutputSize = U64> + Clone,
248{
249    let digest_bytes = digest.clone().finalize();
250    ec_verify_digest(verifier, context, digest, ec_signature)
251        && pq_verify_digest(
252            verifier,
253            &FnDsaDomainContext { 0: context },
254            &digest_bytes,
255            pq_signature,
256        )
257}
258
259/// Verifiable if [`Digestible`] and carrying [`Signatures`].
260pub trait Verifiable: Signatures + Digestible {
261    /// Verify this signed data structure with a verifier.
262    ///
263    /// Both Ed25519 and FN-DSA signatures need to be valid for verification to
264    /// succeed.
265    fn verify<V>(&self, verifier: &V) -> bool
266    where
267        V: Verifier + Verifies<Self>,
268        Self: Sized,
269    {
270        verify_digest(
271            verifier,
272            Self::context(),
273            self.digest(),
274            self.ec_signature(),
275            self.pq_signature(),
276        )
277    }
278}