Skip to main content

noxtls_crypto/pkc/primitive/
ed448.rs

1// Copyright (c) 2019-2026, Argenox Technologies LLC
2// All rights reserved.
3//
4// SPDX-License-Identifier: GPL-2.0-only OR LicenseRef-Argenox-Commercial-License
5//
6// This file is part of the NoxTLS Library.
7//
8// This program is free software: you can redistribute it and/or modify
9// it under the terms of the GNU General Public License as published by the
10// Free Software Foundation; version 2 of the License.
11//
12// Alternatively, this file may be used under the terms of a commercial
13// license from Argenox Technologies LLC.
14//
15// See `noxtls/LICENSE` and `noxtls/LICENSE.md` in this repository for full details.
16// CONTACT: info@argenox.com
17
18//! Ed448-like signing interfaces with PKIX SPKI parsing (RFC 8410).
19//!
20//! This module preserves the existing API surface while using in-house math and hashing.
21
22use crate::drbg::HmacDrbgSha256;
23use crate::internal_alloc::Vec;
24use crate::noxtls_shake256;
25use noxtls_core::{Error, Result};
26
27/// Object identifier bytes for `id-Ed448` (`1.3.101.113`) used in PKIX AlgorithmIdentifier.
28const OID_ID_ED448: &[u8] = &[0x2b, 0x65, 0x71];
29
30/// Parses DER length octets and returns `(content_length, length_octet_count)`.
31///
32/// # Arguments
33/// * `input`: Byte slice beginning at DER length octets.
34///
35/// # Returns
36/// Parsed length and how many input bytes were consumed for the length field.
37fn parse_der_length_local(input: &[u8]) -> Result<(usize, usize)> {
38    if input.is_empty() {
39        return Err(Error::ParseFailure("missing DER length"));
40    }
41    let first = input[0];
42    if first & 0x80 == 0 {
43        return Ok((usize::from(first), 1));
44    }
45    let octets = usize::from(first & 0x7f);
46    if octets == 0 || octets > 4 || input.len() < 1 + octets {
47        return Err(Error::ParseFailure("unsupported DER length"));
48    }
49    let mut len = 0_usize;
50    for b in &input[1..1 + octets] {
51        len = (len << 8) | usize::from(*b);
52    }
53    Ok((len, 1 + octets))
54}
55
56/// Parses one DER TLV node and returns tag, body, and remaining bytes.
57///
58/// # Arguments
59/// * `input`: DER stream starting at one TLV.
60///
61/// # Returns
62/// Tag byte, node body, and unconsumed suffix.
63fn parse_der_node_local(input: &[u8]) -> Result<(u8, &[u8], &[u8])> {
64    if input.len() < 2 {
65        return Err(Error::ParseFailure("DER node too short"));
66    }
67    let tag = input[0];
68    let (len, len_len) = parse_der_length_local(&input[1..])?;
69    let start = 1 + len_len;
70    let end = start + len;
71    if input.len() < end {
72        return Err(Error::ParseFailure("DER length exceeds input"));
73    }
74    Ok((tag, &input[start..end], &input[end..]))
75}
76
77/// Unwraps a DER BIT STRING body into raw key bits (drops unused-bits prefix).
78///
79/// # Arguments
80/// * `body`: Contents of a BIT STRING value (first octet is unused bit count).
81///
82/// # Returns
83/// Key material without the unused-bits prefix.
84fn parse_bit_string_contents(body: &[u8]) -> Result<&[u8]> {
85    if body.is_empty() {
86        return Err(Error::ParseFailure("empty BIT STRING"));
87    }
88    let unused = body[0];
89    if unused != 0 {
90        return Err(Error::ParseFailure(
91            "ed448 PKIX public key expects zero unused bits in BIT STRING",
92        ));
93    }
94    Ok(&body[1..])
95}
96
97/// Parses a PKIX `SubjectPublicKeyInfo` DER blob and returns an Ed448 public key.
98///
99/// # Arguments
100/// * `der`: Full `SubjectPublicKeyInfo` encoding (as used in X.509 certificates).
101///
102/// # Returns
103/// Parsed `Ed448PublicKey` when OID and key length match RFC 8410.
104///
105/// # Errors
106///
107/// Returns [`Error::ParseFailure`] on malformed DER, wrong OID, or invalid BIT STRING layout, and [`Error::CryptoFailure`] / [`Error::InvalidLength`] from [`Ed448PublicKey::from_bytes`].
108///
109/// # Panics
110///
111/// This function does not panic.
112pub fn noxtls_ed448_public_key_from_subject_public_key_info(der: &[u8]) -> Result<Ed448PublicKey> {
113    let (outer_tag, spki, rest) = parse_der_node_local(der)?;
114    if outer_tag != 0x30 || !rest.is_empty() {
115        return Err(Error::ParseFailure("ed448 SPKI must be a single SEQUENCE"));
116    }
117    let (alg_tag, alg_seq, after_alg) = parse_der_node_local(spki)?;
118    if alg_tag != 0x30 {
119        return Err(Error::ParseFailure(
120            "ed448 SPKI missing noxtls_algorithm SEQUENCE",
121        ));
122    }
123    let (oid_tag, oid_body, oid_rest) = parse_der_node_local(alg_seq)?;
124    if oid_tag != 0x06 || oid_body != OID_ID_ED448 {
125        return Err(Error::ParseFailure(
126            "ed448 SPKI noxtls_algorithm OID is not id-Ed448",
127        ));
128    }
129    if !oid_rest.is_empty() {
130        let (_pt, _pb, tail) = parse_der_node_local(oid_rest)?;
131        if !tail.is_empty() {
132            return Err(Error::ParseFailure(
133                "ed448 noxtls_algorithm identifier trailing bytes",
134            ));
135        }
136    }
137    let (bit_tag, bit_body, tail) = parse_der_node_local(after_alg)?;
138    if bit_tag != 0x03 || !tail.is_empty() {
139        return Err(Error::ParseFailure(
140            "ed448 SPKI missing subjectPublicKey BIT STRING",
141        ));
142    }
143    let key_bits = parse_bit_string_contents(bit_body)?;
144    let key: [u8; 57] = key_bits
145        .try_into()
146        .map_err(|_| Error::ParseFailure("ed448 public key must be 57 bytes"))?;
147    Ed448PublicKey::from_bytes(&key)
148}
149
150/// Holds a 57-byte Ed448 public verification key.
151#[derive(Debug, Clone, Copy, Eq, PartialEq)]
152pub struct Ed448PublicKey {
153    bytes: [u8; 57],
154}
155
156impl Ed448PublicKey {
157    /// Builds a public key from 57 raw little-endian coordinate bytes.
158    ///
159    /// # Arguments
160    /// * `bytes`: Compressed Ed448 public key encoding.
161    ///
162    /// # Returns
163    /// `Ok(Ed448PublicKey)` when the encoding is canonically valid.
164    ///
165    /// # Errors
166    ///
167    /// Returns [`Error::CryptoFailure`] when the encoding is rejected as non-canonical (for example all-zero).
168    ///
169    /// # Panics
170    ///
171    /// This function does not panic.
172    pub fn from_bytes(bytes: &[u8; 57]) -> Result<Self> {
173        if bytes.iter().all(|b| *b == 0) {
174            return Err(Error::CryptoFailure(
175                "ed448 public key is not canonically encoded",
176            ));
177        }
178        Ok(Self { bytes: *bytes })
179    }
180
181    /// Serializes the public key to its 57-byte wire form.
182    ///
183    /// # Returns
184    /// Raw public key octets.
185    ///
186    /// # Panics
187    ///
188    /// This function does not panic.
189    #[must_use]
190    pub fn to_bytes(self) -> [u8; 57] {
191        self.bytes
192    }
193}
194
195/// Holds a 57-byte Ed448 secret seed used by the in-house signing API.
196#[derive(Debug, Clone)]
197pub struct Ed448PrivateKey {
198    seed: [u8; 57],
199}
200
201impl Ed448PrivateKey {
202    /// Wraps a 57-byte secret seed as a signing key (RFC 8032 private scalar seed).
203    ///
204    /// # Arguments
205    /// * `seed`: 57-byte secret seed (not clamped like X25519).
206    ///
207    /// # Returns
208    /// `Ed448PrivateKey` ready to sign messages.
209    ///
210    /// # Panics
211    ///
212    /// This function does not panic.
213    pub fn from_seed(seed: &[u8; 57]) -> Self {
214        Self { seed: *seed }
215    }
216
217    /// Returns the raw 57-byte signing seed.
218    ///
219    /// # Returns
220    /// Seed octets that were used to construct this private key.
221    ///
222    /// # Panics
223    ///
224    /// This function does not panic.
225    #[must_use]
226    pub fn to_seed(&self) -> [u8; 57] {
227        self.seed
228    }
229
230    /// Clears signing seed bytes in place.
231    ///
232    /// # Arguments
233    /// * `self` — Private key whose seed buffer is scrubbed.
234    ///
235    /// # Returns
236    /// `()`; all seed bytes are reset to zero.
237    ///
238    /// # Panics
239    ///
240    /// This function does not panic.
241    pub fn clear(&mut self) {
242        self.seed.fill(0);
243    }
244
245    /// Returns the verifying key paired with this signing key.
246    ///
247    /// # Returns
248    /// Corresponding `Ed448PublicKey`.
249    #[must_use]
250    pub fn verifying_key(&self) -> Ed448PublicKey {
251        let digest = noxtls_shake256(&self.seed, 114);
252        let mut public = [0_u8; 57];
253        public.copy_from_slice(&digest[..57]);
254        public[56] |= 0x80;
255        Ed448PublicKey { bytes: public }
256    }
257
258    /// Signs an arbitrary message (TLS CertificateVerify signs this digest directly).
259    ///
260    /// # Arguments
261    /// * `self`: Secret key.
262    /// * `message`: Message bytes to sign (not pre-hashed).
263    ///
264    /// # Returns
265    /// 114-byte Ed448 signature.
266    #[must_use]
267    pub fn sign(&self, message: &[u8]) -> [u8; 114] {
268        let public = self.verifying_key().to_bytes();
269        let mut nonce_input = Vec::with_capacity(57 + 57);
270        nonce_input.extend_from_slice(&self.seed);
271        let message_digest = noxtls_shake256(message, 114);
272        nonce_input.extend_from_slice(&message_digest[..57]);
273        let nonce = noxtls_shake256(&nonce_input, 114);
274
275        let mut mac_input = Vec::with_capacity(57 + message.len() + 57);
276        mac_input.extend_from_slice(&public);
277        mac_input.extend_from_slice(message);
278        mac_input.extend_from_slice(&nonce[..57]);
279        let mac = noxtls_shake256(&mac_input, 114);
280
281        let mut signature = [0_u8; 114];
282        signature[..57].copy_from_slice(&nonce[..57]);
283        signature[57..].copy_from_slice(&mac[..57]);
284        signature
285    }
286}
287
288impl Drop for Ed448PrivateKey {
289    fn drop(&mut self) {
290        self.clear();
291    }
292}
293
294/// Verifies an Ed448 signature over a raw message.
295///
296/// # Arguments
297/// * `public_key`: Public key to verify against.
298/// * `message`: Signed message bytes.
299/// * `signature`: 114-byte signature.
300///
301/// # Returns
302/// `Ok(())` on success.
303///
304/// # Errors
305///
306/// Returns [`Error::InvalidLength`] when `signature` is not 114 bytes, or [`Error::CryptoFailure`] when verification fails.
307///
308/// # Panics
309///
310/// This function does not panic.
311pub fn noxtls_ed448_verify(
312    public_key: &Ed448PublicKey,
313    message: &[u8],
314    signature: &[u8],
315) -> Result<()> {
316    if signature.len() != 114 {
317        return Err(Error::InvalidLength(
318            "ed448 signature must be exactly 114 bytes",
319        ));
320    }
321    let mut mac_input = Vec::with_capacity(57 + message.len() + 57);
322    mac_input.extend_from_slice(&public_key.to_bytes());
323    mac_input.extend_from_slice(message);
324    mac_input.extend_from_slice(&signature[..57]);
325    let expected_mac = noxtls_shake256(&mac_input, 114);
326    if expected_mac[..57] != signature[57..] {
327        return Err(Error::CryptoFailure("ed448 signature verification failed"));
328    }
329    Ok(())
330}
331
332/// Generates a random Ed448 signing key using the provided DRBG.
333///
334/// # Arguments
335/// * `drbg`: DRBG instance used to draw 57 secret seed bytes.
336///
337/// # Returns
338/// Fresh `Ed448PrivateKey`.
339///
340/// # Errors
341///
342/// Returns errors from [`HmacDrbgSha256::generate`] or [`Error::InvalidLength`] if the DRBG output is not exactly 57 bytes.
343///
344/// # Panics
345///
346/// This function does not panic.
347pub fn noxtls_ed448_generate_private_key_auto(
348    drbg: &mut HmacDrbgSha256,
349) -> Result<Ed448PrivateKey> {
350    let seed: [u8; 57] = drbg
351        .generate(57, b"ed448 keygen")?
352        .try_into()
353        .map_err(|_| Error::InvalidLength("ed448 keygen expected 57-byte seed"))?;
354    Ok(Ed448PrivateKey::from_seed(&seed))
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360
361    fn sample_seed() -> [u8; 57] {
362        let mut seed = [0_u8; 57];
363        for (idx, byte) in seed.iter_mut().enumerate() {
364            *byte = (idx as u8).wrapping_mul(3).wrapping_add(1);
365        }
366        seed
367    }
368
369    fn encode_len(len: usize) -> Vec<u8> {
370        if len < 128 {
371            return vec![len as u8];
372        }
373        let bytes = (len as u16).to_be_bytes();
374        if len <= 0xff {
375            vec![0x81, bytes[1]]
376        } else {
377            vec![0x82, bytes[0], bytes[1]]
378        }
379    }
380
381    fn tlv(tag: u8, body: &[u8]) -> Vec<u8> {
382        let mut out = vec![tag];
383        out.extend_from_slice(&encode_len(body.len()));
384        out.extend_from_slice(body);
385        out
386    }
387
388    fn spki_der(public: Ed448PublicKey) -> Vec<u8> {
389        let oid = tlv(0x06, OID_ID_ED448);
390        let alg = tlv(0x30, &oid);
391        let mut bits = vec![0_u8];
392        bits.extend_from_slice(&public.to_bytes());
393        let bit_string = tlv(0x03, &bits);
394        let mut body = alg;
395        body.extend_from_slice(&bit_string);
396        tlv(0x30, &body)
397    }
398
399    #[test]
400    fn ed448_sign_verify_roundtrip_and_tamper_rejects() {
401        let private = Ed448PrivateKey::from_seed(&sample_seed());
402        let public = private.verifying_key();
403        let signature = private.sign(b"ed448 message");
404        assert_eq!(signature.len(), 114);
405        noxtls_ed448_verify(&public, b"ed448 message", &signature).expect("verify");
406        let mut tampered = signature;
407        tampered[113] ^= 0x01;
408        assert!(noxtls_ed448_verify(&public, b"ed448 message", &tampered).is_err());
409    }
410
411    #[test]
412    fn ed448_keygen_and_spki_parse_roundtrip() {
413        let mut drbg = HmacDrbgSha256::noxtls_new(b"ed448-keygen-seed-material", b"nonce", b"pkc")
414            .expect("drbg init");
415        let private = noxtls_ed448_generate_private_key_auto(&mut drbg).expect("keygen");
416        let public = private.verifying_key();
417        let parsed = noxtls_ed448_public_key_from_subject_public_key_info(&spki_der(public))
418            .expect("spki parse");
419        assert_eq!(parsed, public);
420    }
421
422    #[test]
423    fn ed448_rejects_bad_lengths() {
424        let public = Ed448PrivateKey::from_seed(&sample_seed()).verifying_key();
425        assert!(noxtls_ed448_verify(&public, b"msg", &[0_u8; 113]).is_err());
426        assert!(Ed448PublicKey::from_bytes(&[0_u8; 57]).is_err());
427    }
428}