Skip to main content

dig_stun/credential/
signature.rs

1//! The signature: preimage, algorithm, verifier, signer trait (`SPEC.md` §14.6). This crate never
2//! holds a private key — [`StunSigner`] is implemented by the consumer over whatever key object it
3//! already has (dig-node reuses `signer_from_node_cert`'s object, per the SPEC's citation).
4
5use crate::codec::TransactionId;
6use crate::credential::request::RequestKind;
7use crate::credential::wire::{is_valid_spki_der, CredentialError};
8
9/// Domain-separates a `dig:stun:v1` signature from every other message the same TLS-leaf key
10/// signs — a TLS `CertificateVerify`, a `dig:holdings:v1` record, or a future purpose — so a
11/// signature produced for one is never valid for another, in either direction (`SPEC.md` §14.6,
12/// §10 item 7).
13pub const SIG_DOMAIN_TAG: &[u8] = b"dig:stun:v1";
14
15/// Build the exact bytes a signed Binding's signature covers (`SPEC.md` §14.6):
16/// `SIG_DOMAIN_TAG ‖ 0x01 ‖ transaction_id(12) ‖ nonce_len_be(2) ‖ nonce_attr_value ‖ spki_der(91)`.
17///
18/// `nonce_attr_value` MUST be the `NONCE` attribute value EXACTLY as carried on the wire (the
19/// base64url text), never the decoded 20 raw bytes — the signer and the verifier must agree on
20/// this or every signature fails. `spki_der` is the 91-byte SPKI (no version-byte prefix).
21///
22/// Binding the transaction id fixes which response the requester will accept; binding the nonce
23/// fixes the issuing server, the source address, and the time bucket; binding the SPKI stops the
24/// identity from being swapped under an otherwise-valid signature. Nothing else is signed — the
25/// message type is fixed by the server (Binding only) and every other attribute is ignored
26/// (`SPEC.md` §14.5).
27pub fn signing_message(txid: &TransactionId, nonce_attr_value: &[u8], spki_der: &[u8]) -> Vec<u8> {
28    let mut message = Vec::with_capacity(
29        SIG_DOMAIN_TAG.len() + 1 + txid.len() + 2 + nonce_attr_value.len() + spki_der.len(),
30    );
31    message.extend_from_slice(SIG_DOMAIN_TAG);
32    message.push(crate::credential::wire::CREDENTIAL_VERSION);
33    message.extend_from_slice(txid);
34    message.extend_from_slice(&(nonce_attr_value.len() as u16).to_be_bytes());
35    message.extend_from_slice(nonce_attr_value);
36    message.extend_from_slice(spki_der);
37    message
38}
39
40/// A signature-verified requester identity (`SPEC.md` §14.6, §14.10). Carries only the SPKI: this
41/// level-00 crate cannot compute a `peer_id` from it (that is `dig_tls::peer_id_from_tls_spki_der`,
42/// a `dig-*` crate this one must never depend on) — callers that want the `peer_id` hash the SPKI
43/// with that function themselves.
44///
45/// **What this does NOT prove**, stated once so nothing downstream over-reads it: network
46/// membership, relay registration, on-chain standing, or that any later claim from the same
47/// session is true (`SPEC.md` §14.1, §14.10, §7). It proves exactly key possession, freshness, and
48/// return-routability.
49///
50/// `#[non_exhaustive]`: constructed only by [`verify_signed_request`], so an additive field is a
51/// patch release for every consumer.
52#[non_exhaustive]
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct VerifiedIdentity {
55    spki: [u8; crate::credential::wire::P256_SPKI_LEN],
56}
57
58impl VerifiedIdentity {
59    /// The verified 91-byte SPKI DER (no version-byte prefix).
60    pub fn spki_der(&self) -> &[u8; crate::credential::wire::P256_SPKI_LEN] {
61        &self.spki
62    }
63}
64
65/// Verify a [`RequestKind::Signed`]'s signature against its own carried SPKI over
66/// [`signing_message`] (`SPEC.md` §14.6): ECDSA, P-256, SHA-256, ASN.1 DER
67/// (`ring::signature::ECDSA_P256_SHA256_ASN1`).
68///
69/// `kind` MUST be [`RequestKind::Signed`] — the caller has always already matched on `kind` to
70/// decide whether verification even applies (`SPEC.md` §14.5 step 4: reached ONLY for `Signed` +
71/// `Fresh`), so this is a precondition rather than adversary-reachable input. A `kind` of any
72/// other variant returns [`CredentialError::Malformed`] rather than panicking — this function sits
73/// on a path parsing untrusted datagrams, and failing closed costs nothing here.
74///
75/// `RequestKind`'s fields are public, so a `Signed` variant reaching this function is not
76/// guaranteed to carry the 91-byte SPKI shape [`crate::credential::classify_request`] would have
77/// enforced — only a value built from the wire via that parser gets that guarantee for free. This
78/// function re-validates the shape itself (`is_valid_spki_der`) before it slices `spki`, so a
79/// directly-constructed `Signed` carrying a too-short or otherwise malformed SPKI returns
80/// [`CredentialError::Malformed`] rather than panicking on an out-of-bounds index.
81///
82/// # Errors
83///
84/// [`CredentialError::BadSignature`] when the signature does not verify (wrong key, wrong
85/// preimage, or corrupt DER); [`CredentialError::Malformed`] when `kind` is not `Signed`, or when
86/// its `spki` does not have the shape `is_valid_spki_der` requires.
87pub fn verify_signed_request(
88    txid: &TransactionId,
89    kind: &RequestKind<'_>,
90) -> Result<VerifiedIdentity, CredentialError> {
91    let RequestKind::Signed {
92        spki,
93        nonce,
94        signature,
95    } = kind
96    else {
97        return Err(CredentialError::Malformed);
98    };
99
100    // A directly-built `Signed` (its fields are public) is not guaranteed to carry a well-formed
101    // SPKI the way one parsed via `classify_request` is. The slice below, and the `copy_from_slice`
102    // near the end of this function, both require exactly 91 bytes and panic on anything shorter —
103    // reject rather than slice blind: a public function must not panic on any input its own type
104    // permits.
105    if !is_valid_spki_der(spki) {
106        return Err(CredentialError::Malformed);
107    }
108
109    let message = signing_message(txid, nonce, spki);
110    // The 65-byte uncompressed SEC1 point lives at spki[26..91]: byte 26 is the 0x04 marker
111    // `is_valid_spki_der` just confirmed, and 27..91 is X ‖ Y (`SPEC.md` §14.6).
112    let point = &spki[26..crate::credential::wire::P256_SPKI_LEN];
113    let public_key =
114        ring::signature::UnparsedPublicKey::new(&ring::signature::ECDSA_P256_SHA256_ASN1, point);
115    public_key
116        .verify(&message, signature)
117        .map_err(|_| CredentialError::BadSignature)?;
118
119    let mut spki_arr = [0u8; crate::credential::wire::P256_SPKI_LEN];
120    spki_arr.copy_from_slice(spki);
121    Ok(VerifiedIdentity { spki: spki_arr })
122}
123
124/// Implemented by the consumer over the P-256 private key it already holds for its TLS leaf
125/// (`SPEC.md` §14.6). This crate never constructs or stores a private key — no `from_pkcs8` site
126/// lives here; dig-node's adapter wraps the SAME object `signer_from_node_cert` already builds for
127/// `dig:holdings:v1` records, so no second key-loading site is written anywhere in the ecosystem.
128pub trait StunSigner {
129    /// Exactly the 91 bytes of `SPEC.md` §14.3.1 — the SPKI DER with no version-byte prefix.
130    fn spki_der(&self) -> &[u8];
131    /// Sign `message` (always the output of [`signing_message`]) with the leaf's private key,
132    /// returning an ECDSA-P256-SHA256 signature in ASN.1 DER form.
133    fn sign(&self, message: &[u8]) -> Vec<u8>;
134}