webtoken 0.6.3__tar.gz → 0.6.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. {webtoken-0.6.3 → webtoken-0.6.4}/Cargo.toml +1 -0
  2. {webtoken-0.6.3 → webtoken-0.6.4}/PKG-INFO +1 -1
  3. {webtoken-0.6.3 → webtoken-0.6.4}/pyproject.toml +1 -1
  4. {webtoken-0.6.3 → webtoken-0.6.4}/src/crypto_parsing.rs +217 -29
  5. {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_crypto_parsing.py +79 -1
  6. {webtoken-0.6.3 → webtoken-0.6.4}/.github/workflows/release.yml +0 -0
  7. {webtoken-0.6.3 → webtoken-0.6.4}/.github/workflows/tests.yml +0 -0
  8. {webtoken-0.6.3 → webtoken-0.6.4}/.gitignore +0 -0
  9. {webtoken-0.6.3 → webtoken-0.6.4}/Cargo.lock +0 -0
  10. {webtoken-0.6.3 → webtoken-0.6.4}/License +0 -0
  11. {webtoken-0.6.3 → webtoken-0.6.4}/README.md +0 -0
  12. {webtoken-0.6.3 → webtoken-0.6.4}/benchmarks/Readme.md +0 -0
  13. {webtoken-0.6.3 → webtoken-0.6.4}/benchmarks/benchmarks.py +0 -0
  14. {webtoken-0.6.3 → webtoken-0.6.4}/benchmarks/mem_benchmark.py +0 -0
  15. {webtoken-0.6.3 → webtoken-0.6.4}/src/algorithms.rs +0 -0
  16. {webtoken-0.6.3 → webtoken-0.6.4}/src/crypto.rs +0 -0
  17. {webtoken-0.6.3 → webtoken-0.6.4}/src/jwe.rs +0 -0
  18. {webtoken-0.6.3 → webtoken-0.6.4}/src/jwk.rs +0 -0
  19. {webtoken-0.6.3 → webtoken-0.6.4}/src/jws.rs +0 -0
  20. {webtoken-0.6.3 → webtoken-0.6.4}/src/jwt.rs +0 -0
  21. {webtoken-0.6.3 → webtoken-0.6.4}/src/key_utils.rs +0 -0
  22. {webtoken-0.6.3 → webtoken-0.6.4}/src/lib.rs +0 -0
  23. {webtoken-0.6.3 → webtoken-0.6.4}/src/paseto.rs +0 -0
  24. {webtoken-0.6.3 → webtoken-0.6.4}/src/py_utils.rs +0 -0
  25. {webtoken-0.6.3 → webtoken-0.6.4}/src/pyjwt_jwk_api.rs +0 -0
  26. {webtoken-0.6.3 → webtoken-0.6.4}/tests/Readme.md +0 -0
  27. {webtoken-0.6.3 → webtoken-0.6.4}/tests/keys_and_vectors.py +0 -0
  28. {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_advisory.py +0 -0
  29. {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_algorithms.py +0 -0
  30. {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_api_jwk.py +0 -0
  31. {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_api_jws.py +0 -0
  32. {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_api_jwt.py +0 -0
  33. {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_compressed.py +0 -0
  34. {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_exceptions.py +0 -0
  35. {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_jwe.py +0 -0
  36. {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_jwe_chacha20.py +0 -0
  37. {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_jwe_compact.py +0 -0
  38. {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_jwt.py +0 -0
  39. {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_paseto.py +0 -0
  40. {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_paseto_key.py +0 -0
  41. {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_paseto_sample.py +0 -0
  42. {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_paseto_token.py +0 -0
  43. {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_paseto_v4.py +0 -0
  44. {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_paseto_vectors.py +0 -0
  45. {webtoken-0.6.3 → webtoken-0.6.4}/tests/tests.py +0 -0
  46. {webtoken-0.6.3 → webtoken-0.6.4}/webtoken/__init__.py +0 -0
  47. {webtoken-0.6.3 → webtoken-0.6.4}/webtoken/webtoken.py +0 -0
@@ -20,6 +20,7 @@ argon2 = "0.5.3"
20
20
  flate2 = "1.1.9"
21
21
  subtle = "2.6"
22
22
 
23
+
23
24
  base64 = "0.22"
24
25
  serde = { version = "1.0", features = ["derive"] }
25
26
  serde_json = { version = "1.0", features = ["preserve_order"] }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webtoken
3
- Version: 0.6.3
3
+ Version: 0.6.4
4
4
  Summary: A Rust-backed JWT library
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "webtoken"
7
- version = "0.6.3"
7
+ version = "0.6.4"
8
8
  description = "A Rust-backed JWT library"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -1,10 +1,10 @@
1
1
  use base64::{engine::general_purpose::STANDARD, Engine as _};
2
+ use num_bigint::BigUint;
2
3
 
3
- use pyo3::prelude::{Bound, PyModule, wrap_pyfunction, PyModuleMethods};
4
- use pyo3::{pyfunction, PyResult, exceptions::PyValueError};
4
+ use pyo3::prelude::{Bound, PyModule, wrap_pyfunction, PyModuleMethods, Python, PyDictMethods};
5
+ use pyo3::{pyfunction, PyResult, exceptions::PyValueError, types::PyDict};
5
6
 
6
-
7
- use crate::{WebtokenError, };
7
+ use crate::WebtokenError;
8
8
 
9
9
  pub const OID_EC_PUBLIC_KEY: &[u8] = &[0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01];
10
10
  pub const OID_P192: &[u8] = &[0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x01];
@@ -13,7 +13,13 @@ pub const OID_P384: &[u8] = &[0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x22];
13
13
  pub const OID_P521: &[u8] = &[0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x23];
14
14
  pub const OID_SECP256K1: &[u8] = &[0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x0A];
15
15
  pub const OID_RSA_ENCRYPTION: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01];
16
+
16
17
  pub const OID_COMMON_NAME: &[u8] = &[0x55, 0x04, 0x03]; // 2.5.4.3
18
+ pub const OID_COUNTRY: &[u8] = &[0x55, 0x04, 0x06];
19
+ pub const OID_ORG: &[u8] = &[0x55, 0x04, 0x0a];
20
+ pub const OID_ORG_UNIT: &[u8] = &[0x55, 0x04, 0x0b];
21
+ pub const OID_KEY_USAGE: &[u8] = &[0x55, 0x1d, 0x0f]; // 2.5.29.15
22
+ pub const OID_AUTHORITY_KEY_IDENTIFIER: &[u8] = &[0x55, 0x1d, 0x23]; // 2.5.29.35
17
23
 
18
24
 
19
25
  // ============================================================================
@@ -247,13 +253,41 @@ impl<'a> DerReader<'a> {
247
253
  Ok(Some(DerReader::new(content)))
248
254
  } else { Ok(None) }
249
255
  }
250
- }
251
256
 
257
+ /// Returns the exact raw bytes of the current Tag-Length-Value object without parsing inside it
258
+ pub fn read_tlv(&mut self) -> Result<&'a [u8], String> {
259
+ let original_input = self.input;
260
+ let _ = self.read_tag()?; // Advance past it
261
+ let consumed = original_input.len() - self.input.len();
262
+ Ok(&original_input[..consumed])
263
+ }
264
+
265
+ /// Reads an IMPLICIT tag (e.g., Context Specific [0] -> 0x80)
266
+ pub fn read_optional_implicit(&mut self, tag_id: u8) -> Result<Option<&'a [u8]>, String> {
267
+ if !self.input.is_empty() && self.input[0] == (0x80 | tag_id) {
268
+ let (_, content) = self.read_tag()?;
269
+ Ok(Some(content))
270
+ } else { Ok(None) }
271
+ }
272
+ }
252
273
 
253
274
  // ============================================================================
254
275
  // X.509 Certificate Parsing
255
276
  // ============================================================================
256
277
 
278
+ fn decode_directory_string(tag: u8, content: &[u8]) -> Result<String, String> {
279
+
280
+ if tag == 0x1E { // BMPString (UTF-16 BE)
281
+ if content.len() % 2 != 0 { return Err("Invalid BMPString length".into()); }
282
+ let utf16_data: Vec<u16> = content.chunks_exact(2).map(|c| u16::from_be_bytes([c[0], c[1]])).collect();
283
+ String::from_utf16(&utf16_data).map_err(|_| "Invalid UTF-16 in string".into())
284
+ } else {
285
+ // UTF8String (0x0C), PrintableString (0x13), TeletexString (0x14)
286
+ String::from_utf8(content.to_vec()).map_err(|_| "Invalid UTF-8 in string".into())
287
+ }
288
+ }
289
+
290
+
257
291
  pub fn get_x509_subject_cn(der: &[u8]) -> Result<String, String> {
258
292
 
259
293
  let mut cert = DerReader::new(der).read_sequence()?;
@@ -288,33 +322,186 @@ pub fn get_x509_subject_cn(der: &[u8]) -> Result<String, String> {
288
322
  if let Ok(oid) = attr_seq.read_oid() {
289
323
  // If the OID matches the Common Name (CN) OID
290
324
  if oid == OID_COMMON_NAME {
291
- // Extract the string tag and the raw bytes
292
325
  let (str_tag, string_content) = attr_seq.read_tag()?;
293
-
294
- // 0x1E is BMPString (UTF-16 Big Endian) - Common in Microsoft AD CS / Smart Cards
295
- if str_tag == 0x1E {
296
- if string_content.len() % 2 != 0 {
297
- return Err("Invalid BMPString length".into());
326
+ return decode_directory_string(str_tag, string_content);
327
+ }
328
+ }
329
+ }
330
+ }
331
+
332
+ Err("Common Name (CN) not found in certificate".into())
333
+ }
334
+
335
+
336
+ fn parse_asn1_time(tag: u8, content: &[u8]) -> Result<u64, String> {
337
+
338
+ let s = std::str::from_utf8(content).map_err(|_| "Invalid time string")?;
339
+ let (y, mo, d, h, m, sec) = if tag == 0x17 { // UTCTime
340
+ let y2 = s[0..2].parse::<u64>().unwrap_or(0);
341
+ let y = if y2 < 50 { 2000 + y2 } else { 1900 + y2 };
342
+ (y, s[2..4].parse().unwrap_or(1), s[4..6].parse().unwrap_or(1), s[6..8].parse().unwrap_or(0), s[8..10].parse().unwrap_or(0), s[10..12].parse().unwrap_or(0))
343
+ } else if tag == 0x18 { // GeneralizedTime
344
+ (s[0..4].parse().unwrap_or(1970), s[4..6].parse().unwrap_or(1), s[6..8].parse().unwrap_or(1), s[8..10].parse().unwrap_or(0), s[10..12].parse().unwrap_or(0), s[12..14].parse().unwrap_or(0))
345
+ } else {
346
+ return Err("Unknown time tag".into());
347
+ };
348
+
349
+ // Quick Unix Timestamp Calculation (Valid from 1970 to 2100)
350
+ let mut days = 0;
351
+ for year in 1970..y { days += if (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) { 366 } else { 365 }; }
352
+ let days_in_month = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
353
+ for month in 1..mo {
354
+ let mut dim = days_in_month[month as usize];
355
+ if month == 2 && ((y % 4 == 0 && y % 100 != 0) || (y % 400 == 0)) { dim = 29; }
356
+ days += dim;
357
+ }
358
+ days += d - 1;
359
+ Ok(days * 86400 + h * 3600 + m * 60 + sec)
360
+ }
361
+
362
+
363
+ fn parse_x509_name(content: &[u8]) -> String {
364
+ let mut parts = Vec::new();
365
+ let mut outer = DerReader::new(content);
366
+
367
+ // Unpack the outer SEQUENCE envelope first
368
+ if let Ok(mut seq) = outer.read_sequence() {
369
+ while !seq.input.is_empty() {
370
+ if let Ok((tag, set_data)) = seq.read_tag() {
371
+ if tag == 0x31 { // Ensure it's a SET
372
+ let mut set_r = DerReader::new(set_data);
373
+ while !set_r.input.is_empty() {
374
+ if let Ok(mut attr) = set_r.read_sequence() {
375
+ if let Ok(oid) = attr.read_oid() {
376
+ let label = match oid {
377
+ OID_COMMON_NAME => "CN",
378
+ OID_COUNTRY => "C",
379
+ OID_ORG => "O",
380
+ OID_ORG_UNIT => "OU",
381
+ _ => "Unknown"
382
+ };
383
+ if let Ok((str_tag, val)) = attr.read_tag() {
384
+ if let Ok(s) = decode_directory_string(str_tag, val) {
385
+ parts.push(format!("{}={}", label, s));
386
+ }
387
+ }
388
+ }
298
389
  }
299
- let utf16_data: Vec<u16> = string_content
300
- .chunks_exact(2)
301
- .map(|c| u16::from_be_bytes([c[0], c[1]]))
302
- .collect();
303
-
304
- return String::from_utf16(&utf16_data)
305
- .map_err(|_| "Invalid UTF-16 in Common Name".into());
306
- }
307
- // 0x0C is UTF8String, 0x13 is PrintableString, 0x14 is TeletexString
308
- else {
309
- return String::from_utf8(string_content.to_vec())
310
- .map_err(|_| "Invalid UTF-8 in Common Name".into());
311
390
  }
312
391
  }
313
392
  }
314
393
  }
315
394
  }
395
+ // Reverse to match standard RFC4514 representation (from most specific to least)
396
+ parts.reverse();
397
+ parts.join(",")
398
+ }
399
+
400
+
401
+ #[pyfunction]
402
+ pub fn get_x509_public_key(der_bytes: &[u8]) -> PyResult<Vec<u8>> {
403
+
404
+ let mut cert = DerReader::new(der_bytes).read_sequence().map_err(PyValueError::new_err)?;
405
+ let mut tbs = cert.read_sequence().map_err(PyValueError::new_err)?;
406
+
407
+ let _ = tbs.read_optional_explicit(0); // Version
408
+ let _ = tbs.read_integer_bytes(); // Serial
409
+ let _ = tbs.read_sequence(); // Sig Alg
410
+ let _ = tbs.read_sequence(); // Issuer
411
+ let _ = tbs.read_sequence(); // Validity
412
+ let _ = tbs.read_sequence(); // Subject
316
413
 
317
- Err("Common Name (CN) not found in certificate".into())
414
+ // The next object is the SubjectPublicKeyInfo. We extract the raw DER bytes natively!
415
+ let spki_der = tbs.read_tlv().map_err(PyValueError::new_err)?;
416
+ Ok(spki_der.to_vec())
417
+ }
418
+
419
+ #[pyfunction]
420
+ pub fn get_x509_metadata<'py>(py: Python<'py>, der_bytes: &[u8]) -> PyResult<Bound<'py, PyDict>> {
421
+
422
+ let mut cert = DerReader::new(der_bytes).read_sequence().map_err(PyValueError::new_err)?;
423
+ let mut tbs = cert.read_sequence().map_err(PyValueError::new_err)?;
424
+ let dict = PyDict::new(py);
425
+
426
+ let _ = tbs.read_optional_explicit(0); // Version
427
+
428
+ // 1. Serial Number (Parsed to BigInt Base-10 string just like cryptography does)
429
+ if let Ok(serial_bytes) = tbs.read_integer_bytes() {
430
+ let big_serial = BigUint::from_bytes_be(serial_bytes);
431
+ dict.set_item("serial", big_serial.to_string())?;
432
+ }
433
+
434
+ let _ = tbs.read_sequence(); // Sig Alg
435
+
436
+ // 2. Issuer Name
437
+ if let Ok(issuer_der) = tbs.read_tlv() {
438
+ dict.set_item("issuer", parse_x509_name(issuer_der))?;
439
+ }
440
+
441
+ // 3. Validity (Unix Timestamps)
442
+ if let Ok(mut validity) = tbs.read_sequence() {
443
+ if let Ok((t1, c1)) = validity.read_tag() {
444
+ if let Ok(ts) = parse_asn1_time(t1, c1) { dict.set_item("not_before", ts)?; }
445
+ }
446
+ if let Ok((t2, c2)) = validity.read_tag() {
447
+ if let Ok(ts) = parse_asn1_time(t2, c2) { dict.set_item("not_after", ts)?; }
448
+ }
449
+ }
450
+
451
+ let _ = tbs.read_sequence(); // Subject
452
+ let _ = tbs.read_tag(); // SPKI
453
+ let _ = tbs.read_optional_implicit(1); // Issuer Unique ID
454
+ let _ = tbs.read_optional_implicit(2); // Subject Unique ID
455
+
456
+ // 4. Extensions (Extract AKI & Key Usages)
457
+ let mut usages = Vec::new();
458
+ if let Ok(Some(mut ext_wrapper)) = tbs.read_optional_explicit(3) {
459
+ if let Ok(mut ext_seq) = ext_wrapper.read_sequence() {
460
+ while !ext_seq.input.is_empty() {
461
+ if let Ok(mut ext) = ext_seq.read_sequence() {
462
+ let oid = ext.read_oid().unwrap_or(&[]);
463
+ let (mut tag, mut content) = ext.read_tag().unwrap();
464
+
465
+ if tag == 0x01 { // Skip boolean CRITICAL flag
466
+ let next = ext.read_tag().unwrap();
467
+ tag = next.0; content = next.1;
468
+ }
469
+
470
+ if tag == 0x04 { // OCTET STRING containing the extension data
471
+ if oid == OID_KEY_USAGE {
472
+ if let Ok(bits) = DerReader::new(content).read_bit_string() {
473
+ if !bits.is_empty() {
474
+ let b = bits[0];
475
+ if b & (1 << 7) != 0 { usages.push("digitalSignature"); }
476
+ if b & (1 << 6) != 0 { usages.push("nonRepudiation"); }
477
+ if b & (1 << 5) != 0 { usages.push("keyEncipherment"); }
478
+ if b & (1 << 4) != 0 { usages.push("dataEncipherment"); }
479
+ if b & (1 << 3) != 0 { usages.push("keyAgreement"); }
480
+ if b & (1 << 2) != 0 { usages.push("keyCertSign"); }
481
+ if b & (1 << 1) != 0 { usages.push("cRLSign"); }
482
+ if b & (1 << 0) != 0 { usages.push("encipherOnly"); }
483
+ if bits.len() > 1 && bits[1] & (1 << 7) != 0 { usages.push("decipherOnly"); }
484
+ }
485
+ }
486
+ } else if oid == OID_AUTHORITY_KEY_IDENTIFIER {
487
+ let mut inner = DerReader::new(content);
488
+ if let Ok(mut aki_seq) = inner.read_sequence() {
489
+ // Extract the implicit [0] KeyIdentifier
490
+ if let Ok(Some(kid)) = aki_seq.read_optional_implicit(0) {
491
+ let mut hex_str = String::with_capacity(kid.len() * 2);
492
+ for byte in kid { hex_str.push_str(&format!("{:02X}", byte)); }
493
+ dict.set_item("aki", hex_str)?;
494
+ }
495
+ }
496
+ }
497
+ }
498
+ }
499
+ }
500
+ }
501
+ }
502
+ dict.set_item("key_usages", usages)?;
503
+
504
+ Ok(dict)
318
505
  }
319
506
 
320
507
 
@@ -347,10 +534,11 @@ pub fn get_x509_subject_py(der_bytes: &[u8]) -> PyResult<String> {
347
534
 
348
535
 
349
536
  pub fn export_functions(m: &Bound<'_, PyModule>) -> PyResult<()> {
350
- m.add_function(wrap_pyfunction!(extract_ed25519_private_key_py, m)?)?;
351
- m.add_function(wrap_pyfunction!(extract_ed25519_public_key_py, m)?)?;
352
- m.add_function(wrap_pyfunction!(get_x509_subject_py, m)?)?;
537
+ m.add_function(wrap_pyfunction!(extract_ed25519_private_key_py, m)?)?;
538
+ m.add_function(wrap_pyfunction!(extract_ed25519_public_key_py, m)?)?;
539
+ m.add_function(wrap_pyfunction!(get_x509_subject_py, m)?)?;
540
+ m.add_function(wrap_pyfunction!(get_x509_public_key, m)?)?;
541
+ m.add_function(wrap_pyfunction!(get_x509_metadata, m)?)?;
353
542
 
354
543
  Ok(())
355
- }
356
-
544
+ }
@@ -214,4 +214,82 @@ class TestCryptoParsing:
214
214
  der_bytes = base64.b64decode(real_cert_b64)
215
215
  subject = webtoken.get_x509_subject(der_bytes)
216
216
 
217
- assert subject == 'Testing'
217
+ assert subject == 'Testing'
218
+
219
+
220
+ def test_x509_metadata_extraction(self):
221
+ '''
222
+ PROVES: The native Rust metadata extractor perfectly pulls out
223
+ the serial number, issuer, timestamps, AKI, and Key Usages from a real cert
224
+ and hands them to Python as a standard dictionary.
225
+ '''
226
+
227
+ real_cert_b64 = (
228
+ 'MIICnTCCAYUCBgGAUN03JTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdUZXN0aW5nMB4X'
229
+ 'DTIyMDQyMjEwNDAxNloXDTMyMDQyMjEwNDE1NlowEjEQMA4GA1UEAwwHVGVzdGluZzCCASIw'
230
+ 'DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKJnn9zZF3+PvugbVDyo4ZVe6X+lb+xzIPlS'
231
+ '/iE1/CkGUw+C081jt8fUT8FXqSo4H7yXvyImRWiV+/Pmu86XBvZqWRvHM6dwvJ2UrwCSqYb2'
232
+ 'C3fbPamKxjBVvbdXh8hsJiEDdNlV8B3mdCQ3eV+Iu7DuFz5DcnH80qMWkG7+8ADWAU3L3FnI'
233
+ '2FcSI+GaWJErEKq6zk5uvRuxcrq7XxMRnO45UkXL/hrm6vytyECxxh05YpdtMKmZorNXSycK'
234
+ 'QI4E8WO7kEsBHaiRwiUd6u+m7A3pSAWaW0dO5KiDl6mLudsNMJAv9Vu/x3FTyzaek/zC9PT/'
235
+ 'IxrDlnzDvef83IZLHkMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAi7ZppYbkpt0ALn5NXIIP'
236
+ 'gA04svRwAmsUJWKLBS5iKVXq6HOJPsz0GAB9oKpjar83rUomwK2UE0XFJLMDvrB0nTZJBjm2'
237
+ 'DCANLL1GtTKUd+mdvhyHCIMrUApkhAYzv2Rk1c4+Jt7f5/h8FnM8jdl9FGc5TBy5ixS0Oxny'
238
+ 'W1JOakClYQz8vNS7LrC4hmLWwy7GAmUdemNLEefQcECaNzaLN5gGk1ht5lJyNCsHu9STZeYM'
239
+ '2UXdDAtMtu9HAepfzh2CAOscSDtZr89SmFSwxKaOfbJyXH4PivMgWK4zO0P6ofuv8d8gRbUA'
240
+ 'UgnysKHQc0isTVWOxgmzI69EUe/iVXJHig=='
241
+ )
242
+ der_bytes = base64.b64decode(real_cert_b64)
243
+
244
+ # Execute
245
+ meta = webtoken.get_x509_metadata(der_bytes)
246
+
247
+ # Verify structure and PyO3 type bindings
248
+ assert isinstance(meta, dict)
249
+ assert isinstance(meta.get('serial'), str)
250
+ assert isinstance(meta.get('issuer'), str)
251
+ assert isinstance(meta.get('not_before'), int)
252
+ assert isinstance(meta.get('not_after'), int)
253
+ assert isinstance(meta.get('key_usages'), list)
254
+
255
+ # Verify data accuracy (This specific test cert is valid for 10 years)
256
+ assert meta['issuer'] == 'CN=Testing'
257
+ assert (meta['not_after'] - meta['not_before']) == 315619300 # 3653 days * 86400 seconds + 100 seconds
258
+
259
+
260
+ def test_x509_public_key_spki_extraction(self):
261
+ '''
262
+ PROVES: The parser successfully isolates the SubjectPublicKeyInfo (SPKI)
263
+ block and returns it as raw ASN.1 DER bytes so `webtoken.verify` can
264
+ natively digest it.
265
+ '''
266
+
267
+ real_cert_b64 = (
268
+ 'MIICnTCCAYUCBgGAUN03JTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdUZXN0aW5nMB4X'
269
+ 'DTIyMDQyMjEwNDAxNloXDTMyMDQyMjEwNDE1NlowEjEQMA4GA1UEAwwHVGVzdGluZzCCASIw'
270
+ 'DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKJnn9zZF3+PvugbVDyo4ZVe6X+lb+xzIPlS'
271
+ '/iE1/CkGUw+C081jt8fUT8FXqSo4H7yXvyImRWiV+/Pmu86XBvZqWRvHM6dwvJ2UrwCSqYb2'
272
+ 'C3fbPamKxjBVvbdXh8hsJiEDdNlV8B3mdCQ3eV+Iu7DuFz5DcnH80qMWkG7+8ADWAU3L3FnI'
273
+ '2FcSI+GaWJErEKq6zk5uvRuxcrq7XxMRnO45UkXL/hrm6vytyECxxh05YpdtMKmZorNXSycK'
274
+ 'QI4E8WO7kEsBHaiRwiUd6u+m7A3pSAWaW0dO5KiDl6mLudsNMJAv9Vu/x3FTyzaek/zC9PT/'
275
+ 'IxrDlnzDvef83IZLHkMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAi7ZppYbkpt0ALn5NXIIP'
276
+ 'gA04svRwAmsUJWKLBS5iKVXq6HOJPsz0GAB9oKpjar83rUomwK2UE0XFJLMDvrB0nTZJBjm2'
277
+ 'DCANLL1GtTKUd+mdvhyHCIMrUApkhAYzv2Rk1c4+Jt7f5/h8FnM8jdl9FGc5TBy5ixS0Oxny'
278
+ 'W1JOakClYQz8vNS7LrC4hmLWwy7GAmUdemNLEefQcECaNzaLN5gGk1ht5lJyNCsHu9STZeYM'
279
+ '2UXdDAtMtu9HAepfzh2CAOscSDtZr89SmFSwxKaOfbJyXH4PivMgWK4zO0P6ofuv8d8gRbUA'
280
+ 'UgnysKHQc0isTVWOxgmzI69EUe/iVXJHig=='
281
+ )
282
+ der_bytes = base64.b64decode(real_cert_b64)
283
+
284
+ # Execute
285
+ spki_bytes = webtoken.get_x509_public_key(der_bytes)
286
+
287
+ # Verify
288
+ assert isinstance(spki_bytes, bytes)
289
+ assert len(spki_bytes) > 0
290
+
291
+ # SPKI is an ASN.1 SEQUENCE, so it MUST mathematically start with 0x30
292
+ assert spki_bytes[0] == 0x30
293
+
294
+ # The SPKI block for an RSA 2048-bit key is exactly 294 bytes
295
+ assert len(spki_bytes) == 294
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes