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}