baseid_crypto/
jwt.rs

1//! JWT (JSON Web Token) encode, decode, and verify.
2//!
3//! Provides compact JWT serialization used by JWT-VC and SD-JWT.
4
5use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
6use baseid_core::error::{CryptoError, SerializationError};
7use baseid_core::types::SignatureAlgorithm;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11use crate::signer::{Signer, Verifier};
12
13/// JWT header (JOSE header).
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct JwtHeader {
16    /// Signature algorithm (e.g., "EdDSA", "ES256").
17    pub alg: String,
18    /// Token type (typically "JWT").
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub typ: Option<String>,
21    /// Key ID.
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub kid: Option<String>,
24    /// Additional header parameters (for SD-JWT extensibility).
25    #[serde(flatten)]
26    pub additional: serde_json::Map<String, Value>,
27}
28
29/// Map a `SignatureAlgorithm` to its JWA string.
30pub fn alg_to_str(alg: SignatureAlgorithm) -> &'static str {
31    match alg {
32        SignatureAlgorithm::EdDsa => "EdDSA",
33        SignatureAlgorithm::Es256 => "ES256",
34        SignatureAlgorithm::Es384 => "ES384",
35        SignatureAlgorithm::Es256k => "ES256K",
36        SignatureAlgorithm::BbsPlus => "BBS+",
37    }
38}
39
40/// Map a JWA string to a `SignatureAlgorithm`.
41pub fn str_to_alg(s: &str) -> baseid_core::Result<SignatureAlgorithm> {
42    match s {
43        "EdDSA" => Ok(SignatureAlgorithm::EdDsa),
44        "ES256" => Ok(SignatureAlgorithm::Es256),
45        "ES384" => Ok(SignatureAlgorithm::Es384),
46        "ES256K" => Ok(SignatureAlgorithm::Es256k),
47        _ => Err(CryptoError::UnsupportedAlgorithm.into()),
48    }
49}
50
51/// Encode and sign a JWT, returning the compact serialization (`header.payload.signature`).
52pub fn encode_jwt(
53    header: &JwtHeader,
54    claims: &Value,
55    signer: &dyn Signer,
56) -> baseid_core::Result<String> {
57    let header_json = serde_json::to_vec(header).map_err(SerializationError::Json)?;
58    let claims_json = serde_json::to_vec(claims).map_err(SerializationError::Json)?;
59
60    let header_b64 = URL_SAFE_NO_PAD.encode(&header_json);
61    let claims_b64 = URL_SAFE_NO_PAD.encode(&claims_json);
62
63    let signing_input = format!("{header_b64}.{claims_b64}");
64    let signature = signer.sign(signing_input.as_bytes())?;
65    let sig_b64 = URL_SAFE_NO_PAD.encode(&signature);
66
67    Ok(format!("{signing_input}.{sig_b64}"))
68}
69
70/// Decode a JWT without verifying the signature.
71///
72/// Returns the header and claims. Useful for inspecting tokens before
73/// selecting the right verification key.
74pub fn decode_jwt_unverified(jwt: &str) -> baseid_core::Result<(JwtHeader, Value)> {
75    let parts: Vec<&str> = jwt.splitn(4, '.').collect();
76    if parts.len() != 3 {
77        return Err(CryptoError::VerificationFailed.into());
78    }
79
80    let header_bytes = URL_SAFE_NO_PAD
81        .decode(parts[0])
82        .map_err(|_| CryptoError::VerificationFailed)?;
83    let claims_bytes = URL_SAFE_NO_PAD
84        .decode(parts[1])
85        .map_err(|_| CryptoError::VerificationFailed)?;
86
87    let header: JwtHeader =
88        serde_json::from_slice(&header_bytes).map_err(SerializationError::Json)?;
89    let claims: Value = serde_json::from_slice(&claims_bytes).map_err(SerializationError::Json)?;
90
91    Ok((header, claims))
92}
93
94/// Decode a JWT with signature verification.
95///
96/// Validates that the header `alg` matches the verifier's algorithm,
97/// verifies the signature, then returns the header and claims.
98pub fn decode_jwt(jwt: &str, verifier: &dyn Verifier) -> baseid_core::Result<(JwtHeader, Value)> {
99    let parts: Vec<&str> = jwt.splitn(4, '.').collect();
100    if parts.len() != 3 {
101        return Err(CryptoError::VerificationFailed.into());
102    }
103
104    // Validate algorithm before verifying signature (prevents algorithm confusion)
105    let header_bytes = URL_SAFE_NO_PAD
106        .decode(parts[0])
107        .map_err(|_| CryptoError::VerificationFailed)?;
108    let header: JwtHeader =
109        serde_json::from_slice(&header_bytes).map_err(SerializationError::Json)?;
110
111    let expected_alg = alg_to_str(verifier.algorithm());
112    if header.alg != expected_alg {
113        return Err(CryptoError::VerificationFailed.into());
114    }
115
116    let signing_input = format!("{}.{}", parts[0], parts[1]);
117    let signature = URL_SAFE_NO_PAD
118        .decode(parts[2])
119        .map_err(|_| CryptoError::VerificationFailed)?;
120
121    let valid = verifier.verify(signing_input.as_bytes(), &signature)?;
122    if !valid {
123        return Err(CryptoError::VerificationFailed.into());
124    }
125
126    let claims_bytes = URL_SAFE_NO_PAD
127        .decode(parts[1])
128        .map_err(|_| CryptoError::VerificationFailed)?;
129    let claims: Value = serde_json::from_slice(&claims_bytes).map_err(SerializationError::Json)?;
130
131    Ok((header, claims))
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::key::KeyPair;
138    use baseid_core::types::KeyType;
139
140    fn roundtrip(key_type: KeyType) {
141        let kp = KeyPair::generate(key_type).unwrap();
142        let header = JwtHeader {
143            alg: alg_to_str(kp.algorithm()).to_string(),
144            typ: Some("JWT".to_string()),
145            kid: Some("key-1".to_string()),
146            additional: serde_json::Map::new(),
147        };
148        let claims = serde_json::json!({
149            "sub": "did:key:z6Mk...",
150            "iss": "did:key:z6Mk...",
151            "exp": 9999999999u64,
152        });
153
154        let jwt = encode_jwt(&header, &claims, &kp).unwrap();
155        let (decoded_header, decoded_claims) = decode_jwt(&jwt, &kp.public).unwrap();
156
157        assert_eq!(decoded_header.alg, header.alg);
158        assert_eq!(decoded_header.typ, header.typ);
159        assert_eq!(decoded_header.kid, header.kid);
160        assert_eq!(decoded_claims, claims);
161    }
162
163    #[test]
164    fn roundtrip_ed25519() {
165        roundtrip(KeyType::Ed25519);
166    }
167
168    #[test]
169    fn roundtrip_p256() {
170        roundtrip(KeyType::P256);
171    }
172
173    #[test]
174    fn roundtrip_p384() {
175        roundtrip(KeyType::P384);
176    }
177
178    #[test]
179    fn roundtrip_secp256k1() {
180        roundtrip(KeyType::Secp256k1);
181    }
182
183    #[test]
184    fn unverified_decode() {
185        let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
186        let header = JwtHeader {
187            alg: "EdDSA".to_string(),
188            typ: Some("JWT".to_string()),
189            kid: None,
190            additional: serde_json::Map::new(),
191        };
192        let claims = serde_json::json!({"sub": "test"});
193        let jwt = encode_jwt(&header, &claims, &kp).unwrap();
194
195        let (h, c) = decode_jwt_unverified(&jwt).unwrap();
196        assert_eq!(h.alg, "EdDSA");
197        assert_eq!(c["sub"], "test");
198    }
199
200    #[test]
201    fn wrong_key_rejected() {
202        let kp1 = KeyPair::generate(KeyType::Ed25519).unwrap();
203        let kp2 = KeyPair::generate(KeyType::Ed25519).unwrap();
204        let header = JwtHeader {
205            alg: "EdDSA".to_string(),
206            typ: Some("JWT".to_string()),
207            kid: None,
208            additional: serde_json::Map::new(),
209        };
210        let claims = serde_json::json!({"sub": "test"});
211        let jwt = encode_jwt(&header, &claims, &kp1).unwrap();
212
213        let result = decode_jwt(&jwt, &kp2.public);
214        assert!(result.is_err());
215    }
216
217    #[test]
218    fn tampered_claims_rejected() {
219        let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
220        let header = JwtHeader {
221            alg: "EdDSA".to_string(),
222            typ: Some("JWT".to_string()),
223            kid: None,
224            additional: serde_json::Map::new(),
225        };
226        let claims = serde_json::json!({"sub": "test"});
227        let jwt = encode_jwt(&header, &claims, &kp).unwrap();
228
229        // Tamper with the claims part
230        let parts: Vec<&str> = jwt.split('.').collect();
231        let fake_claims = URL_SAFE_NO_PAD.encode(b"{\"sub\":\"hacked\"}");
232        let tampered = format!("{}.{}.{}", parts[0], fake_claims, parts[2]);
233
234        let result = decode_jwt(&tampered, &kp.public);
235        assert!(result.is_err());
236    }
237
238    #[test]
239    fn malformed_jwt_rejected() {
240        let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
241        assert!(decode_jwt("not-a-jwt", &kp.public).is_err());
242        assert!(decode_jwt("a.b", &kp.public).is_err());
243        assert!(decode_jwt("a.b.c.d", &kp.public).is_err());
244        assert!(decode_jwt_unverified("single").is_err());
245    }
246
247    #[test]
248    fn alg_string_roundtrip() {
249        for alg in [
250            SignatureAlgorithm::EdDsa,
251            SignatureAlgorithm::Es256,
252            SignatureAlgorithm::Es384,
253            SignatureAlgorithm::Es256k,
254        ] {
255            let s = alg_to_str(alg);
256            let back = str_to_alg(s).unwrap();
257            assert_eq!(back, alg);
258        }
259    }
260
261    #[test]
262    fn unknown_alg_rejected() {
263        assert!(str_to_alg("RS256").is_err());
264    }
265
266    #[test]
267    fn algorithm_confusion_rejected() {
268        // Sign with Ed25519 but try to verify with P-256 key
269        // (even if signature somehow matched, the alg mismatch should reject)
270        let ed_kp = KeyPair::generate(KeyType::Ed25519).unwrap();
271        let p256_kp = KeyPair::generate(KeyType::P256).unwrap();
272
273        let header = JwtHeader {
274            alg: "EdDSA".to_string(),
275            typ: Some("JWT".to_string()),
276            kid: None,
277            additional: serde_json::Map::new(),
278        };
279        let claims = serde_json::json!({"sub": "test"});
280        let jwt = encode_jwt(&header, &claims, &ed_kp).unwrap();
281
282        // P-256 verifier expects ES256, but header says EdDSA → must reject
283        let result = decode_jwt(&jwt, &p256_kp.public);
284        assert!(result.is_err(), "Algorithm confusion should be rejected");
285    }
286
287    #[test]
288    fn tampered_alg_header_rejected() {
289        // Sign with Ed25519, tamper header to say ES256, verify with Ed25519 key
290        let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
291        let header = JwtHeader {
292            alg: "ES256".to_string(), // WRONG: key is Ed25519
293            typ: Some("JWT".to_string()),
294            kid: None,
295            additional: serde_json::Map::new(),
296        };
297        let claims = serde_json::json!({"sub": "test"});
298
299        // encode_jwt will sign with Ed25519 regardless of header.alg
300        let jwt = encode_jwt(&header, &claims, &kp).unwrap();
301
302        // Ed25519 verifier expects EdDSA, but header says ES256 → must reject
303        let result = decode_jwt(&jwt, &kp.public);
304        assert!(result.is_err(), "Tampered alg header should be rejected");
305    }
306
307    // --- C1: Cross-library JWT interop with jsonwebtoken crate ---
308
309    mod interop {
310        use super::*;
311        use base64::engine::general_purpose::URL_SAFE_NO_PAD;
312
313        /// Sign a JWT with baseid-crypto, verify with jsonwebtoken.
314        #[test]
315        fn baseid_sign_jsonwebtoken_verify_ed25519() {
316            let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
317
318            let header = JwtHeader {
319                alg: "EdDSA".to_string(),
320                typ: Some("JWT".to_string()),
321                kid: None,
322                additional: serde_json::Map::new(),
323            };
324            let claims = serde_json::json!({
325                "sub": "user123",
326                "iss": "baseid",
327                "exp": 9999999999u64,
328            });
329            let jwt_str = encode_jwt(&header, &claims, &kp).unwrap();
330
331            // Build jsonwebtoken DecodingKey from raw Ed25519 public key bytes
332            let x_b64 = URL_SAFE_NO_PAD.encode(&kp.public.bytes);
333            let dk = jsonwebtoken::DecodingKey::from_ed_components(&x_b64).unwrap();
334
335            let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::EdDSA);
336            validation.set_required_spec_claims::<String>(&[]);
337            validation.validate_exp = false;
338
339            let token_data: jsonwebtoken::TokenData<serde_json::Value> =
340                jsonwebtoken::decode(&jwt_str, &dk, &validation).unwrap();
341
342            assert_eq!(token_data.claims["sub"], "user123");
343            assert_eq!(token_data.claims["iss"], "baseid");
344        }
345
346        /// Sign a JWT with jsonwebtoken, verify with baseid-crypto.
347        #[test]
348        fn jsonwebtoken_sign_baseid_verify_ed25519() {
349            let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
350
351            // Build jsonwebtoken EncodingKey from raw Ed25519 secret bytes (PKCS8 DER)
352            let signing_key =
353                ed25519_dalek::SigningKey::from_bytes(kp.secret_bytes().try_into().unwrap());
354            let pkcs8_der = build_ed25519_pkcs8_der(&signing_key);
355            let ek = jsonwebtoken::EncodingKey::from_ed_der(&pkcs8_der);
356
357            let jw_header = jsonwebtoken::Header::new(jsonwebtoken::Algorithm::EdDSA);
358            let claims = serde_json::json!({
359                "sub": "from_jsonwebtoken",
360                "iss": "external",
361                "exp": 9999999999u64,
362            });
363            let jwt_str = jsonwebtoken::encode(&jw_header, &claims, &ek).unwrap();
364
365            // Verify with baseid-crypto
366            let (decoded_header, decoded_claims) = decode_jwt(&jwt_str, &kp.public).unwrap();
367            assert_eq!(decoded_header.alg, "EdDSA");
368            assert_eq!(decoded_claims["sub"], "from_jsonwebtoken");
369            assert_eq!(decoded_claims["iss"], "external");
370        }
371
372        /// Sign with baseid-crypto ES256, verify with jsonwebtoken.
373        #[test]
374        fn baseid_sign_jsonwebtoken_verify_p256() {
375            let kp = KeyPair::generate(KeyType::P256).unwrap();
376
377            let header = JwtHeader {
378                alg: "ES256".to_string(),
379                typ: Some("JWT".to_string()),
380                kid: None,
381                additional: serde_json::Map::new(),
382            };
383            let claims = serde_json::json!({"sub": "p256_test", "exp": 9999999999u64});
384            let jwt_str = encode_jwt(&header, &claims, &kp).unwrap();
385
386            // Decompress P-256 public key to get x,y coordinates
387            let (x, y) = decompress_p256(&kp.public.bytes);
388            let x_b64 = URL_SAFE_NO_PAD.encode(&x);
389            let y_b64 = URL_SAFE_NO_PAD.encode(&y);
390
391            let dk = jsonwebtoken::DecodingKey::from_ec_components(&x_b64, &y_b64).unwrap();
392            let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::ES256);
393            validation.set_required_spec_claims::<String>(&[]);
394            validation.validate_exp = false;
395
396            let token_data: jsonwebtoken::TokenData<serde_json::Value> =
397                jsonwebtoken::decode(&jwt_str, &dk, &validation).unwrap();
398            assert_eq!(token_data.claims["sub"], "p256_test");
399        }
400
401        /// Sign with baseid-crypto ES384, verify with jsonwebtoken.
402        #[test]
403        fn baseid_sign_jsonwebtoken_verify_p384() {
404            let kp = KeyPair::generate(KeyType::P384).unwrap();
405
406            let header = JwtHeader {
407                alg: "ES384".to_string(),
408                typ: Some("JWT".to_string()),
409                kid: None,
410                additional: serde_json::Map::new(),
411            };
412            let claims = serde_json::json!({"sub": "p384_test", "exp": 9999999999u64});
413            let jwt_str = encode_jwt(&header, &claims, &kp).unwrap();
414
415            let (x, y) = decompress_p384(&kp.public.bytes);
416            let x_b64 = URL_SAFE_NO_PAD.encode(&x);
417            let y_b64 = URL_SAFE_NO_PAD.encode(&y);
418
419            let dk = jsonwebtoken::DecodingKey::from_ec_components(&x_b64, &y_b64).unwrap();
420            let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::ES384);
421            validation.set_required_spec_claims::<String>(&[]);
422            validation.validate_exp = false;
423
424            let token_data: jsonwebtoken::TokenData<serde_json::Value> =
425                jsonwebtoken::decode(&jwt_str, &dk, &validation).unwrap();
426            assert_eq!(token_data.claims["sub"], "p384_test");
427        }
428
429        /// Build a minimal PKCS#8 DER encoding for an Ed25519 private key.
430        /// PKCS#8 wraps the 32-byte seed in an ASN.1 structure.
431        fn build_ed25519_pkcs8_der(signing_key: &ed25519_dalek::SigningKey) -> Vec<u8> {
432            // PKCS#8 for Ed25519: SEQUENCE { version, algorithm, key }
433            // algorithm = SEQUENCE { OID 1.3.101.112 }
434            // key = OCTET STRING containing OCTET STRING of 32-byte seed
435            let seed = signing_key.to_bytes();
436            let mut der = Vec::new();
437            // Outer SEQUENCE
438            der.push(0x30);
439            der.push(0x2e); // length = 46
440                            // Version INTEGER 0
441            der.extend_from_slice(&[0x02, 0x01, 0x00]);
442            // Algorithm SEQUENCE { OID }
443            der.extend_from_slice(&[0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70]);
444            // PrivateKey OCTET STRING wrapping OCTET STRING
445            der.push(0x04); // outer OCTET STRING
446            der.push(0x22); // length = 34
447            der.push(0x04); // inner OCTET STRING
448            der.push(0x20); // length = 32
449            der.extend_from_slice(&seed);
450            der
451        }
452
453        /// Decompress a P-256 compressed public key to (x, y).
454        fn decompress_p256(compressed: &[u8]) -> (Vec<u8>, Vec<u8>) {
455            use p256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint};
456            let point = p256::EncodedPoint::from_bytes(compressed).unwrap();
457            let affine = p256::AffinePoint::from_encoded_point(&point).unwrap();
458            let uncompressed = affine.to_encoded_point(false);
459            (
460                uncompressed.x().unwrap().to_vec(),
461                uncompressed.y().unwrap().to_vec(),
462            )
463        }
464
465        /// Decompress a P-384 compressed public key to (x, y).
466        fn decompress_p384(compressed: &[u8]) -> (Vec<u8>, Vec<u8>) {
467            use p384::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint};
468            let point = p384::EncodedPoint::from_bytes(compressed).unwrap();
469            let affine = p384::AffinePoint::from_encoded_point(&point).unwrap();
470            let uncompressed = affine.to_encoded_point(false);
471            (
472                uncompressed.x().unwrap().to_vec(),
473                uncompressed.y().unwrap().to_vec(),
474            )
475        }
476    }
477}