1use baseid_crypto::{alg_to_str, encode_jwt, JwtHeader, Signer};
4use std::time::{SystemTime, UNIX_EPOCH};
5
6pub fn create_proof_jwt(
13 signer: &dyn Signer,
14 issuer_url: &str,
15 c_nonce: &str,
16 did: &str,
17) -> baseid_core::Result<String> {
18 let alg_str = alg_to_str(signer.algorithm());
19
20 let header = JwtHeader {
21 alg: alg_str.to_string(),
22 typ: Some("openid4vci-proof+jwt".to_string()),
23 kid: Some(format!("{did}#key-1")),
24 additional: serde_json::Map::new(),
25 };
26
27 let iat = SystemTime::now()
28 .duration_since(UNIX_EPOCH)
29 .unwrap_or_default()
30 .as_secs();
31
32 let claims = serde_json::json!({
33 "iss": did,
34 "aud": issuer_url,
35 "iat": iat,
36 "nonce": c_nonce,
37 });
38
39 encode_jwt(&header, &claims, signer)
40}
41
42#[cfg(test)]
43mod tests {
44 use super::*;
45 use baseid_core::types::KeyType;
46 use baseid_crypto::{decode_jwt_unverified, KeyPair};
47
48 #[test]
49 fn proof_jwt_structure() {
50 let kp = KeyPair::generate(KeyType::P256).unwrap();
51 let jwt = create_proof_jwt(
52 &kp,
53 "https://issuer.example.com",
54 "server-nonce-123",
55 "did:key:zDnae...",
56 )
57 .unwrap();
58
59 let (header, claims) = decode_jwt_unverified(&jwt).unwrap();
60
61 assert_eq!(header.typ.as_deref(), Some("openid4vci-proof+jwt"));
62 assert_eq!(header.alg, "ES256");
63 assert_eq!(header.kid.as_deref(), Some("did:key:zDnae...#key-1"));
64 assert_eq!(claims["iss"], "did:key:zDnae...");
65 assert_eq!(claims["aud"], "https://issuer.example.com");
66 assert_eq!(claims["nonce"], "server-nonce-123");
67 assert!(claims["iat"].is_u64());
68 }
69
70 #[test]
71 fn proof_jwt_ed25519() {
72 let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
73 let jwt = create_proof_jwt(
74 &kp,
75 "https://issuer.example.com",
76 "nonce",
77 "did:key:z6Mk...",
78 )
79 .unwrap();
80
81 let (header, _) = decode_jwt_unverified(&jwt).unwrap();
82 assert_eq!(header.alg, "EdDSA");
83 }
84
85 mod interop {
88 use super::*;
89 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
90 use base64::Engine as _;
91
92 #[test]
94 fn baseid_proof_jwt_jsonwebtoken_verify_ed25519() {
95 let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
96 let jwt = create_proof_jwt(
97 &kp,
98 "https://issuer.example.com",
99 "nonce-abc",
100 "did:key:z6MkTest",
101 )
102 .unwrap();
103
104 let x_b64 = URL_SAFE_NO_PAD.encode(&kp.public.bytes);
106 let dk = jsonwebtoken::DecodingKey::from_ed_components(&x_b64).unwrap();
107
108 let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::EdDSA);
109 validation.set_required_spec_claims::<String>(&[]);
110 validation.validate_exp = false;
111 validation.validate_aud = false;
112
113 let token_data: jsonwebtoken::TokenData<serde_json::Value> =
114 jsonwebtoken::decode(&jwt, &dk, &validation).unwrap();
115
116 assert_eq!(token_data.claims["iss"], "did:key:z6MkTest");
117 assert_eq!(token_data.claims["aud"], "https://issuer.example.com");
118 assert_eq!(token_data.claims["nonce"], "nonce-abc");
119 assert!(token_data.claims["iat"].is_u64());
120
121 assert_eq!(
123 token_data.header.typ,
124 Some("openid4vci-proof+jwt".to_string())
125 );
126 assert_eq!(token_data.header.alg, jsonwebtoken::Algorithm::EdDSA);
127 assert_eq!(
128 token_data.header.kid,
129 Some("did:key:z6MkTest#key-1".to_string())
130 );
131 }
132
133 #[test]
135 fn baseid_proof_jwt_jsonwebtoken_verify_p256() {
136 let kp = KeyPair::generate(KeyType::P256).unwrap();
137 let jwt = create_proof_jwt(
138 &kp,
139 "https://issuer.example.com",
140 "nonce-xyz",
141 "did:key:zDnaePK",
142 )
143 .unwrap();
144
145 let (x, y) = decompress_p256(&kp.public.bytes);
147 let x_b64 = URL_SAFE_NO_PAD.encode(&x);
148 let y_b64 = URL_SAFE_NO_PAD.encode(&y);
149
150 let dk = jsonwebtoken::DecodingKey::from_ec_components(&x_b64, &y_b64).unwrap();
151 let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::ES256);
152 validation.set_required_spec_claims::<String>(&[]);
153 validation.validate_exp = false;
154 validation.validate_aud = false;
155
156 let token_data: jsonwebtoken::TokenData<serde_json::Value> =
157 jsonwebtoken::decode(&jwt, &dk, &validation).unwrap();
158
159 assert_eq!(token_data.claims["iss"], "did:key:zDnaePK");
160 assert_eq!(token_data.claims["aud"], "https://issuer.example.com");
161 assert_eq!(token_data.claims["nonce"], "nonce-xyz");
162
163 assert_eq!(
165 token_data.header.typ,
166 Some("openid4vci-proof+jwt".to_string())
167 );
168 assert_eq!(token_data.header.alg, jsonwebtoken::Algorithm::ES256);
169 }
170
171 #[test]
173 fn baseid_proof_jwt_header_spec_compliance() {
174 for key_type in [KeyType::Ed25519, KeyType::P256] {
175 let kp = KeyPair::generate(key_type).unwrap();
176 let jwt = create_proof_jwt(
177 &kp,
178 "https://issuer.example.com",
179 "server-nonce",
180 "did:key:zTest",
181 )
182 .unwrap();
183
184 let parts: Vec<&str> = jwt.split('.').collect();
186 assert_eq!(parts.len(), 3, "JWT must have 3 parts");
187
188 let header_bytes = URL_SAFE_NO_PAD.decode(parts[0]).unwrap();
189 let header: serde_json::Value = serde_json::from_slice(&header_bytes).unwrap();
190
191 assert_eq!(header["typ"], "openid4vci-proof+jwt");
193
194 let alg = header["alg"].as_str().unwrap();
196 assert!(
197 ["EdDSA", "ES256", "ES384", "ES256K"].contains(&alg),
198 "Unexpected alg: {alg}"
199 );
200
201 let kid = header["kid"].as_str().unwrap();
203 assert!(
204 kid.contains('#'),
205 "kid must contain '#' fragment separator: {kid}"
206 );
207 assert!(kid.starts_with("did:"), "kid must start with 'did:': {kid}");
208 }
209 }
210
211 fn decompress_p256(compressed: &[u8]) -> (Vec<u8>, Vec<u8>) {
213 use p256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint};
214 let point = p256::EncodedPoint::from_bytes(compressed).unwrap();
215 let affine = p256::AffinePoint::from_encoded_point(&point).unwrap();
216 let uncompressed = affine.to_encoded_point(false);
217 (
218 uncompressed.x().unwrap().to_vec(),
219 uncompressed.y().unwrap().to_vec(),
220 )
221 }
222 }
223}