1use crate::error::{SignatifError, SignatifResult};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum JwsAlg {
16 Ed25519,
18 Es256,
20}
21
22impl JwsAlg {
23 pub fn as_str(&self) -> &'static str {
25 match self {
26 JwsAlg::Ed25519 => "EdDSA",
27 JwsAlg::Es256 => "ES256",
28 }
29 }
30}
31
32fn b64url_encode(bytes: &[u8]) -> String {
33 use base64ct::Encoding as _;
34 base64ct::Base64UrlUnpadded::encode_string(bytes)
35}
36
37fn b64url_decode(s: &str) -> SignatifResult<Vec<u8>> {
38 use base64ct::Encoding as _;
39 base64ct::Base64UrlUnpadded::decode_vec(s)
40 .map_err(|e| SignatifError::Encoding(format!("base64url decode: {e}")))
41}
42
43pub fn detached_signing_input(
52 alg: JwsAlg,
53 kid: Option<&str>,
54 external_payload: &[u8],
55) -> SignatifResult<String> {
56 let mut header = serde_json::Map::new();
57 header.insert("alg".into(), serde_json::json!(alg.as_str()));
58 if let Some(k) = kid {
59 header.insert("kid".into(), serde_json::json!(k));
60 }
61 header.insert("b64".into(), serde_json::json!(false));
62 header.insert("crit".into(), serde_json::json!(["b64"]));
63 let header_json = crate::jcs::canonicalize(&serde_json::Value::Object(header))?;
64 Ok(format!(
65 "{}.{}",
66 b64url_encode(header_json.as_bytes()),
67 b64url_encode(external_payload)
68 ))
69}
70
71pub fn sign_detached_ed25519(
78 signing_key: &ed25519_dalek::SigningKey,
79 kid: Option<&str>,
80 external_payload: &[u8],
81) -> SignatifResult<String> {
82 use ed25519_dalek::Signer as _;
83 let input = detached_signing_input(JwsAlg::Ed25519, kid, external_payload)?;
84 let sig = signing_key.sign(input.as_bytes());
85 let (_, header_b64) = input.split_once('.').expect("two segments");
86 let _ = header_b64;
87 Ok(format!(
88 "{}.{}",
89 input.split('.').next().expect("header"),
90 b64url_encode(&sig.to_bytes())
91 ))
92}
93
94pub fn verify_detached_ed25519(
103 jws: &str,
104 external_payload: &[u8],
105 public_key: &[u8],
106) -> SignatifResult<()> {
107 use ed25519_dalek::Signature;
108 use ed25519_dalek::Verifier;
109 let (header_b64, sig_b64) = jws
110 .split_once('.')
111 .ok_or_else(|| SignatifError::ArtifactFormat("JWS must be header.signature".into()))?;
112 let header_bytes = b64url_decode(header_b64)?;
113 let header: serde_json::Value = serde_json::from_slice(&header_bytes)
114 .map_err(|e| SignatifError::ArtifactFormat(format!("JWS header: {e}")))?;
115 let alg = header
116 .get("alg")
117 .and_then(|v| v.as_str())
118 .ok_or_else(|| SignatifError::ArtifactFormat("JWS header lacks alg".into()))?;
119 if alg != JwsAlg::Ed25519.as_str() {
120 return Err(SignatifError::ArtifactFormat(format!(
121 "unsupported JWS alg {alg}"
122 )));
123 }
124 let kid = header.get("kid").and_then(|v| v.as_str());
125 let input = detached_signing_input(JwsAlg::Ed25519, kid, external_payload)?;
126 let sig_bytes = b64url_decode(sig_b64)?;
127 let vk = ed25519_dalek::VerifyingKey::from_bytes(public_key.try_into().map_err(|_| {
128 SignatifError::BadSignature {
129 context: "Ed25519 public key must be 32 bytes".into(),
130 }
131 })?)
132 .map_err(|e| SignatifError::BadSignature {
133 context: format!("Ed25519 pubkey: {e}"),
134 })?;
135 let sig = Signature::from_slice(&sig_bytes).map_err(|_| SignatifError::BadSignature {
136 context: "Ed25519 signature must be 64 bytes".into(),
137 })?;
138 vk.verify(input.as_bytes(), &sig)
139 .map_err(|_| SignatifError::BadSignature {
140 context: "JWS detached signature".into(),
141 })
142}
143
144pub fn sign_detached_es256(
152 signing_key: &p256::ecdsa::SigningKey,
153 kid: Option<&str>,
154 external_payload: &[u8],
155) -> SignatifResult<String> {
156 use p256::ecdsa::signature::Signer as _;
157 let input = detached_signing_input(JwsAlg::Es256, kid, external_payload)?;
158 let sig: p256::ecdsa::Signature = signing_key.sign(input.as_bytes());
159 let header_b64 = input.split('.').next().expect("header segment").to_string();
160 Ok(format!("{}.{}", header_b64, b64url_encode(&sig.to_bytes())))
161}
162
163pub fn verify_detached_es256(
169 jws: &str,
170 external_payload: &[u8],
171 public_key: &p256::ecdsa::VerifyingKey,
172) -> SignatifResult<()> {
173 use p256::ecdsa::signature::Verifier as _;
174 let (header_b64, sig_b64) = jws
175 .split_once('.')
176 .ok_or_else(|| SignatifError::ArtifactFormat("JWS must be header.signature".into()))?;
177 let header_bytes = b64url_decode(header_b64)?;
178 let header: serde_json::Value = serde_json::from_slice(&header_bytes)
179 .map_err(|e| SignatifError::ArtifactFormat(format!("JWS header: {e}")))?;
180 let alg = header
181 .get("alg")
182 .and_then(|v| v.as_str())
183 .ok_or_else(|| SignatifError::ArtifactFormat("JWS header lacks alg".into()))?;
184 if alg != JwsAlg::Es256.as_str() {
185 return Err(SignatifError::ArtifactFormat(format!(
186 "unsupported JWS alg {alg}"
187 )));
188 }
189 let kid = header.get("kid").and_then(|v| v.as_str());
190 let input = detached_signing_input(JwsAlg::Es256, kid, external_payload)?;
191 let sig_bytes = b64url_decode(sig_b64)?;
192 let sig = p256::ecdsa::Signature::from_slice(&sig_bytes).map_err(|_| {
193 SignatifError::BadSignature {
194 context: "ES256 signature must be 64-byte r||s".into(),
195 }
196 })?;
197 public_key
198 .verify(input.as_bytes(), &sig)
199 .map_err(|_| SignatifError::BadSignature {
200 context: "JWS detached ES256 signature".into(),
201 })
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use rand_core::RngCore;
208
209 fn generate_key() -> ed25519_dalek::SigningKey {
210 let mut seed = [0u8; 32];
211 rand_core::OsRng.fill_bytes(&mut seed);
212 ed25519_dalek::SigningKey::from_bytes(&seed)
213 }
214
215 #[test]
216 fn detached_jws_round_trip() {
217 let sk = generate_key();
218 let payload = crate::jcs::canonicalize(&serde_json::json!({"batch":"LOT-1"}))
219 .unwrap()
220 .into_bytes();
221 let jws = sign_detached_ed25519(&sk, Some("end-cert-7"), &payload).unwrap();
222 assert_eq!(jws.split('.').count(), 2);
224 let pk = sk.verifying_key().as_bytes().to_vec();
225 assert!(verify_detached_ed25519(&jws, &payload, &pk).is_ok());
226
227 assert!(verify_detached_ed25519(&jws, b"other", &pk).is_err());
229
230 let mut parts: Vec<&str> = jws.split('.').collect();
232 let mut sig = b64url_decode(parts[1]).unwrap();
233 sig[0] ^= 1;
234 parts[1] = "";
235 let tampered = format!("{}.{}", parts[0], b64url_encode(&sig));
236 assert!(verify_detached_ed25519(&tampered, &payload, &pk).is_err());
237 }
238
239 #[test]
240 fn es256_round_trip() {
241 use p256::elliptic_curve::Generate;
242 let sk = p256::ecdsa::SigningKey::generate();
243 let payload = crate::jcs::canonicalize(&serde_json::json!({"dose":500}))
244 .unwrap()
245 .into_bytes();
246 let jws = sign_detached_es256(&sk, Some("end-p256"), &payload).unwrap();
247 let sig_len = b64url_decode(jws.split('.').nth(1).unwrap()).unwrap().len();
249 assert_eq!(sig_len, 64, "ES256 must be r||s, not DER");
250 assert!(verify_detached_es256(&jws, &payload, sk.verifying_key()).is_ok());
251 assert!(verify_detached_es256(&jws, b"other", sk.verifying_key()).is_err());
252 let other: p256::ecdsa::SigningKey = p256::ecdsa::SigningKey::generate();
254 assert!(verify_detached_es256(&jws, &payload, other.verifying_key()).is_err());
255 }
256
257 #[test]
258 fn signing_input_is_deterministic() {
259 let a = detached_signing_input(JwsAlg::Ed25519, None, b"payload").unwrap();
260 let b = detached_signing_input(JwsAlg::Ed25519, None, b"payload").unwrap();
261 assert_eq!(a, b);
262 assert!(
263 a.starts_with("eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19"),
264 "header was {a}"
265 );
266 }
267}