Skip to main content

sipx_sip/
identity.rs

1//! SIP authenticated identity and `PASSporT` (RFC 8224 and RFC 8225).
2//!
3//! This module is deliberately sans-I/O: callers supply time and keys. Credential retrieval,
4//! trust policy, and authorization belong to the user-agent layer.
5
6use std::fmt::Write as _;
7
8use base64::Engine as _;
9use base64::engine::general_purpose::URL_SAFE_NO_PAD;
10use bytes::Bytes;
11use p256::ecdsa::signature::{Signer as _, Verifier as _};
12use p256::ecdsa::{Signature, SigningKey, VerifyingKey};
13use p256::pkcs8::{DecodePrivateKey as _, DecodePublicKey as _};
14use serde_json::Value;
15use thiserror::Error;
16
17use crate::error::HeaderError;
18use crate::headers::grammar::{is_token_char, trim};
19use crate::headers::{Date, From, To};
20use crate::{HeaderName, Request, Scheme, TypedHeader, Uri};
21
22const IDENTITY: &str = "Identity";
23const ES256: &str = "ES256";
24
25/// A parsed RFC 8224 `Identity` header value.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct IdentityHeader {
28    /// Full `header.payload.signature` or compact `..signature` `PASSporT`.
29    pub digest: String,
30    /// Credential-reference URI from `info`.
31    pub info: String,
32    /// JWS algorithm. RFC 8224 defaults an absent parameter to `ES256`.
33    pub algorithm: String,
34    /// Mandatory `PASSporT` extension, if one was requested.
35    pub passport_type: Option<String>,
36    /// Unknown generic parameters retained in wire order.
37    pub extensions: Vec<(String, Option<String>)>,
38}
39
40impl IdentityHeader {
41    /// Serialize the typed header value.
42    #[must_use]
43    pub fn to_bytes(&self) -> Bytes {
44        let mut out = format!("{};info=<{}>", self.digest, self.info);
45        if self.algorithm != ES256 {
46            let _ = write!(out, ";alg={}", self.algorithm);
47        }
48        if let Some(passport_type) = &self.passport_type {
49            let _ = write!(out, ";ppt={passport_type}");
50        }
51        for (name, value) in &self.extensions {
52            out.push(';');
53            out.push_str(name);
54            if let Some(value) = value {
55                out.push('=');
56                out.push_str(value);
57            }
58        }
59        Bytes::from(out)
60    }
61}
62
63impl TypedHeader for IdentityHeader {
64    const NAME: HeaderName = HeaderName::Identity;
65
66    fn decode(value: &[u8]) -> Result<Self, HeaderError> {
67        parse_identity(value).ok_or(HeaderError::Syntax { header: IDENTITY })
68    }
69}
70
71fn parse_identity(value: &[u8]) -> Option<IdentityHeader> {
72    let text = std::str::from_utf8(trim(value)).ok()?;
73    let parts = split_identity_parts(text)?;
74    let digest = parts.first()?.trim();
75    if !valid_digest(digest) {
76        return None;
77    }
78
79    let mut info = None;
80    let mut algorithm = None;
81    let mut passport_type = None;
82    let mut extensions = Vec::new();
83    for (index, raw) in parts.iter().skip(1).enumerate() {
84        let raw = raw.trim();
85        let (name, value) = raw
86            .split_once('=')
87            .map_or((raw, None), |(n, v)| (n, Some(v)));
88        if name.is_empty() || !name.as_bytes().iter().all(|&b| is_token_char(b)) {
89            return None;
90        }
91        match name.to_ascii_lowercase().as_str() {
92            "info" => {
93                if info.is_some() {
94                    return None;
95                }
96                let value = value?;
97                let uri = value.strip_prefix('<')?.strip_suffix('>')?;
98                Uri::parse(Bytes::copy_from_slice(uri.as_bytes())).ok()?;
99                info = Some(uri.to_owned());
100            }
101            "alg" => {
102                if index == 0 {
103                    return None;
104                }
105                if algorithm.is_some() {
106                    return None;
107                }
108                let value = value?;
109                if value.is_empty() || !value.as_bytes().iter().all(|&b| is_token_char(b)) {
110                    return None;
111                }
112                algorithm = Some(value.to_owned());
113            }
114            "ppt" => {
115                if index == 0 {
116                    return None;
117                }
118                if passport_type.is_some() {
119                    return None;
120                }
121                let value = value?;
122                if value.is_empty() || !value.as_bytes().iter().all(|&b| is_token_char(b)) {
123                    return None;
124                }
125                passport_type = Some(value.to_owned());
126            }
127            _ => {
128                if index == 0 {
129                    return None;
130                }
131                extensions.push((name.to_owned(), value.map(str::to_owned)));
132            }
133        }
134    }
135    Some(IdentityHeader {
136        digest: digest.to_owned(),
137        info: info?,
138        algorithm: algorithm.unwrap_or_else(|| ES256.to_owned()),
139        passport_type,
140        extensions,
141    })
142}
143
144fn split_identity_parts(value: &str) -> Option<Vec<&str>> {
145    let mut parts = Vec::new();
146    let mut start = 0usize;
147    let mut in_angle = false;
148    for (at, byte) in value.bytes().enumerate() {
149        match byte {
150            b'<' if !in_angle => in_angle = true,
151            b'>' if in_angle => in_angle = false,
152            b';' if !in_angle => {
153                parts.push(value.get(start..at)?);
154                start = at.saturating_add(1);
155            }
156            _ => {}
157        }
158    }
159    if in_angle {
160        return None;
161    }
162    parts.push(value.get(start..)?);
163    Some(parts)
164}
165
166fn valid_digest(value: &str) -> bool {
167    let segments: Vec<_> = value.split('.').collect();
168    if segments.len() != 3 {
169        return false;
170    }
171    let full = segments.first().is_some_and(|part| !part.is_empty())
172        && segments.get(1).is_some_and(|part| !part.is_empty());
173    let compact = segments.first().is_some_and(|part| part.is_empty())
174        && segments.get(1).is_some_and(|part| part.is_empty());
175    (full || compact)
176        && segments.get(2).is_some_and(|part| !part.is_empty())
177        && value
178            .bytes()
179            .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'+' | b'/' | b'.'))
180}
181
182/// A canonical origin or destination identity.
183#[derive(Debug, Clone, PartialEq, Eq, Hash)]
184pub enum CanonicalIdentity {
185    /// Telephone-number identity, containing only digits, `*`, and `#`.
186    TelephoneNumber(String),
187    /// Canonical SIP/SIPS address of record.
188    Uri(String),
189}
190
191impl CanonicalIdentity {
192    /// Derive the RFC 8224 §§8.1, 8.3, and 8.5 identity from a URI.
193    pub fn from_uri(uri: &Uri) -> Result<Self, IdentityError> {
194        match uri.scheme() {
195            Scheme::Tel => canonical_number(uri.opaque().unwrap_or_default()),
196            Scheme::Sip | Scheme::Sips => {
197                let telephone = uri
198                    .params()
199                    .and_then(|params| params.value("user"))
200                    .is_some_and(|value| value.eq_ignore_ascii_case(b"phone"));
201                if telephone {
202                    return canonical_number(&uri.decoded_user().ok_or(IdentityError::Identity)?);
203                }
204                let user = uri.user().ok_or(IdentityError::Identity)?;
205                let user = normalized_user(user)?;
206                let host = uri.host().ok_or(IdentityError::Identity)?.to_string();
207                let scheme = if matches!(uri.scheme(), Scheme::Sips) {
208                    "sips"
209                } else {
210                    "sip"
211                };
212                Ok(Self::Uri(format!(
213                    "{scheme}:{}@{}",
214                    user.to_ascii_lowercase(),
215                    host.to_ascii_lowercase()
216                )))
217            }
218            Scheme::Other(_) => Err(IdentityError::Identity),
219        }
220    }
221
222    fn kind(&self) -> &'static str {
223        match self {
224            Self::TelephoneNumber(_) => "tn",
225            Self::Uri(_) => "uri",
226        }
227    }
228
229    fn value(&self) -> &str {
230        match self {
231            Self::TelephoneNumber(value) | Self::Uri(value) => value,
232        }
233    }
234}
235
236fn canonical_number(raw: &[u8]) -> Result<CanonicalIdentity, IdentityError> {
237    let before_params = raw.split(|&b| b == b';').next().unwrap_or_default();
238    let number: String = before_params
239        .iter()
240        .copied()
241        .filter(|b| b.is_ascii_digit() || matches!(b, b'*' | b'#'))
242        .map(char::from)
243        .collect();
244    if number.is_empty() {
245        Err(IdentityError::Identity)
246    } else {
247        Ok(CanonicalIdentity::TelephoneNumber(number))
248    }
249}
250
251fn normalized_user(raw: &[u8]) -> Result<String, IdentityError> {
252    let mut out = String::new();
253    let mut at = 0usize;
254    while let Some(&byte) = raw.get(at) {
255        if byte == b'%' {
256            let hi = raw.get(at.saturating_add(1)).copied().and_then(hex_value);
257            let lo = raw.get(at.saturating_add(2)).copied().and_then(hex_value);
258            let decoded = hi
259                .zip(lo)
260                .map(|(hi, lo)| hi * 16 + lo)
261                .ok_or(IdentityError::Identity)?;
262            if decoded.is_ascii_alphanumeric() || matches!(decoded, b'-' | b'.' | b'_' | b'~') {
263                out.push(char::from(decoded));
264            } else {
265                let _ = write!(out, "%{decoded:02X}");
266            }
267            at = at.saturating_add(3);
268        } else {
269            if !byte.is_ascii() {
270                return Err(IdentityError::Identity);
271            }
272            out.push(char::from(byte));
273            at = at.saturating_add(1);
274        }
275    }
276    if out.is_empty() {
277        Err(IdentityError::Identity)
278    } else {
279        Ok(out)
280    }
281}
282
283fn hex_value(byte: u8) -> Option<u8> {
284    match byte {
285        b'0'..=b'9' => Some(byte - b'0'),
286        b'a'..=b'f' => Some(byte - b'a' + 10),
287        b'A'..=b'F' => Some(byte - b'A' + 10),
288        _ => None,
289    }
290}
291
292/// An `ES256` private key. Its debug form never includes key material.
293pub struct Es256SigningKey(SigningKey);
294
295impl std::fmt::Debug for Es256SigningKey {
296    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297        formatter
298            .debug_struct("Es256SigningKey")
299            .finish_non_exhaustive()
300    }
301}
302
303impl Es256SigningKey {
304    /// Read an unencrypted `PKCS #8` PEM private key.
305    pub fn from_pkcs8_pem(pem: &str) -> Result<Self, IdentityError> {
306        SigningKey::from_pkcs8_pem(pem)
307            .map(Self)
308            .map_err(|_| IdentityError::Credential)
309    }
310
311    /// The public key corresponding to this private key.
312    #[must_use]
313    pub fn verifying_key(&self) -> Es256VerifyingKey {
314        Es256VerifyingKey(*self.0.verifying_key())
315    }
316}
317
318/// An `ES256` public key.
319#[derive(Clone)]
320pub struct Es256VerifyingKey(VerifyingKey);
321
322impl std::fmt::Debug for Es256VerifyingKey {
323    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
324        formatter
325            .debug_struct("Es256VerifyingKey")
326            .finish_non_exhaustive()
327    }
328}
329
330impl Es256VerifyingKey {
331    /// Read a `SubjectPublicKeyInfo` PEM public key.
332    pub fn from_public_key_pem(pem: &str) -> Result<Self, IdentityError> {
333        VerifyingKey::from_public_key_pem(pem)
334            .map(Self)
335            .map_err(|_| IdentityError::Credential)
336    }
337}
338
339/// A `PASSporT` construction or verification failure.
340#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
341#[non_exhaustive]
342pub enum IdentityError {
343    /// A SIP identity could not be derived or canonicalized.
344    #[error("invalid SIP identity")]
345    Identity,
346    /// The `PASSporT` grammar, base64url, JSON, or required baseline claims are invalid.
347    #[error("invalid PASSporT")]
348    Passport,
349    /// The algorithm is not mandatory-to-implement ES256.
350    #[error("unsupported PASSporT algorithm")]
351    Algorithm,
352    /// A mandatory `PASSporT` extension is not supported.
353    #[error("unsupported PASSporT type")]
354    PassportType,
355    /// ES256 key material is not usable.
356    #[error("unsupported ES256 credential")]
357    Credential,
358    /// The ES256 signature did not verify.
359    #[error("invalid PASSporT signature")]
360    Signature,
361    /// A SIP Date cannot be represented or decoded.
362    #[error("invalid SIP Date")]
363    Date,
364}
365
366/// Canonical From and To identities from one SIP request.
367pub fn request_identities(
368    request: &Request,
369) -> Result<(CanonicalIdentity, CanonicalIdentity), IdentityError> {
370    let from = request
371        .headers
372        .typed::<From>()
373        .ok_or(IdentityError::Identity)?
374        .map_err(|_| IdentityError::Identity)?;
375    let to = request
376        .headers
377        .typed::<To>()
378        .ok_or(IdentityError::Identity)?
379        .map_err(|_| IdentityError::Identity)?;
380    Ok((
381        CanonicalIdentity::from_uri(&from.uri)?,
382        CanonicalIdentity::from_uri(&to.uri)?,
383    ))
384}
385
386/// Construct and deterministically sign a full baseline `PASSporT`.
387pub fn sign_passport(
388    key: &Es256SigningKey,
389    origin: &CanonicalIdentity,
390    destination: &CanonicalIdentity,
391    issued_at: i64,
392    info: &str,
393) -> IdentityHeader {
394    let protected = protected_json(info);
395    let payload = payload_json(origin, destination, issued_at);
396    let protected = URL_SAFE_NO_PAD.encode(protected.as_bytes());
397    let payload = URL_SAFE_NO_PAD.encode(payload.as_bytes());
398    let signing_input = format!("{protected}.{payload}");
399    let signature: Signature = key.0.sign(signing_input.as_bytes());
400    let signature = URL_SAFE_NO_PAD.encode(signature.to_bytes());
401    IdentityHeader {
402        digest: format!("{signing_input}.{signature}"),
403        info: info.to_owned(),
404        algorithm: ES256.to_owned(),
405        passport_type: None,
406        extensions: Vec::new(),
407    }
408}
409
410/// Verify a full or compact baseline `PASSporT` against SIP-derived claims.
411pub fn verify_passport(
412    header: &IdentityHeader,
413    key: &Es256VerifyingKey,
414    origin: &CanonicalIdentity,
415    destination: &CanonicalIdentity,
416    issued_at: i64,
417) -> Result<(), IdentityError> {
418    if header.passport_type.is_some() {
419        return Err(IdentityError::PassportType);
420    }
421    if header.algorithm != ES256 {
422        return Err(IdentityError::Algorithm);
423    }
424    let mut parts = header.digest.split('.');
425    let encoded_header = parts.next().ok_or(IdentityError::Passport)?;
426    let encoded_payload = parts.next().ok_or(IdentityError::Passport)?;
427    let encoded_signature = parts.next().ok_or(IdentityError::Passport)?;
428    if parts.next().is_some() || encoded_signature.is_empty() {
429        return Err(IdentityError::Passport);
430    }
431
432    let expected_header = protected_json(&header.info);
433    let expected_payload = payload_json(origin, destination, issued_at);
434    let (protected, payload) = if encoded_header.is_empty() && encoded_payload.is_empty() {
435        (
436            URL_SAFE_NO_PAD.encode(expected_header.as_bytes()),
437            URL_SAFE_NO_PAD.encode(expected_payload.as_bytes()),
438        )
439    } else if !encoded_header.is_empty() && !encoded_payload.is_empty() {
440        validate_full_json(
441            encoded_header,
442            encoded_payload,
443            &expected_header,
444            &expected_payload,
445        )?;
446        (encoded_header.to_owned(), encoded_payload.to_owned())
447    } else {
448        return Err(IdentityError::Passport);
449    };
450
451    let signature = URL_SAFE_NO_PAD
452        .decode(encoded_signature)
453        .map_err(|_| IdentityError::Passport)?;
454    let signature = Signature::from_slice(&signature).map_err(|_| IdentityError::Passport)?;
455    let signing_input = format!("{protected}.{payload}");
456    verify_es256(&key.0, signing_input.as_bytes(), &signature)
457}
458
459/// Read `iat` from a full `PASSporT`; a compact form carries none to read.
460pub fn passport_issued_at(header: &IdentityHeader) -> Result<Option<i64>, IdentityError> {
461    let mut parts = header.digest.split('.');
462    let encoded_header = parts.next().ok_or(IdentityError::Passport)?;
463    let encoded_payload = parts.next().ok_or(IdentityError::Passport)?;
464    let signature = parts.next().ok_or(IdentityError::Passport)?;
465    if parts.next().is_some() || signature.is_empty() {
466        return Err(IdentityError::Passport);
467    }
468    if encoded_header.is_empty() && encoded_payload.is_empty() {
469        return Ok(None);
470    }
471    if encoded_header.is_empty() || encoded_payload.is_empty() {
472        return Err(IdentityError::Passport);
473    }
474    let payload = URL_SAFE_NO_PAD
475        .decode(encoded_payload)
476        .map_err(|_| IdentityError::Passport)?;
477    let value: Value = serde_json::from_slice(&payload).map_err(|_| IdentityError::Passport)?;
478    value
479        .as_object()
480        .and_then(|object| object.get("iat"))
481        .and_then(Value::as_i64)
482        .map(Some)
483        .ok_or(IdentityError::Passport)
484}
485
486fn verify_es256(
487    key: &VerifyingKey,
488    signing_input: &[u8],
489    signature: &Signature,
490) -> Result<(), IdentityError> {
491    if key.verify(signing_input, signature).is_ok() {
492        return Ok(());
493    }
494    // JWS specifies the mathematical ECDSA signature and does not require low-S normalization.
495    // Some cryptographic backends enforce low-S to avoid malleability, so accept the equivalent
496    // normalized form when the wire carried high-S.
497    signature
498        .normalize_s()
499        .ok_or(IdentityError::Signature)
500        .and_then(|normalized| {
501            key.verify(signing_input, &normalized)
502                .map_err(|_| IdentityError::Signature)
503        })
504}
505
506fn validate_full_json(
507    encoded_header: &str,
508    encoded_payload: &str,
509    expected_header: &str,
510    expected_payload: &str,
511) -> Result<(), IdentityError> {
512    let header = URL_SAFE_NO_PAD
513        .decode(encoded_header)
514        .map_err(|_| IdentityError::Passport)?;
515    let payload = URL_SAFE_NO_PAD
516        .decode(encoded_payload)
517        .map_err(|_| IdentityError::Passport)?;
518    let header_value: Value =
519        serde_json::from_slice(&header).map_err(|_| IdentityError::Passport)?;
520    let payload_value: Value =
521        serde_json::from_slice(&payload).map_err(|_| IdentityError::Passport)?;
522    let expected_header_value: Value =
523        serde_json::from_str(expected_header).map_err(|_| IdentityError::Passport)?;
524    let expected_payload_value: Value =
525        serde_json::from_str(expected_payload).map_err(|_| IdentityError::Passport)?;
526    if header_value != expected_header_value || payload_value != expected_payload_value {
527        return Err(IdentityError::Passport);
528    }
529    // Semantic equality is not enough: RFC 8225 §9 makes the deterministic octets normative.
530    if header.as_slice() != expected_header.as_bytes()
531        || payload.as_slice() != expected_payload.as_bytes()
532    {
533        return Err(IdentityError::Passport);
534    }
535    Ok(())
536}
537
538fn protected_json(info: &str) -> String {
539    let info = serde_json::to_string(info).unwrap_or_else(|_| "\"\"".to_owned());
540    format!(r#"{{"alg":"ES256","typ":"passport","x5u":{info}}}"#)
541}
542
543fn payload_json(
544    origin: &CanonicalIdentity,
545    destination: &CanonicalIdentity,
546    issued_at: i64,
547) -> String {
548    let origin_value = serde_json::to_string(origin.value()).unwrap_or_else(|_| "\"\"".to_owned());
549    let destination_value =
550        serde_json::to_string(destination.value()).unwrap_or_else(|_| "\"\"".to_owned());
551    format!(
552        r#"{{"dest":{{"{}":[{}]}},"iat":{},"orig":{{"{}":{}}}}}"#,
553        destination.kind(),
554        destination_value,
555        issued_at,
556        origin.kind(),
557        origin_value
558    )
559}
560
561/// Parse one already syntax-checked SIP Date into Unix seconds.
562pub fn date_timestamp(date: &Date) -> Result<i64, IdentityError> {
563    let value = date.0.as_slice();
564    if value.len() != 29 {
565        return Err(IdentityError::Date);
566    }
567    let number = |from: usize, to: usize| -> Result<i64, IdentityError> {
568        let bytes = value.get(from..to).ok_or(IdentityError::Date)?;
569        let text = std::str::from_utf8(bytes).map_err(|_| IdentityError::Date)?;
570        text.parse().map_err(|_| IdentityError::Date)
571    };
572    let year = number(12, 16)?;
573    let month = month_number(value.get(8..11).ok_or(IdentityError::Date)?)?;
574    let day = number(5, 7)?;
575    let hour = number(17, 19)?;
576    let minute = number(20, 22)?;
577    let second = number(23, 25)?;
578    if !(1..=9999).contains(&year)
579        || !(1..=days_in_month(year, month)).contains(&day)
580        || hour > 23
581        || minute > 59
582        || second > 59
583    {
584        return Err(IdentityError::Date);
585    }
586    let days =
587        days_before_year(year) + days_before_month(year, month) + day - 1 - days_before_year(1970);
588    let weekday = weekday_name(days);
589    if value.get(0..3) != Some(weekday.as_bytes()) {
590        return Err(IdentityError::Date);
591    }
592    Ok(days * 86_400 + hour * 3_600 + minute * 60 + second)
593}
594
595/// Format Unix seconds as the one GMT date form SIP permits.
596pub fn date_from_timestamp(timestamp: i64) -> Result<Date, IdentityError> {
597    const MIN: i64 = -62_135_596_800; // 0001-01-01 00:00:00 UTC
598    const MAX: i64 = 253_402_300_799; // 9999-12-31 23:59:59 UTC
599    if !(MIN..=MAX).contains(&timestamp) {
600        return Err(IdentityError::Date);
601    }
602    let days = timestamp.div_euclid(86_400);
603    let seconds = timestamp.rem_euclid(86_400);
604    let mut year = 1970i64;
605    let mut day_of_year = days;
606    if day_of_year >= 0 {
607        while day_of_year >= days_in_year(year) {
608            day_of_year -= days_in_year(year);
609            year += 1;
610        }
611    } else {
612        while day_of_year < 0 {
613            year -= 1;
614            day_of_year += days_in_year(year);
615        }
616    }
617    let mut month = 1i64;
618    while day_of_year >= days_in_month(year, month) {
619        day_of_year -= days_in_month(year, month);
620        month += 1;
621    }
622    let day = day_of_year + 1;
623    let hour = seconds / 3_600;
624    let minute = (seconds % 3_600) / 60;
625    let second = seconds % 60;
626    let value = format!(
627        "{}, {day:02} {} {year:04} {hour:02}:{minute:02}:{second:02} GMT",
628        weekday_name(days),
629        month_name(month)?
630    );
631    Ok(Date(value.into_bytes()))
632}
633
634fn month_number(month: &[u8]) -> Result<i64, IdentityError> {
635    const MONTHS: [&[u8]; 12] = [
636        b"Jan", b"Feb", b"Mar", b"Apr", b"May", b"Jun", b"Jul", b"Aug", b"Sep", b"Oct", b"Nov",
637        b"Dec",
638    ];
639    MONTHS
640        .iter()
641        .position(|candidate| *candidate == month)
642        .and_then(|index| i64::try_from(index).ok())
643        .map(|index| index + 1)
644        .ok_or(IdentityError::Date)
645}
646
647fn month_name(month: i64) -> Result<&'static str, IdentityError> {
648    const MONTHS: [&str; 12] = [
649        "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
650    ];
651    usize::try_from(month - 1)
652        .ok()
653        .and_then(|index| MONTHS.get(index).copied())
654        .ok_or(IdentityError::Date)
655}
656
657fn weekday_name(days_since_epoch: i64) -> &'static str {
658    const DAYS: [&str; 7] = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
659    usize::try_from((days_since_epoch + 3).rem_euclid(7))
660        .ok()
661        .and_then(|index| DAYS.get(index).copied())
662        .unwrap_or("Thu")
663}
664
665fn leap(year: i64) -> bool {
666    year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
667}
668
669fn days_in_year(year: i64) -> i64 {
670    if leap(year) { 366 } else { 365 }
671}
672
673fn days_before_year(year: i64) -> i64 {
674    let previous = year - 1;
675    previous * 365 + previous / 4 - previous / 100 + previous / 400
676}
677
678fn days_before_month(year: i64, month: i64) -> i64 {
679    (1..month)
680        .map(|candidate| days_in_month(year, candidate))
681        .sum()
682}
683
684fn days_in_month(year: i64, month: i64) -> i64 {
685    match month {
686        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
687        4 | 6 | 9 | 11 => 30,
688        2 if leap(year) => 29,
689        2 => 28,
690        _ => 0,
691    }
692}
693
694#[cfg(test)]
695#[allow(
696    clippy::unwrap_used,
697    clippy::expect_used,
698    clippy::panic,
699    clippy::indexing_slicing
700)]
701mod tests {
702    use super::*;
703
704    const PRIVATE: &str = "-----BEGIN PRIVATE KEY-----\n\
705MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgi7q2TZvN9VDFg8Vy\n\
706qCP06bETrR2v8MRvr89rn4i+UAahRANCAAQWfaj1HUETpoNCrOtp9KA8o0V79IuW\n\
707ARKt9C1cFPkyd3FBP4SeiNZxQhDrD0tdBHls3/wFe8++K2FrPyQF9vuh\n\
708-----END PRIVATE KEY-----";
709    const PUBLIC: &str = "-----BEGIN PUBLIC KEY-----\n\
710MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8HNbQd/TmvCKwPKHkMF9fScavGeH\n\
71178YTU8qLS8I5HLHSSmlATLcslQMhNC/OhlWBYC626nIlo7XeebYS7Sb37g==\n\
712-----END PUBLIC KEY-----";
713    const RFC_TOKEN: &str = concat!(
714        "eyJhbGciOiJFUzI1NiIsInR5cCI6InBhc3Nwb3J0IiwieDV1IjoiaHR0cHM6Ly9j",
715        "ZXJ0LmV4YW1wbGUub3JnL3Bhc3Nwb3J0LmNlciJ9.",
716        "eyJkZXN0Ijp7InVyaSI6WyJzaXA6YWxpY2VAZXhhbXBsZS5jb20iXX0sImlhdCI",
717        "6MTQ3MTM3NTQxOCwib3JpZyI6eyJ0biI6IjEyMTU1NTUxMjEyIn19.",
718        "VLBCIVDCaeK6M4hLJb6SHQvacAQVvoiiEOWQ_iUkqk79UD81fHQ0E1b3_GluIkb",
719        "a7UWYRM47ZbNFdOJquE35cw"
720    );
721
722    #[test]
723    fn rfc8225_appendix_a_is_the_signing_oracle() {
724        let key = Es256SigningKey::from_pkcs8_pem(PRIVATE).unwrap();
725        let origin = CanonicalIdentity::TelephoneNumber("12155551212".to_owned());
726        let destination = CanonicalIdentity::Uri("sip:alice@example.com".to_owned());
727        let identity = sign_passport(
728            &key,
729            &origin,
730            &destination,
731            1_471_375_418,
732            "https://cert.example.org/passport.cer",
733        );
734        let repeated = sign_passport(
735            &key,
736            &origin,
737            &destination,
738            1_471_375_418,
739            "https://cert.example.org/passport.cer",
740        );
741        assert_eq!(identity.digest, repeated.digest);
742        let expected_signing_input = RFC_TOKEN.rsplit_once('.').unwrap().0;
743        assert_eq!(
744            identity.digest.rsplit_once('.').unwrap().0,
745            expected_signing_input
746        );
747        verify_passport(
748            &identity,
749            &key.verifying_key(),
750            &origin,
751            &destination,
752            1_471_375_418,
753        )
754        .unwrap();
755    }
756
757    #[test]
758    fn a_valid_signature_over_differently_ordered_json_is_rejected() {
759        let key = Es256SigningKey::from_pkcs8_pem(PRIVATE).unwrap();
760        let origin = CanonicalIdentity::TelephoneNumber("12155551212".to_owned());
761        let destination = CanonicalIdentity::Uri("sip:alice@example.com".to_owned());
762        let mut identity = sign_passport(
763            &key,
764            &origin,
765            &destination,
766            1_471_375_418,
767            "https://cert.example.org/passport.cer",
768        );
769        let mut parts = identity.digest.split('.');
770        let _original_header = parts.next().unwrap();
771        let payload = parts.next().unwrap();
772        let _original_signature = parts.next().unwrap();
773        let reordered_header = URL_SAFE_NO_PAD.encode(
774            br#"{"typ":"passport","alg":"ES256","x5u":"https://cert.example.org/passport.cer"}"#,
775        );
776        let signing_input = format!("{reordered_header}.{payload}");
777        let signature: Signature = key.0.sign(signing_input.as_bytes());
778        identity.digest = format!(
779            "{signing_input}.{}",
780            URL_SAFE_NO_PAD.encode(signature.to_bytes())
781        );
782        assert_eq!(
783            verify_passport(
784                &identity,
785                &key.verifying_key(),
786                &origin,
787                &destination,
788                1_471_375_418,
789            ),
790            Err(IdentityError::Passport)
791        );
792    }
793
794    #[test]
795    fn rfc7515_appendix_a3_is_the_es256_verification_oracle() {
796        let x = URL_SAFE_NO_PAD
797            .decode("f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU")
798            .unwrap();
799        let y = URL_SAFE_NO_PAD
800            .decode("x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0")
801            .unwrap();
802        let mut point = Vec::with_capacity(65);
803        point.push(4);
804        point.extend_from_slice(&x);
805        point.extend_from_slice(&y);
806        let key = VerifyingKey::from_sec1_bytes(&point).unwrap();
807        let signature = URL_SAFE_NO_PAD
808            .decode(concat!(
809                "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSA",
810                "pmWQxfKTUJqPP3-Kg6NU1Q"
811            ))
812            .unwrap();
813        let signature = Signature::from_slice(&signature).unwrap();
814        let input = concat!(
815            "eyJhbGciOiJFUzI1NiJ9.",
816            "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt",
817            "cGxlLmNvbS9pc19yb290Ijp0cnVlfQ"
818        );
819        verify_es256(&key, input.as_bytes(), &signature).unwrap();
820    }
821
822    #[test]
823    fn identity_defaults_an_absent_algorithm_to_es256() {
824        let parsed = IdentityHeader::decode(
825            format!("{RFC_TOKEN};info=<https://cert.example.org/passport.cer>").as_bytes(),
826        )
827        .unwrap();
828        assert_eq!(parsed.algorithm, "ES256");
829    }
830
831    #[test]
832    fn unknown_ppt_is_retained_for_mandatory_refusal() {
833        let parsed = IdentityHeader::decode(
834            format!("{RFC_TOKEN};info=<https://cert.example.org/c>;ppt=unknown").as_bytes(),
835        )
836        .unwrap();
837        assert_eq!(parsed.passport_type.as_deref(), Some("unknown"));
838    }
839
840    #[test]
841    fn canonicalization_distinguishes_phone_from_uri() {
842        let phone = Uri::parse(Bytes::from_static(
843            b"sip:+1-(215)-555-1212@example.com;user=phone",
844        ))
845        .unwrap();
846        assert_eq!(
847            CanonicalIdentity::from_uri(&phone).unwrap(),
848            CanonicalIdentity::TelephoneNumber("12155551212".to_owned())
849        );
850        let uri = Uri::parse(Bytes::from_static(
851            b"SIP:Al%69ce:secret@EXAMPLE.com:5060;transport=tcp?Subject=x",
852        ))
853        .unwrap();
854        assert_eq!(
855            CanonicalIdentity::from_uri(&uri).unwrap(),
856            CanonicalIdentity::Uri("sip:alice@example.com".to_owned())
857        );
858    }
859
860    #[test]
861    fn sip_dates_round_trip_and_reject_a_false_weekday() {
862        let date = date_from_timestamp(1_471_375_418).unwrap();
863        assert_eq!(date.0, b"Tue, 16 Aug 2016 19:23:38 GMT");
864        assert_eq!(date_timestamp(&date).unwrap(), 1_471_375_418);
865        assert!(date_timestamp(&Date(b"Mon, 16 Aug 2016 19:23:38 GMT".to_vec())).is_err());
866    }
867
868    #[test]
869    fn key_debug_reports_never_contain_key_material() {
870        let private = Es256SigningKey::from_pkcs8_pem(PRIVATE).unwrap();
871        let public = Es256VerifyingKey::from_public_key_pem(PUBLIC).unwrap();
872        assert!(!format!("{private:?}").contains("MIGH"));
873        assert!(!format!("{public:?}").contains("MFkw"));
874    }
875}