Skip to main content

ic_core/
sig.rs

1//! Signing through an interface, so a key need not be in memory.
2//!
3//! The signature schemes in this library take a private key as bytes. That is
4//! the wrong shape for a key held in an HSM, a TPM or a cloud key service,
5//! where the caller has a handle and the device does the arithmetic. [`Signer`]
6//! is the shape both fit: name the algorithm, pass the message, get a
7//! signature. A TLS handshake or a certificate issuer written against it does
8//! not know or care where the key lives; [`Signer::custody`] is there for the
9//! callers that must.
10//!
11//! This crate defines the interface and nothing that implements it. Software
12//! keys and the hardware backends are other crates'; verification, which needs
13//! no private key, is `ic_sig::verify`.
14//!
15//! # Encodings
16//!
17//! Public keys are DER `SubjectPublicKeyInfo`. Signatures are in the form
18//! X.509 and TLS 1.3 both carry: an ASN.1 `Ecdsa-Sig-Value` for ECDSA, and the
19//! algorithm's own bytes for Ed25519, RSA and ML-DSA. One encoding at the
20//! interface means a signer written for certificates serves a handshake
21//! unchanged.
22
23use crate::traits::RandomSource;
24use crate::Result;
25
26/// A signature algorithm: the key type and everything that goes with it.
27///
28/// Each names one entry in the ontology, by [`SignatureAlgorithm::id`]. RSA
29/// keys serve several of these; every other key serves exactly one.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31#[non_exhaustive]
32pub enum SignatureAlgorithm {
33    /// ECDSA over P-256 with SHA-256.
34    EcdsaP256Sha256,
35    /// ECDSA over P-384 with SHA-384.
36    EcdsaP384Sha384,
37    /// ECDSA over P-521 with SHA-512.
38    EcdsaP521Sha512,
39    /// Ed25519.
40    Ed25519,
41    /// RSASSA-PKCS1-v1_5 with SHA-256.
42    RsaPkcs1Sha256,
43    /// RSASSA-PKCS1-v1_5 with SHA-384.
44    RsaPkcs1Sha384,
45    /// RSASSA-PKCS1-v1_5 with SHA-512.
46    RsaPkcs1Sha512,
47    /// RSASSA-PSS with SHA-256, MGF1-SHA-256 and a 32-byte salt.
48    RsaPssSha256,
49    /// RSASSA-PSS with SHA-384, MGF1-SHA-384 and a 48-byte salt.
50    RsaPssSha384,
51    /// RSASSA-PSS with SHA-512, MGF1-SHA-512 and a 64-byte salt.
52    RsaPssSha512,
53    /// ML-DSA-44 (FIPS 204), pure, with an empty context.
54    MlDsa44,
55    /// ML-DSA-65 (FIPS 204), pure, with an empty context.
56    MlDsa65,
57    /// ML-DSA-87 (FIPS 204), pure, with an empty context.
58    MlDsa87,
59}
60
61impl SignatureAlgorithm {
62    /// Every algorithm, for callers that enumerate them.
63    pub const ALL: &'static [SignatureAlgorithm] = &[
64        Self::EcdsaP256Sha256,
65        Self::EcdsaP384Sha384,
66        Self::EcdsaP521Sha512,
67        Self::Ed25519,
68        Self::RsaPkcs1Sha256,
69        Self::RsaPkcs1Sha384,
70        Self::RsaPkcs1Sha512,
71        Self::RsaPssSha256,
72        Self::RsaPssSha384,
73        Self::RsaPssSha512,
74        Self::MlDsa44,
75        Self::MlDsa65,
76        Self::MlDsa87,
77    ];
78
79    /// The ontology identifier of the algorithm.
80    pub const fn id(self) -> &'static str {
81        match self {
82            Self::EcdsaP256Sha256 => "ecdsa-p256-sha256",
83            Self::EcdsaP384Sha384 => "ecdsa-p384-sha384",
84            Self::EcdsaP521Sha512 => "ecdsa-p521-sha512",
85            Self::Ed25519 => "ed25519",
86            Self::RsaPkcs1Sha256 => "rsa-pkcs1-sha256",
87            Self::RsaPkcs1Sha384 => "rsa-pkcs1-sha384",
88            Self::RsaPkcs1Sha512 => "rsa-pkcs1-sha512",
89            Self::RsaPssSha256 => "rsa-pss-sha256",
90            Self::RsaPssSha384 => "rsa-pss-sha384",
91            Self::RsaPssSha512 => "rsa-pss-sha512",
92            Self::MlDsa44 => "ml-dsa-44",
93            Self::MlDsa65 => "ml-dsa-65",
94            Self::MlDsa87 => "ml-dsa-87",
95        }
96    }
97
98    /// The algorithm an ontology identifier names, if it is one of these.
99    pub fn from_id(id: &str) -> Option<Self> {
100        Self::ALL.iter().copied().find(|a| a.id() == id)
101    }
102
103    /// The longest signature the algorithm produces, in the encoding this
104    /// module's interface uses: a buffer this long always suffices.
105    ///
106    /// ECDSA is the DER `Ecdsa-Sig-Value`, whose length varies with the
107    /// leading bits of `r` and `s`. RSA is for the largest modulus this
108    /// library accepts, 4096 bits.
109    pub const fn max_signature_len(self) -> usize {
110        match self {
111            // SEQUENCE { INTEGER, INTEGER }, each integer one byte longer
112            // than the scalar when its top bit is set.
113            Self::EcdsaP256Sha256 => 72,
114            Self::EcdsaP384Sha384 => 104,
115            Self::EcdsaP521Sha512 => 139,
116            Self::Ed25519 => 64,
117            Self::RsaPkcs1Sha256
118            | Self::RsaPkcs1Sha384
119            | Self::RsaPkcs1Sha512
120            | Self::RsaPssSha256
121            | Self::RsaPssSha384
122            | Self::RsaPssSha512 => 512,
123            Self::MlDsa44 => 2420,
124            Self::MlDsa65 => 3309,
125            Self::MlDsa87 => 4627,
126        }
127    }
128}
129
130/// Where a private key lives, as far as its holder can say.
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132#[non_exhaustive]
133pub enum Custody {
134    /// In this process's memory.
135    Software,
136    /// In a hardware device -- an HSM, a TPM, a smart card -- that performs the
137    /// operation itself.
138    Hardware,
139    /// In a remote service that performs the operation, such as a cloud key
140    /// management service.
141    Service,
142}
143
144impl Custody {
145    /// Stable identifier used in every serialized form.
146    pub const fn id(self) -> &'static str {
147        match self {
148            Self::Software => "software",
149            Self::Hardware => "hardware",
150            Self::Service => "service",
151        }
152    }
153}
154
155/// A private key that can sign, wherever it is held.
156///
157/// Object-safe, and `Send + Sync`: a server shares one key between the
158/// connections it is serving at once, so a signer that could not cross threads
159/// would be unusable exactly where a signer is most needed. Signing takes
160/// `&self` for the same reason; a signer with state to change guards it itself.
161///
162/// # What an implementation promises
163///
164/// - [`sign`](Signer::sign) signs `message` itself, not a digest of it: the
165///   algorithm names its own hash.
166/// - It writes nothing past the length it returns, and allocates only if the
167///   implementation must, so a software signer can serve a caller that may not
168///   allocate.
169/// - It refuses, rather than substitutes, an algorithm it did not list in
170///   [`algorithms`](Signer::algorithms).
171///
172/// # Blocking
173///
174/// `sign` returns when the signature exists. For a key in a remote service that
175/// is a network round trip made inside the call; a caller that cannot block
176/// needs to run it elsewhere and is not served by this interface alone.
177pub trait Signer: Send + Sync {
178    /// The algorithms this key signs with, most preferred first.
179    ///
180    /// The order is meaningful: a protocol negotiating an algorithm takes the
181    /// first one here that its peer and its policy also allow. Listing an
182    /// algorithm is a statement about the key, not about any protocol -- an
183    /// RSA key may list PKCS#1 v1.5 for the certificates it signs, and a TLS
184    /// 1.3 handshake still will not use it.
185    fn algorithms(&self) -> &[SignatureAlgorithm];
186
187    /// The public key, as a DER `SubjectPublicKeyInfo`.
188    ///
189    /// These must be the bytes the key's certificate carries, not a
190    /// re-encoding of the same key: callers compare the two byte for byte to
191    /// check that a certificate and a signer belong together. A signer for a
192    /// remote key reads this once, when it is made.
193    fn public_key(&self) -> &[u8];
194
195    /// Where the private key lives.
196    fn custody(&self) -> Custody;
197
198    /// Sign `message` with `algorithm`, writing the signature to the front of
199    /// `out` and returning its length.
200    ///
201    /// `rng` supplies randomness for the algorithms that use it; a device that
202    /// draws its own ignores it. `out` shorter than the signature is refused
203    /// with `InvalidLength`, with nothing written and nothing consumed;
204    /// [`SignatureAlgorithm::max_signature_len`] is always long enough. An
205    /// algorithm not in [`algorithms`](Signer::algorithms) is refused with
206    /// `InvalidParameter`.
207    fn sign(
208        &self,
209        algorithm: SignatureAlgorithm,
210        message: &[u8],
211        rng: &mut dyn RandomSource,
212        out: &mut [u8],
213    ) -> Result<usize>;
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn identifiers_are_unique_and_round_trip() {
222        for (i, a) in SignatureAlgorithm::ALL.iter().enumerate() {
223            assert_eq!(SignatureAlgorithm::from_id(a.id()), Some(*a));
224            assert!(SignatureAlgorithm::ALL[..i]
225                .iter()
226                .all(|b| b.id() != a.id()));
227        }
228        assert_eq!(SignatureAlgorithm::from_id("ecdsa-p256-sha1"), None);
229    }
230
231    /// The trait is object-safe, which is the property protocols rely on.
232    #[test]
233    fn a_signer_can_be_a_trait_object() {
234        struct Fixed;
235        impl Signer for Fixed {
236            fn algorithms(&self) -> &[SignatureAlgorithm] {
237                &[SignatureAlgorithm::Ed25519]
238            }
239            fn public_key(&self) -> &[u8] {
240                &[]
241            }
242            fn custody(&self) -> Custody {
243                Custody::Hardware
244            }
245            fn sign(
246                &self,
247                _: SignatureAlgorithm,
248                _: &[u8],
249                _: &mut dyn RandomSource,
250                out: &mut [u8],
251            ) -> Result<usize> {
252                out[0] = 7;
253                Ok(1)
254            }
255        }
256        struct NoRng;
257        impl RandomSource for NoRng {
258            fn fill(&mut self, _: &mut [u8]) -> Result<()> {
259                Ok(())
260            }
261        }
262        // Shared across threads, as a server shares its key.
263        fn shareable<T: Send + Sync + ?Sized>(_: &T) {}
264        let signer: &dyn Signer = &Fixed;
265        shareable(signer);
266        let mut out = [0u8; 4];
267        assert_eq!(
268            signer
269                .sign(SignatureAlgorithm::Ed25519, b"m", &mut NoRng, &mut out)
270                .unwrap(),
271            1
272        );
273        assert_eq!(signer.custody().id(), "hardware");
274    }
275}