cratefield_push_auth/
rs256.rs1use base64::Engine;
5use base64::engine::general_purpose::URL_SAFE_NO_PAD;
6use rsa::pkcs1::DecodeRsaPrivateKey as _;
7use rsa::pkcs1v15::SigningKey;
8use rsa::pkcs8::DecodePrivateKey as _;
9use rsa::sha2::Sha256;
10use rsa::signature::{SignatureEncoding as _, Signer as _};
11use serde_json::Value;
12
13use crate::{KeyError, signing_input};
14
15pub struct Rs256Signer {
32 key: SigningKey<Sha256>,
33}
34
35impl Rs256Signer {
36 pub fn from_pkcs8_pem(pem: &str) -> Result<Self, KeyError> {
43 rsa::RsaPrivateKey::from_pkcs8_pem(pem)
44 .map(|key| Self {
45 key: SigningKey::new(key),
46 })
47 .map_err(|err| KeyError::parse("RS256", err))
48 }
49
50 pub fn from_pkcs1_pem(pem: &str) -> Result<Self, KeyError> {
57 rsa::RsaPrivateKey::from_pkcs1_pem(pem)
58 .map(|key| Self {
59 key: SigningKey::new(key),
60 })
61 .map_err(|err| KeyError::parse("RS256", err))
62 }
63
64 pub fn from_pem(pem: &str) -> Result<Self, KeyError> {
72 if pem.contains("BEGIN RSA PRIVATE KEY") {
73 Self::from_pkcs1_pem(pem)
74 } else {
75 Self::from_pkcs8_pem(pem)
76 }
77 }
78
79 #[must_use]
81 pub fn sign_jwt(&self, header: &Value, claims: &Value) -> String {
82 let input = signing_input(header, claims);
83 let signature = self.key.sign(input.as_bytes());
84 format!("{input}.{}", URL_SAFE_NO_PAD.encode(signature.to_bytes()))
85 }
86}
87
88impl std::fmt::Debug for Rs256Signer {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 f.write_str("Rs256Signer { .. }")
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use rsa::sha2::Digest as _;
99 use rsa::traits::PublicKeyParts as _;
100 use serde_json::json;
101
102 const TEST_PKCS8: &str = "-----BEGIN PRIVATE KEY-----
106MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCgQ/qAORkSKZjW
107xMCbrhGXqbVCHpslcKpb3Ew/5qpRclIDXmJcaXehqY4swKuYdyqcQloRH5inZ/6U
108DR/p72s+6Gu5ExAje64hw2AChozIdmKA/xZYxPjQ5AtFXLofTglg1VeudH+6/bDw
109sXKGsaqRPWghb0lc1cai0ETHZDfP9frfJKjJRm2HlZqp8CKwwl1Jy6CQOGrjKlQX
110ThV07JKPpRPTZFscZTMzjG9fIfYCcxWt7c7HcPN4G7QUrx4oEX/jkuJcA2zgQ0WL
1110gJ4IKUc1BiNDP67UniFq8bxlcqePQc8MN3bBDrPKktX24u9ZJJZjx6otgBIWCaB
112nIKyuyfzAgMBAAECggEAS0Af+tzUfMazUQSJO4/8Cq5QwX8Fcgr4srE5zDdOeXeo
113MpS6spGC7pFihHjjGW+6viwZhjjDwLb/vhx7g6g7Pwp6qifdSAvms0u9ZPIwYF/V
1142KPtpji2a77n2+WyLsjBdoo15WAmKXK9BgcLs1rwr8mZfzl1xPVLk18fLFBONIKY
115ySiiGRQC2NZXfAEZF/pDMaBT5+my7hjZkw07XMrV//DnR71Gre2IPj0SNPWjS+Fd
116qMQq5UvxaJxLP6dDswYqXNvoWC7YAlsnf9ySD3ykLThATh4yUCL6y97wvKEdU9yb
1171mruLpOGNCnjCDqx+spY49qt+3uWBcj2BXjAPUlQoQKBgQDPOLZSz772jquTiyk0
1184OiUkCofmhLdpKU/vJW6TWGjGWv3MSUvi1wA5q/AByljCtr+/lwzkj1c9/6Hzo0N
119zv0kagaVRmb5INhTEu+1957lBP/WucHOvKPhiUvLjiH0YTqFSl+uB5ZbzbIxMOko
120T86cq9/qFkOl3BhR8CFoRWwbbwKBgQDF/avnjCFOEVzluU+FnNepR36mZWlhQPNt
121xT/wlavbPVQbIYbUJ4vKaU2OjIRE0fPJiC/XXEdD74LeX3gfoufau/M3hSmUDLoq
122DZCng5zGKtOlPMLQkf0Q2xVesM5eCJ2dPWZDR8jgkoXaLRa1YIFUofl24AkeabqO
123u56pgEwJvQKBgAaN+65o5dh0sNas6zPB/XldigeP3xLlt1hpxa6r7e+zySd7hXqY
124hON+aIbBczyvxjeUoiP7dzdunL18+hc6ueUh+W1VWcJ9mHogOjbeS0dhPhpzq763
125VtO2fRBGQaqyPKCktpwRn17uBbnqmyVsSNPJ1/5Wj/M6IAbPeq8Kqx2/AoGAPy6u
126lxu+3RzpWl4CpI7iu6CXKB6gvGpvxI3305zP1Q0DNA1E65sbHyLvnxf0dcnSVHPj
127YISQMXvTdYdd3Cqudr0X5pXWKOrO1fCyQuLbOtob5FU5jjmoWqKvdSJTGOsC8VTQ
128t5PG5POdR3ywDH2ZiBqQc4EXJ99xq27wOQM6QLkCgYB9f9GrE+DQIYBzb7hf1SMz
129ELvz+3ijy43njvIg4CihiUAzIuc0VJaVCKZcxIMZxLoDB6roeLoYBS+GhVrMoHUj
130mbv4voW/HXdaIlZUbMa0y9q0cg3Q8oaw5bwSzrSZxEkadudn4+MFKbLRvmYFX7We
1316OmL9MfIa/kocbiIIu0TSA==
132-----END PRIVATE KEY-----";
133
134 const TEST_PKCS1: &str = "-----BEGIN RSA PRIVATE KEY-----
135MIIEogIBAAKCAQEAoEP6gDkZEimY1sTAm64Rl6m1Qh6bJXCqW9xMP+aqUXJSA15i
136XGl3oamOLMCrmHcqnEJaER+Yp2f+lA0f6e9rPuhruRMQI3uuIcNgAoaMyHZigP8W
137WMT40OQLRVy6H04JYNVXrnR/uv2w8LFyhrGqkT1oIW9JXNXGotBEx2Q3z/X63ySo
138yUZth5WaqfAisMJdScugkDhq4ypUF04VdOySj6UT02RbHGUzM4xvXyH2AnMVre3O
139x3DzeBu0FK8eKBF/45LiXANs4ENFi9ICeCClHNQYjQz+u1J4havG8ZXKnj0HPDDd
1402wQ6zypLV9uLvWSSWY8eqLYASFgmgZyCsrsn8wIDAQABAoIBAEtAH/rc1HzGs1EE
141iTuP/AquUMF/BXIK+LKxOcw3Tnl3qDKUurKRgu6RYoR44xlvur4sGYY4w8C2/74c
142e4OoOz8Keqon3UgL5rNLvWTyMGBf1dij7aY4tmu+59vlsi7IwXaKNeVgJilyvQYH
143C7Na8K/JmX85dcT1S5NfHyxQTjSCmMkoohkUAtjWV3wBGRf6QzGgU+fpsu4Y2ZMN
144O1zK1f/w50e9Rq3tiD49EjT1o0vhXajEKuVL8WicSz+nQ7MGKlzb6Fgu2AJbJ3/c
145kg98pC04QE4eMlAi+sve8LyhHVPcm9Zq7i6ThjQp4wg6sfrKWOParft7lgXI9gV4
146wD1JUKECgYEAzzi2Us++9o6rk4spNODolJAqH5oS3aSlP7yVuk1hoxlr9zElL4tc
147AOavwAcpYwra/v5cM5I9XPf+h86NDc79JGoGlUZm+SDYUxLvtfee5QT/1rnBzryj
1484YlLy44h9GE6hUpfrgeWW82yMTDpKE/OnKvf6hZDpdwYUfAhaEVsG28CgYEAxf2r
14954whThFc5blPhZzXqUd+pmVpYUDzbcU/8JWr2z1UGyGG1CeLymlNjoyERNHzyYgv
15011xHQ++C3l94H6Ln2rvzN4UplAy6Kg2Qp4OcxirTpTzC0JH9ENsVXrDOXgidnT1m
151Q0fI4JKF2i0WtWCBVKH5duAJHmm6jrueqYBMCb0CgYAGjfuuaOXYdLDWrOszwf15
152XYoHj98S5bdYacWuq+3vs8kne4V6mITjfmiGwXM8r8Y3lKIj+3c3bpy9fPoXOrnl
153IfltVVnCfZh6IDo23ktHYT4ac6u+t1bTtn0QRkGqsjygpLacEZ9e7gW56pslbEjT
154ydf+Vo/zOiAGz3qvCqsdvwKBgD8urpcbvt0c6VpeAqSO4ruglygeoLxqb8SN99Oc
155z9UNAzQNROubGx8i758X9HXJ0lRz42CEkDF703WHXdwqrna9F+aV1ijqztXwskLi
1562zraG+RVOY45qFqir3UiUxjrAvFU0LeTxuTznUd8sAx9mYgakHOBFyffcatu8DkD
157OkC5AoGAfX/RqxPg0CGAc2+4X9UjMxC78/t4o8uN547yIOAooYlAMyLnNFSWlQim
158XMSDGcS6Aweq6Hi6GAUvhoVazKB1I5m7+L6Fvx13WiJWVGzGtMvatHIN0PKGsOW8
159Es60mcRJGnbnZ+PjBSmy0b5mBV+1nujpi/THyGv5KHG4iCLtE0g=
160-----END RSA PRIVATE KEY-----";
161
162 fn claims() -> (Value, Value) {
163 (
164 json!({ "alg": "RS256", "typ": "JWT" }),
165 json!({
166 "iss": "svc@project.iam.gserviceaccount.com",
167 "scope": "https://www.googleapis.com/auth/firebase.messaging",
168 "aud": "https://oauth2.googleapis.com/token",
169 "iat": 1_700_000_000,
170 "exp": 1_700_003_600,
171 }),
172 )
173 }
174
175 #[test]
176 fn signs_a_jwt_that_verifies_under_pkcs1v15() {
177 let signer = Rs256Signer::from_pkcs8_pem(TEST_PKCS8).unwrap();
178 let (header, payload) = claims();
179 let jwt = signer.sign_jwt(&header, &payload);
180
181 let parts: Vec<&str> = jwt.split('.').collect();
182 assert_eq!(parts.len(), 3, "header.claims.signature");
183 let signature = URL_SAFE_NO_PAD.decode(parts[2]).expect("base64url");
184 let public = rsa::RsaPublicKey::from(signer.key.as_ref());
185 assert_eq!(signature.len(), public.size(), "a full-width RSA signature");
186
187 let digest = rsa::sha2::Sha256::digest(format!("{}.{}", parts[0], parts[1]).as_bytes());
191 public
192 .verify(
193 rsa::Pkcs1v15Sign::new::<rsa::sha2::Sha256>(),
194 &digest,
195 &signature,
196 )
197 .expect("the JWT verifies under the matching public key");
198 }
199
200 #[test]
201 fn both_pem_encodings_parse_to_the_same_key() {
202 let (header, payload) = claims();
203 let from_pkcs8 = Rs256Signer::from_pem(TEST_PKCS8).unwrap();
204 let from_pkcs1 = Rs256Signer::from_pem(TEST_PKCS1).unwrap();
205 assert_eq!(
208 from_pkcs8.sign_jwt(&header, &payload),
209 from_pkcs1.sign_jwt(&header, &payload)
210 );
211 }
212
213 #[test]
214 fn rejects_a_pem_that_is_not_a_key() {
215 let error = Rs256Signer::from_pem(
216 "-----BEGIN RSA PRIVATE KEY-----\nnope\n-----END RSA PRIVATE KEY-----",
217 )
218 .unwrap_err();
219 assert!(error.to_string().contains("RS256"), "{error}");
220 let error = Rs256Signer::from_pem("not a key at all").unwrap_err();
221 assert!(error.to_string().contains("RS256"), "{error}");
222 }
223}