Skip to main content

didbot_key/
secp256r1.rs

1//! P-256 verification, and deliberately no P-256 signing.
2//!
3//! atproto accepts two signing curves. This server mints secp256k1 keys and
4//! only secp256k1 keys, for the reason the crate documentation gives: `k256`
5//! normalizes `s` on the way out and refuses a high `s` on the way in, and
6//! `p256` does neither, so a P-256 *signer* here would emit a malleable
7//! signature about half the time and a relay would refuse it.
8//!
9//! Verifying is the other direction and the argument does not apply to it. A
10//! key that arrives from somewhere else is not ours to normalize; it is ours
11//! to judge, and the judgement is the same three questions asked of a
12//! secp256k1 signature — a 64-byte compact pair, a low `s`, and a
13//! verification that succeeds. The low-`s` check that `k256` performs
14//! internally is written out by hand here, because `p256` will happily verify
15//! a high-`s` signature that atproto refuses.
16//!
17//! Half of the network's accounts sign with this curve, so a server that can
18//! read another server's commit needs it. Nothing in this crate can produce
19//! one.
20//!
21//! ```
22//! use didbot_key::secp256r1;
23//!
24//! // A key from the W3C did:key vectors, which mint nothing here.
25//! let key = secp256r1::VerifyingKey::from_did_key(
26//!     "did:key:zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb",
27//! )?;
28//! assert!(key.to_did_key().starts_with("did:key:zDn"));
29//! # Ok::<(), didbot_key::KeyError>(())
30//! ```
31
32use core::fmt;
33
34use jacquard_common::types::crypto::{multikey, KeyCodec, PublicKey};
35use p256::ecdsa::signature::Verifier;
36use p256::elliptic_curve::scalar::IsHigh;
37
38use crate::{KeyError, COMPRESSED_LEN, DID_KEY_PREFIX};
39
40/// The multicodec for a P-256 public key.
41const P256_CODEC: u64 = 0x1200;
42
43/// A P-256 ECDSA signature over SHA-256, in 64-byte compact `r || s` form.
44#[derive(Clone, Copy, PartialEq, Eq)]
45pub struct Signature(p256::ecdsa::Signature);
46
47impl Signature {
48    /// Parses a 64-byte compact `r || s` signature.
49    ///
50    /// A high-`s` signature parses. Refusing malleability is
51    /// [`VerifyingKey::verify`]'s job, so that a signature from elsewhere can
52    /// be read and then honestly reported as one atproto does not accept.
53    pub fn from_bytes(bytes: &[u8; 64]) -> Result<Self, KeyError> {
54        p256::ecdsa::Signature::from_slice(bytes)
55            .map(Self)
56            .map_err(|_| KeyError::InvalidSignature)
57    }
58
59    /// The 64-byte compact `r || s` encoding.
60    pub fn to_bytes(&self) -> [u8; 64] {
61        self.0.to_bytes().into()
62    }
63
64    /// Whether `s` is in the lower half of the curve order, as atproto
65    /// requires.
66    pub fn is_low_s(&self) -> bool {
67        !bool::from(self.0.s().is_high())
68    }
69}
70
71impl fmt::Debug for Signature {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        f.write_str("secp256r1::Signature(")?;
74        for byte in self.to_bytes() {
75            write!(f, "{byte:02x}")?;
76        }
77        f.write_str(")")
78    }
79}
80
81/// A P-256 public key that arrived from somewhere else.
82///
83/// There is no private counterpart in this crate by design.
84#[derive(Clone, Copy, PartialEq, Eq)]
85pub struct VerifyingKey(p256::ecdsa::VerifyingKey);
86
87impl VerifyingKey {
88    /// The `publicKeyMultibase` form: base58btc over the `0x1200` multicodec
89    /// and the 33-byte SEC1 compressed point.
90    pub fn to_multibase(&self) -> String {
91        multikey(P256_CODEC, self.0.to_encoded_point(true).as_bytes())
92    }
93
94    /// The `did:key` URI form.
95    pub fn to_did_key(&self) -> String {
96        format!("{DID_KEY_PREFIX}{}", self.to_multibase())
97    }
98
99    /// Parses a `did:key:z...` URI naming a P-256 key.
100    pub fn from_did_key(did: &str) -> Result<Self, KeyError> {
101        let multibase = did
102            .strip_prefix(DID_KEY_PREFIX)
103            .ok_or(KeyError::NotDidKey)?;
104        Self::from_multibase(multibase)
105    }
106
107    /// Parses a bare `publicKeyMultibase` string naming a P-256 key.
108    ///
109    /// Refuses any other curve and any encoding but the 33-byte compressed
110    /// point, exactly as the secp256k1 reader does: a key that round-trips
111    /// through this crate is one atproto will accept.
112    pub fn from_multibase(multibase: &str) -> Result<Self, KeyError> {
113        if !multibase.starts_with('z') {
114            return Err(KeyError::NotBase58Btc);
115        }
116        let public = PublicKey::decode_owned(multibase)?;
117        if public.codec != KeyCodec::P256 {
118            return Err(KeyError::WrongCurve {
119                codec: public.codec,
120            });
121        }
122        if public.bytes.len() != COMPRESSED_LEN {
123            return Err(KeyError::NotCompressed {
124                got: public.bytes.len(),
125            });
126        }
127        // Decoded here rather than through `jacquard-common`'s `to_p256`,
128        // which sits behind a feature this workspace does not turn on. The
129        // bytes are a SEC1 point either way and the check above has already
130        // established the codec and the length.
131        let point = p256::PublicKey::from_sec1_bytes(public.bytes.as_ref())
132            .map_err(|_| KeyError::InvalidPublicKey)?;
133        Ok(Self(point.into()))
134    }
135
136    /// Verifies a signature over `message`, refusing a high `s`.
137    ///
138    /// The low-`s` check is written out rather than inherited. `p256` accepts
139    /// a high-`s` signature and atproto does not, so a verifier that leaned on
140    /// the library the way the secp256k1 one does would accept exactly the
141    /// signatures the protocol exists to rule out.
142    pub fn verify(&self, message: &[u8], sig: &Signature) -> Result<(), KeyError> {
143        if !sig.is_low_s() {
144            return Err(KeyError::VerificationFailed);
145        }
146        self.0
147            .verify(message, &sig.0)
148            .map_err(|_| KeyError::VerificationFailed)
149    }
150}
151
152impl fmt::Debug for VerifyingKey {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        f.debug_tuple("secp256r1::VerifyingKey")
155            .field(&self.to_did_key())
156            .finish()
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    /// The one vector the W3C file carries, so the module has a check that
165    /// does not depend on the conformance target being run.
166    const W3C_DID: &str = "did:key:zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb";
167
168    #[test]
169    fn a_p256_did_key_round_trips() {
170        let key = VerifyingKey::from_did_key(W3C_DID).expect("the vector's key decodes");
171        assert_eq!(key.to_did_key(), W3C_DID);
172    }
173
174    #[test]
175    fn a_secp256k1_key_is_refused_by_curve() {
176        let ours = crate::SigningKey::generate().verifying_key().to_did_key();
177        assert!(matches!(
178            VerifyingKey::from_did_key(&ours),
179            Err(KeyError::WrongCurve { .. })
180        ));
181    }
182
183    #[test]
184    fn a_uri_that_is_not_a_did_key_is_refused() {
185        assert!(matches!(
186            VerifyingKey::from_did_key("did:web:example.com"),
187            Err(KeyError::NotDidKey)
188        ));
189    }
190}