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.
- {webtoken-0.6.3 → webtoken-0.6.4}/Cargo.toml +1 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/PKG-INFO +1 -1
- {webtoken-0.6.3 → webtoken-0.6.4}/pyproject.toml +1 -1
- {webtoken-0.6.3 → webtoken-0.6.4}/src/crypto_parsing.rs +217 -29
- {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_crypto_parsing.py +79 -1
- {webtoken-0.6.3 → webtoken-0.6.4}/.github/workflows/release.yml +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/.github/workflows/tests.yml +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/.gitignore +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/Cargo.lock +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/License +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/README.md +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/benchmarks/Readme.md +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/benchmarks/benchmarks.py +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/benchmarks/mem_benchmark.py +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/src/algorithms.rs +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/src/crypto.rs +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/src/jwe.rs +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/src/jwk.rs +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/src/jws.rs +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/src/jwt.rs +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/src/key_utils.rs +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/src/lib.rs +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/src/paseto.rs +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/src/py_utils.rs +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/src/pyjwt_jwk_api.rs +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/tests/Readme.md +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/tests/keys_and_vectors.py +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_advisory.py +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_algorithms.py +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_api_jwk.py +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_api_jws.py +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_api_jwt.py +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_compressed.py +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_exceptions.py +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_jwe.py +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_jwe_chacha20.py +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_jwe_compact.py +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_jwt.py +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_paseto.py +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_paseto_key.py +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_paseto_sample.py +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_paseto_token.py +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_paseto_v4.py +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/tests/test_paseto_vectors.py +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/tests/tests.py +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/webtoken/__init__.py +0 -0
- {webtoken-0.6.3 → webtoken-0.6.4}/webtoken/webtoken.py +0 -0
|
@@ -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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
|
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
|