cratefield_push_auth/
es256.rs1use base64::Engine;
4use base64::engine::general_purpose::URL_SAFE_NO_PAD;
5use p256::ecdsa::SigningKey;
6use p256::ecdsa::signature::Signer as _;
7use p256::pkcs8::DecodePrivateKey as _;
8use serde_json::Value;
9
10use crate::{KeyError, signing_input};
11
12pub struct Es256Signer {
19 key: SigningKey,
20}
21
22impl Es256Signer {
23 pub fn from_pkcs8_pem(pem: &str) -> Result<Self, KeyError> {
29 SigningKey::from_pkcs8_pem(pem)
30 .map(|key| Self { key })
31 .map_err(|err| KeyError::parse("ES256", err))
32 }
33
34 pub fn from_p8_pem(pem: &str) -> Result<Self, KeyError> {
41 Self::from_pkcs8_pem(pem)
42 }
43
44 pub fn from_scalar(scalar: &[u8; 32]) -> Result<Self, KeyError> {
55 SigningKey::from_slice(scalar)
56 .map(|key| Self { key })
57 .map_err(|err| KeyError::parse("ES256", err))
58 }
59
60 #[must_use]
66 pub fn sign_jwt(&self, header: &Value, claims: &Value) -> String {
67 let input = signing_input(header, claims);
68 let signature: p256::ecdsa::Signature = self.key.sign(input.as_bytes());
71 format!("{input}.{}", URL_SAFE_NO_PAD.encode(signature.to_bytes()))
72 }
73
74 #[must_use]
77 pub fn public_key_uncompressed(&self) -> [u8; 65] {
78 let point = self.key.verifying_key().to_sec1_point(false);
79 let mut out = [0u8; 65];
80 out.copy_from_slice(point.as_bytes());
82 out
83 }
84}
85
86impl std::fmt::Debug for Es256Signer {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 f.write_str("Es256Signer { .. }")
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use p256::ecdsa::signature::Verifier as _;
97 use serde_json::json;
98
99 const TEST_P8: &str = "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgcXMgRpW+eLn7ZvCx\nIuTdd8csWMZ69azlRzS0dy2FN6GhRANCAATJ6GazR2lhWcC3JYsazLR0uWOyDKrC\nmeP4HPWghRmfoa4z3Ux7mG3Ylz+auRaBukKGicSdSvVG+jGeQwr3fNag\n-----END PRIVATE KEY-----";
103
104 #[test]
105 fn rejects_a_pem_that_is_not_a_key() {
106 let error = Es256Signer::from_p8_pem("not a key").unwrap_err();
107 assert!(error.to_string().contains("ES256"), "{error}");
108 }
109
110 #[test]
111 fn signs_a_verifiable_jwt() {
112 let signer = Es256Signer::from_p8_pem(TEST_P8).unwrap();
113 let jwt = signer.sign_jwt(
114 &json!({ "alg": "ES256", "kid": "ABC1234567" }),
115 &json!({ "iss": "TEAM987654", "iat": 1_700_000_000 }),
116 );
117
118 let parts: Vec<&str> = jwt.split('.').collect();
119 assert_eq!(parts.len(), 3, "header.claims.signature");
120
121 let verifying = signer.key.verifying_key();
123 let signature = p256::ecdsa::Signature::from_slice(
124 &URL_SAFE_NO_PAD.decode(parts[2]).expect("base64url"),
125 )
126 .expect("64-byte r||s");
127 let input = format!("{}.{}", parts[0], parts[1]);
128 verifying
129 .verify(input.as_bytes(), &signature)
130 .expect("the JWT verifies under its own key");
131 }
132
133 #[test]
136 fn the_token_is_a_fixed_string_for_a_fixed_key_and_claims() {
137 let signer = Es256Signer::from_p8_pem(TEST_P8).unwrap();
138 let jwt = signer.sign_jwt(
139 &json!({ "alg": "ES256", "kid": "ABC1234567" }),
140 &json!({ "iss": "TEAM987654", "iat": 1_700_000_000 }),
141 );
142 assert_eq!(
143 jwt,
144 "eyJhbGciOiJFUzI1NiIsImtpZCI6IkFCQzEyMzQ1NjcifQ.\
145 eyJpc3MiOiJURUFNOTg3NjU0IiwiaWF0IjoxNzAwMDAwMDAwfQ.\
146 FLaMDTVoUcLQhdV3iub8kZEpb2FRTQcrjNgvBvt8mIdsVi6hnRSrdAgbM06sQEsGKInnfX_M1sknBYJIsrAH0Q"
147 );
148 }
149
150 #[test]
154 fn a_bare_scalar_and_its_pkcs8_wrapper_are_the_same_key() {
155 let from_pem = Es256Signer::from_p8_pem(TEST_P8).unwrap();
156 let scalar: [u8; 32] = from_pem.key.to_bytes().into();
157 let from_scalar = Es256Signer::from_scalar(&scalar).unwrap();
158
159 assert_eq!(
160 from_scalar.public_key_uncompressed(),
161 from_pem.public_key_uncompressed()
162 );
163 let header = json!({ "alg": "ES256" });
164 let claims = json!({ "sub": "mailto:ops@example.test" });
165 assert_eq!(
166 from_scalar.sign_jwt(&header, &claims),
167 from_pem.sign_jwt(&header, &claims)
168 );
169 }
170
171 #[test]
172 fn a_scalar_that_is_not_a_private_key_is_refused() {
173 let error = Es256Signer::from_scalar(&[0u8; 32]).unwrap_err();
176 assert!(error.to_string().contains("ES256"), "{error}");
177 let order = [
178 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
179 0xff, 0xff, 0xbc, 0xe6, 0xfa, 0xad, 0xa7, 0x17, 0x9e, 0x84, 0xf3, 0xb9, 0xca, 0xc2,
180 0xfc, 0x63, 0x25, 0x51,
181 ];
182 assert!(Es256Signer::from_scalar(&order).is_err());
183 }
184
185 #[test]
186 fn the_public_key_is_an_uncompressed_sec1_point() {
187 let signer = Es256Signer::from_p8_pem(TEST_P8).unwrap();
188 let point = signer.public_key_uncompressed();
189 assert_eq!(point.len(), 65);
190 assert_eq!(point[0], 0x04, "uncompressed marker");
191 let verifying =
194 p256::ecdsa::VerifyingKey::from_sec1_bytes(&point).expect("valid SEC1 point");
195 assert_eq!(&verifying, signer.key.verifying_key());
196 }
197}