Skip to main content

structured_pkix/key/
ec.rs

1//! The elliptic-curve public key a certificate carries, read in place.
2//!
3//! RFC 5480 §2.1.1 writes the curve as `ECParameters ::= CHOICE { namedCurve
4//! OBJECT IDENTIFIER, implicitCurve NULL, specifiedCurve SpecifiedECDomain }`
5//! and allows only `namedCurve` in PKIX; §2.2 carries the point as the SEC1
6//! octets themselves. [`read`] reports which choice was written rather than
7//! refusing the other two, and refuses only a point no curve can have. Which
8//! curves are supported, their coordinate widths and whether the point lies on
9//! the curve belong to the provider that verifies with the key.
10//! <https://www.rfc-editor.org/rfc/rfc5480#section-2.1.1>
11
12use super::same_oid;
13use crate::algorithm::Producer;
14use crate::error::{Error, ErrorCode, Located, Result};
15use crate::path::WorkingKey;
16use structured_asn1::memory::{Execution, Work};
17use structured_asn1::{Element, Oid, Tag, universal};
18
19const FIELD: &str = "subjectPublicKey.ec";
20const PARAMETERS: &str = "subjectPublicKeyInfo.algorithm.parameters";
21
22/// Which `ECParameters` choice was written.
23#[derive(Debug, Clone, Copy)]
24pub enum Curve<'a> {
25    /// `namedCurve`, the only choice RFC 5480 §2.1.1 permits in PKIX.
26    Named(Oid<'a>),
27    /// `implicitCurve`: the curve is known only out of band.
28    Implicit,
29    /// `specifiedCurve`: explicit domain parameters, kept encoded.
30    Specified(Element<'a>),
31}
32
33/// A SEC1 point as written (SEC1 v2 §2.3.3).
34#[derive(Debug, Clone, Copy)]
35pub enum Point<'a> {
36    /// `0x04 || X || Y`, with both coordinates of one width.
37    Uncompressed {
38        /// The x coordinate.
39        x: &'a [u8],
40        /// The y coordinate.
41        y: &'a [u8],
42    },
43    /// `0x02 || X` or `0x03 || X`, naming the parity of y.
44    Compressed {
45        /// The x coordinate.
46        x: &'a [u8],
47        /// Whether y is odd (`0x03`).
48        odd: bool,
49    },
50}
51
52impl Point<'_> {
53    /// The coordinate width in octets, to compare with the width of the
54    /// curve the caller recognized.
55    #[must_use]
56    pub fn coordinate_len(self) -> usize {
57        match self {
58            Self::Uncompressed { x, .. } | Self::Compressed { x, .. } => x.len(),
59        }
60    }
61}
62
63/// An elliptic-curve public key, as the certificate wrote it.
64#[derive(Debug, Clone, Copy)]
65pub struct PublicKey<'a> {
66    curve: Curve<'a>,
67    parameters: Element<'a>,
68    parameter_producer: Producer<'a>,
69    inherited: bool,
70    point: Point<'a>,
71    octets: &'a [u8],
72    offset: u64,
73}
74
75impl<'a> PublicKey<'a> {
76    /// The curve choice in force.
77    #[must_use]
78    pub fn curve(self) -> Curve<'a> {
79        self.curve
80    }
81
82    /// The original `ECParameters` element the curve was read from.
83    #[must_use]
84    pub fn parameters(self) -> Element<'a> {
85        self.parameters
86    }
87
88    /// Funds deferred parameter queries against their source and current Work.
89    ///
90    /// The source may be an issuer rather than this key's subject SPKI.
91    /// Construction performs no scan, parsing, allocation or account clone;
92    /// the caller charges its actual work before classifying a named curve or
93    /// inspecting explicit parameters. Errors can be located at
94    /// `self.parameters().span().start`. The original source and current root
95    /// operation ceilings remain cumulative, including after foreign queries.
96    /// Point/key work still uses the subject SPKI's execution, independently.
97    ///
98    /// The returned execution borrows current Work and the original source,
99    /// not this temporary view or the operation that originally decoded it.
100    #[must_use]
101    pub fn parameters_execution_in<'query, W: Work>(&self, work: &'query W) -> Execution<'query>
102    where
103        'a: 'query,
104    {
105        self.parameter_producer.execution_in(work)
106    }
107
108    /// Whether the parameters were written by an authority above this key
109    /// rather than by the key itself (RFC 5280 §6.1.4(e)).
110    #[must_use]
111    pub fn is_inherited(self) -> bool {
112        self.inherited
113    }
114
115    /// The point, split into its coordinates.
116    #[must_use]
117    pub fn point(self) -> Point<'a> {
118        self.point
119    }
120
121    /// The whole original point encoding, prefix octet included.
122    #[must_use]
123    pub fn as_bytes(self) -> &'a [u8] {
124        self.octets
125    }
126
127    /// Where the point encoding starts in the whole document.
128    #[must_use]
129    pub fn offset(self) -> u64 {
130        self.offset
131    }
132}
133
134/// Reads the elliptic-curve key carried by `key` under the current `execution`.
135///
136/// The key's algorithm must be `id-ecPublicKey`. Its own parameters apply
137/// where written; a key that writes none or NULL stands in the parameters
138/// inherited along the path, and a lone NULL is `implicitCurve`. Parameters
139/// are decoded with their original parser session, offsets, encoding and
140/// depth, which may belong to the authority's certificate; `execution` pays
141/// for every step. Point and parameter bytes remain borrowed; nested-reader
142/// metadata uses the existing accounted scratch path.
143///
144/// # Errors
145///
146/// `UnexpectedField` for another algorithm, `MissingField` when no parameters
147/// are in force, `MalformedValue` for padded key bits or a point that is
148/// empty, the point at infinity, the hybrid form RFC 5480 §2.2 forbids or of
149/// unequal coordinate widths, and the located encoding or resource error
150/// otherwise. A resource refusal happens before the work it would fund.
151///
152/// # Examples
153///
154/// ```
155/// use std::sync::Arc;
156/// use structured_asn1::{Parser, memory::{Memory, MemoryAccount}};
157/// use structured_pkix::{PublicKeyInfo, key::ec, path::WorkingKey};
158/// let memory = Memory::new(Arc::new(MemoryAccount::new(4096, 4096)));
159/// let parser = Parser::der(memory.clone());
160/// // id-ecPublicKey, prime256v1; compressed point with a one-octet x
161/// let spki = b"\x30\x1b\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\
162///              \x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07\x03\x04\x00\x03\x05\x06";
163/// let mut reader = parser.read(spki, 0).unwrap();
164/// let element = reader.next_element().unwrap().unwrap();
165/// let key = PublicKeyInfo::read_element(&reader, &element, "subjectPublicKeyInfo")?;
166/// let read = ec::read(WorkingKey::standing(key), &memory)?;
167/// assert!(matches!(read.curve(), ec::Curve::Named(_)));
168/// assert!(matches!(read.point(), ec::Point::Compressed { odd: true, .. }));
169/// assert_eq!(read.point().coordinate_len(), 2);
170/// assert_eq!(read.offset(), 26);
171/// # Ok::<(), structured_pkix::Error>(())
172/// ```
173pub fn read<'a, W: Work>(key: WorkingKey<'a>, execution: &W) -> Result<PublicKey<'a>> {
174    let info = key.key();
175    let own = info.algorithm();
176    // The key's OID and point belong to its own source even when the curve
177    // comes from another certificate. Current funding cannot reset either.
178    let query = own.query_in(execution);
179    if !same_oid(
180        own.algorithm(),
181        crate::oid::EC_PUBLIC_KEY,
182        &query,
183        FIELD,
184        own.offset(),
185    )? {
186        return Err(Error::new(ErrorCode::UnexpectedField, FIELD, own.offset()));
187    }
188    // RFC 5280 §6.1.4(e) treats absent and NULL parameters alike for
189    // inheritance. Without an authority's parameters, a written NULL is the
190    // key's own `implicitCurve` choice.
191    let (source, inherited) = match (key.parameter_source(), own.parameters_nested()) {
192        (Some(source), _) => (source, own.has_no_parameters()),
193        (None, Some(null)) => (null, false),
194        (None, None) => {
195            return Err(Error::new(
196                ErrorCode::MissingField,
197                PARAMETERS,
198                own.offset(),
199            ));
200        }
201    };
202    let (reader, parameters) = source.open(execution, PARAMETERS)?;
203    let curve = if parameters.tag() == Tag::primitive(universal::OID) {
204        Curve::Named(reader.oid(&parameters).field(PARAMETERS)?)
205    } else if parameters.tag() == Tag::primitive(universal::NULL) {
206        reader.null(&parameters).field(PARAMETERS)?;
207        Curve::Implicit
208    } else if parameters.tag() == Tag::constructed(universal::SEQUENCE) {
209        Curve::Specified(parameters)
210    } else {
211        return Err(Error::new(
212            ErrorCode::Encoding(structured_asn1::ErrorCode::UnexpectedTag),
213            PARAMETERS,
214            parameters.span().start,
215        ));
216    };
217
218    let offset = info.key_offset();
219    // Admit framing before both padded-bit refusal and prefix inspection.
220    query
221        .charge_work(1)
222        .map_err(|reason| Error::resource(reason, FIELD, offset))?;
223    let octets = info.whole_octets(FIELD)?;
224    // SEC1 v2 §2.3.3: one prefix octet, then x, then y for the uncompressed
225    // form. The single zero octet is the point at infinity, which is not a
226    // public key (SEC1 v2 §3.2.2), and RFC 5480 §2.2 forbids the hybrid
227    // 0x06/0x07 form. https://www.secg.org/sec1-v2.pdf
228    let point = match octets {
229        [0x04, coordinates @ ..] if !coordinates.is_empty() && coordinates.len() % 2 == 0 => {
230            let (x, y) = coordinates.split_at(coordinates.len() / 2);
231            Point::Uncompressed { x, y }
232        }
233        [prefix @ (0x02 | 0x03), x @ ..] if !x.is_empty() => Point::Compressed {
234            x,
235            odd: *prefix == 0x03,
236        },
237        _ => return Err(Error::new(ErrorCode::MalformedValue, FIELD, offset)),
238    };
239    Ok(PublicKey {
240        curve,
241        parameters,
242        parameter_producer: source.execution_origin(),
243        inherited,
244        point,
245        octets,
246        offset,
247    })
248}