1use a3s_box_core::error::{BoxError, Result};
8#[cfg(test)]
11use base64::Engine;
12use der::Decode;
13use oci_distribution::client::ClientConfig;
14use oci_distribution::errors::{OciDistributionError, OciErrorCode};
15use oci_distribution::secrets::RegistryAuth;
16use oci_distribution::{Client, Reference};
17use serde::{Deserialize, Serialize};
18
19mod crypto;
20mod sign;
21
22use crypto::*;
23#[cfg(test)]
24use sign::{extract_pem_content, parse_pem_private_key};
25pub use sign::{sign_image, SignResult};
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
29pub enum SignaturePolicy {
30 #[default]
32 Skip,
33 CosignKey {
35 public_key: String,
37 },
38 CosignKeyless {
40 issuer: String,
42 identity: String,
44 },
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum VerifyResult {
50 Verified,
52 Skipped,
54 NoSignature,
56 Failed(String),
58}
59
60impl VerifyResult {
61 pub fn is_ok(&self) -> bool {
63 matches!(self, Self::Verified | Self::Skipped)
64 }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub(super) struct CosignPayload {
70 pub(super) critical: CosignCritical,
72 #[serde(default)]
74 pub(super) optional: serde_json::Value,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub(super) struct CosignCritical {
80 pub(super) identity: CosignIdentity,
82 pub(super) image: CosignImage,
84 #[serde(rename = "type")]
86 pub(super) sig_type: String,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub(super) struct CosignIdentity {
92 #[serde(rename = "docker-reference")]
94 pub(super) docker_reference: String,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99pub(super) struct CosignImage {
100 #[serde(rename = "docker-manifest-digest")]
102 pub(super) docker_manifest_digest: String,
103}
104
105mod annotations {
107 pub const CERTIFICATE: &str = "dev.sigstore.cosign/certificate";
108 #[allow(dead_code)]
110 pub const CHAIN: &str = "dev.sigstore.cosign/chain";
111 #[allow(dead_code)]
113 pub const BUNDLE: &str = "dev.sigstore.cosign/bundle";
114}
115
116fn cosign_signature_tag(manifest_digest: &str) -> String {
118 let hex = manifest_digest
119 .strip_prefix("sha256:")
120 .unwrap_or(manifest_digest);
121 format!("sha256-{}.sig", hex)
122}
123
124struct CosignSignatureData {
126 layer_data: Vec<u8>,
128 annotations: std::collections::HashMap<String, String>,
130}
131
132async fn fetch_cosign_signature(
136 registry: &str,
137 repository: &str,
138 manifest_digest: &str,
139) -> Result<Option<CosignSignatureData>> {
140 let sig_tag = cosign_signature_tag(manifest_digest);
141 let reference_str = format!("{}/{}:{}", registry, repository, sig_tag);
142
143 let reference: Reference = reference_str.parse().map_err(|e| BoxError::RegistryError {
144 registry: registry.to_string(),
145 message: format!("Invalid signature reference: {}", e),
146 })?;
147
148 let config = ClientConfig {
149 protocol: oci_distribution::client::ClientProtocol::Https,
150 ..Default::default()
151 };
152 let client = Client::new(config);
153
154 match client
156 .pull_image_manifest(&reference, &RegistryAuth::Anonymous)
157 .await
158 {
159 Ok((manifest, _digest)) => {
160 let mut all_annotations = std::collections::HashMap::new();
162
163 if let Some(ref anns) = manifest.annotations {
165 all_annotations.extend(anns.clone());
166 }
167 for layer in &manifest.layers {
168 if let Some(ref anns) = layer.annotations {
169 all_annotations.extend(anns.clone());
170 }
171 }
172
173 if let Some(layer) = manifest.layers.first() {
175 let mut buf = Vec::new();
176 match client.pull_blob(&reference, layer, &mut buf).await {
177 Ok(()) => Ok(Some(CosignSignatureData {
178 layer_data: buf,
179 annotations: all_annotations,
180 })),
181 Err(e) => {
182 tracing::warn!(
183 reference = %reference_str,
184 error = %e,
185 "Failed to pull cosign signature blob"
186 );
187 Ok(None)
188 }
189 }
190 } else {
191 Ok(None)
192 }
193 }
194 Err(e) => {
195 let is_not_found = matches!(e, OciDistributionError::ImageManifestNotFoundError(_))
197 || matches!(&e, OciDistributionError::RegistryError { envelope, .. }
198 if envelope.errors.iter().any(|oe| oe.code == OciErrorCode::ManifestUnknown));
199 if is_not_found {
200 Ok(None)
201 } else {
202 tracing::warn!(
203 reference = %reference_str,
204 error = %e,
205 "Registry error while fetching cosign signature"
206 );
207 Err(BoxError::RegistryError {
208 registry: registry.to_string(),
209 message: format!("Failed to fetch cosign signature: {}", e),
210 })
211 }
212 }
213 }
214}
215
216fn verify_cosign_payload(payload: &[u8], manifest_digest: &str) -> Result<CosignPayload> {
221 let cosign_payload: CosignPayload =
223 serde_json::from_slice(payload).map_err(|e| BoxError::RegistryError {
224 registry: String::new(),
225 message: format!("Invalid cosign payload: {}", e),
226 })?;
227
228 if cosign_payload.critical.image.docker_manifest_digest != manifest_digest {
230 return Err(BoxError::RegistryError {
231 registry: String::new(),
232 message: format!(
233 "Signature digest mismatch: expected {}, got {}",
234 manifest_digest, cosign_payload.critical.image.docker_manifest_digest
235 ),
236 });
237 }
238
239 Ok(cosign_payload)
240}
241
242pub async fn verify_image_signature(
244 policy: &SignaturePolicy,
245 registry: &str,
246 repository: &str,
247 manifest_digest: &str,
248) -> VerifyResult {
249 match policy {
250 SignaturePolicy::Skip => VerifyResult::Skipped,
251
252 SignaturePolicy::CosignKey { public_key } => {
253 verify_cosign_key(public_key, registry, repository, manifest_digest).await
254 }
255
256 SignaturePolicy::CosignKeyless { issuer, identity } => {
257 verify_cosign_keyless(issuer, identity, registry, repository, manifest_digest).await
258 }
259 }
260}
261
262async fn verify_cosign_key(
271 public_key_path: &str,
272 registry: &str,
273 repository: &str,
274 manifest_digest: &str,
275) -> VerifyResult {
276 let pem_bytes = match std::fs::read(public_key_path) {
278 Ok(b) => b,
279 Err(e) => {
280 return VerifyResult::Failed(format!(
281 "Failed to read public key file '{}': {}",
282 public_key_path, e
283 ));
284 }
285 };
286
287 let verifying_key = match parse_pem_public_key(&pem_bytes) {
288 Ok(k) => k,
289 Err(e) => {
290 return VerifyResult::Failed(format!("Failed to parse public key: {}", e));
291 }
292 };
293
294 let sig_data = match fetch_cosign_signature(registry, repository, manifest_digest).await {
296 Ok(Some(data)) => data,
297 Ok(None) => return VerifyResult::NoSignature,
298 Err(e) => {
299 return VerifyResult::Failed(format!("Failed to fetch signature: {}", e));
300 }
301 };
302
303 let sig_envelope: CosignSignatureEnvelope = match serde_json::from_slice(&sig_data.layer_data) {
305 Ok(e) => e,
306 Err(e) => {
307 return VerifyResult::Failed(format!(
308 "Failed to parse cosign signature envelope: {}",
309 e
310 ));
311 }
312 };
313
314 let payload_bytes = match base64_decode(&sig_envelope.payload) {
315 Ok(b) => b,
316 Err(e) => {
317 return VerifyResult::Failed(format!("Failed to decode signature payload: {}", e));
318 }
319 };
320
321 let signature_bytes = match base64_decode(&sig_envelope.signature) {
322 Ok(b) => b,
323 Err(e) => {
324 return VerifyResult::Failed(format!("Failed to decode signature bytes: {}", e));
325 }
326 };
327
328 if let Err(e) = verify_ecdsa_p256(&verifying_key, &payload_bytes, &signature_bytes) {
330 return VerifyResult::Failed(format!("Signature verification failed: {}", e));
331 }
332
333 match verify_cosign_payload(&payload_bytes, manifest_digest) {
335 Ok(_) => VerifyResult::Verified,
336 Err(e) => VerifyResult::Failed(format!("Payload validation failed: {}", e)),
337 }
338}
339
340async fn verify_cosign_keyless(
350 expected_issuer: &str,
351 expected_identity: &str,
352 registry: &str,
353 repository: &str,
354 manifest_digest: &str,
355) -> VerifyResult {
356 let sig_data = match fetch_cosign_signature(registry, repository, manifest_digest).await {
358 Ok(Some(data)) => data,
359 Ok(None) => return VerifyResult::NoSignature,
360 Err(e) => {
361 return VerifyResult::Failed(format!("Failed to fetch signature: {}", e));
362 }
363 };
364
365 let cert_pem = match sig_data.annotations.get(annotations::CERTIFICATE) {
367 Some(c) => c.clone(),
368 None => {
369 return VerifyResult::Failed(
370 "Keyless signature missing Fulcio certificate annotation \
371 (dev.sigstore.cosign/certificate)"
372 .to_string(),
373 );
374 }
375 };
376
377 let cert_der = match pem_to_der(&cert_pem) {
379 Ok(d) => d,
380 Err(e) => {
381 return VerifyResult::Failed(format!("Failed to parse Fulcio certificate PEM: {}", e));
382 }
383 };
384
385 let cert = match x509_cert::Certificate::from_der(&cert_der) {
386 Ok(c) => c,
387 Err(e) => {
388 return VerifyResult::Failed(format!("Failed to parse Fulcio certificate DER: {}", e));
389 }
390 };
391
392 if let Err(e) = verify_fulcio_issuer(&cert, expected_issuer) {
394 return VerifyResult::Failed(format!("Fulcio issuer mismatch: {}", e));
395 }
396
397 if let Err(e) = verify_fulcio_identity(&cert, expected_identity) {
399 return VerifyResult::Failed(format!("Fulcio identity mismatch: {}", e));
400 }
401
402 let pub_key_bytes = match extract_cert_public_key(&cert) {
404 Ok(k) => k,
405 Err(e) => {
406 return VerifyResult::Failed(format!(
407 "Failed to extract public key from Fulcio cert: {}",
408 e
409 ));
410 }
411 };
412
413 let sig_envelope: CosignSignatureEnvelope = match serde_json::from_slice(&sig_data.layer_data) {
415 Ok(e) => e,
416 Err(e) => {
417 return VerifyResult::Failed(format!(
418 "Failed to parse cosign signature envelope: {}",
419 e
420 ));
421 }
422 };
423
424 let payload_bytes = match base64_decode(&sig_envelope.payload) {
425 Ok(b) => b,
426 Err(e) => {
427 return VerifyResult::Failed(format!("Failed to decode signature payload: {}", e));
428 }
429 };
430
431 let signature_bytes = match base64_decode(&sig_envelope.signature) {
432 Ok(b) => b,
433 Err(e) => {
434 return VerifyResult::Failed(format!("Failed to decode signature bytes: {}", e));
435 }
436 };
437
438 if let Err(e) = verify_ecdsa_p256(&pub_key_bytes, &payload_bytes, &signature_bytes) {
440 return VerifyResult::Failed(format!("Keyless signature verification failed: {}", e));
441 }
442
443 match verify_cosign_payload(&payload_bytes, manifest_digest) {
445 Ok(_) => {
446 tracing::info!(
447 digest = %manifest_digest,
448 issuer = %expected_issuer,
449 identity = %expected_identity,
450 "Cosign keyless signature verified"
451 );
452 VerifyResult::Verified
453 }
454 Err(e) => VerifyResult::Failed(format!("Payload validation failed: {}", e)),
455 }
456}
457
458const FULCIO_ISSUER_OID: &str = "1.3.6.1.4.1.57264.1.1";
460
461#[derive(Debug, Serialize, Deserialize)]
463struct CosignSignatureEnvelope {
464 payload: String,
466 signature: String,
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473
474 #[test]
477 fn test_signature_policy_default_is_skip() {
478 assert_eq!(SignaturePolicy::default(), SignaturePolicy::Skip);
479 }
480
481 #[test]
482 fn test_signature_policy_serde_skip() {
483 let policy = SignaturePolicy::Skip;
484 let json = serde_json::to_string(&policy).unwrap();
485 let parsed: SignaturePolicy = serde_json::from_str(&json).unwrap();
486 assert_eq!(parsed, SignaturePolicy::Skip);
487 }
488
489 #[test]
490 fn test_signature_policy_serde_cosign_key() {
491 let policy = SignaturePolicy::CosignKey {
492 public_key: "/path/to/cosign.pub".to_string(),
493 };
494 let json = serde_json::to_string(&policy).unwrap();
495 let parsed: SignaturePolicy = serde_json::from_str(&json).unwrap();
496 assert_eq!(parsed, policy);
497 }
498
499 #[test]
500 fn test_signature_policy_serde_cosign_keyless() {
501 let policy = SignaturePolicy::CosignKeyless {
502 issuer: "https://accounts.google.com".to_string(),
503 identity: "user@example.com".to_string(),
504 };
505 let json = serde_json::to_string(&policy).unwrap();
506 let parsed: SignaturePolicy = serde_json::from_str(&json).unwrap();
507 assert_eq!(parsed, policy);
508 }
509
510 #[test]
513 fn test_verify_result_is_ok() {
514 assert!(VerifyResult::Verified.is_ok());
515 assert!(VerifyResult::Skipped.is_ok());
516 assert!(!VerifyResult::NoSignature.is_ok());
517 assert!(!VerifyResult::Failed("err".to_string()).is_ok());
518 }
519
520 #[test]
521 fn test_verify_result_debug() {
522 let r = VerifyResult::Verified;
523 assert!(format!("{:?}", r).contains("Verified"));
524 }
525
526 #[test]
529 fn test_cosign_signature_tag_with_prefix() {
530 let tag = cosign_signature_tag("sha256:abc123def456");
531 assert_eq!(tag, "sha256-abc123def456.sig");
532 }
533
534 #[test]
535 fn test_cosign_signature_tag_without_prefix() {
536 let tag = cosign_signature_tag("abc123def456");
537 assert_eq!(tag, "sha256-abc123def456.sig");
538 }
539
540 #[test]
543 fn test_verify_cosign_payload_valid() {
544 let digest = "sha256:abc123";
545 let payload = serde_json::json!({
546 "critical": {
547 "identity": {
548 "docker-reference": "docker.io/library/alpine"
549 },
550 "image": {
551 "docker-manifest-digest": digest
552 },
553 "type": "cosign container image signature"
554 },
555 "optional": {}
556 });
557 let bytes = serde_json::to_vec(&payload).unwrap();
558 let result = verify_cosign_payload(&bytes, digest);
559 assert!(result.is_ok());
560 let p = result.unwrap();
561 assert_eq!(p.critical.image.docker_manifest_digest, digest);
562 assert_eq!(
563 p.critical.identity.docker_reference,
564 "docker.io/library/alpine"
565 );
566 }
567
568 #[test]
569 fn test_verify_cosign_payload_digest_mismatch() {
570 let payload = serde_json::json!({
571 "critical": {
572 "identity": {
573 "docker-reference": "docker.io/library/alpine"
574 },
575 "image": {
576 "docker-manifest-digest": "sha256:wrong"
577 },
578 "type": "cosign container image signature"
579 },
580 "optional": {}
581 });
582 let bytes = serde_json::to_vec(&payload).unwrap();
583 let result = verify_cosign_payload(&bytes, "sha256:expected");
584 assert!(result.is_err());
585 assert!(result.unwrap_err().to_string().contains("mismatch"));
586 }
587
588 #[test]
589 fn test_verify_cosign_payload_invalid_json() {
590 let result = verify_cosign_payload(b"not json", "sha256:abc");
591 assert!(result.is_err());
592 assert!(result
593 .unwrap_err()
594 .to_string()
595 .contains("Invalid cosign payload"));
596 }
597
598 #[tokio::test]
601 async fn test_verify_image_signature_skip() {
602 let result = verify_image_signature(
603 &SignaturePolicy::Skip,
604 "docker.io",
605 "library/alpine",
606 "sha256:abc",
607 )
608 .await;
609 assert_eq!(result, VerifyResult::Skipped);
610 }
611
612 #[tokio::test]
613 async fn test_verify_image_signature_cosign_key_missing_file() {
614 let policy = SignaturePolicy::CosignKey {
615 public_key: "/nonexistent/cosign.pub".to_string(),
616 };
617 let result =
618 verify_image_signature(&policy, "docker.io", "library/alpine", "sha256:abc").await;
619 match result {
620 VerifyResult::Failed(msg) => assert!(msg.contains("Failed to read public key")),
621 other => panic!("Expected Failed, got {:?}", other),
622 }
623 }
624
625 #[tokio::test]
626 #[ignore = "requires registry network access"]
627 async fn test_verify_image_signature_cosign_keyless_no_signature() {
628 let policy = SignaturePolicy::CosignKeyless {
631 issuer: "https://accounts.google.com".to_string(),
632 identity: "user@example.com".to_string(),
633 };
634 let result =
635 verify_image_signature(&policy, "docker.io", "library/alpine", "sha256:abc").await;
636 assert!(!result.is_ok());
638 }
639
640 fn generate_test_p256_key() -> (p256::ecdsa::SigningKey, Vec<u8>, String) {
644 use p256::ecdsa::SigningKey;
645
646 let signing_key = SigningKey::random(&mut rand::thread_rng());
647 let verifying_key = signing_key.verifying_key();
648 let pub_bytes = verifying_key.to_encoded_point(false).as_bytes().to_vec();
649
650 let spki_der = build_p256_spki_der(&pub_bytes);
652 let b64 = base64_encode_for_test(&spki_der);
653 let pem = format!(
654 "-----BEGIN PUBLIC KEY-----\n{}\n-----END PUBLIC KEY-----\n",
655 b64
656 );
657
658 (signing_key, pub_bytes, pem)
659 }
660
661 fn build_p256_spki_der(pub_key_bytes: &[u8]) -> Vec<u8> {
663 let ec_oid: &[u8] = &[0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01];
665 let p256_oid: &[u8] = &[0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07];
667
668 let alg_content_len = ec_oid.len() + p256_oid.len();
670 let mut alg_id = vec![0x30];
671 encode_der_length(&mut alg_id, alg_content_len);
672 alg_id.extend_from_slice(ec_oid);
673 alg_id.extend_from_slice(p256_oid);
674
675 let bit_string_len = 1 + pub_key_bytes.len(); let mut bit_string = vec![0x03];
678 encode_der_length(&mut bit_string, bit_string_len);
679 bit_string.push(0x00); bit_string.extend_from_slice(pub_key_bytes);
681
682 let total_content_len = alg_id.len() + bit_string.len();
684 let mut spki = vec![0x30];
685 encode_der_length(&mut spki, total_content_len);
686 spki.extend_from_slice(&alg_id);
687 spki.extend_from_slice(&bit_string);
688
689 spki
690 }
691
692 fn encode_der_length(buf: &mut Vec<u8>, len: usize) {
693 if len < 0x80 {
694 buf.push(len as u8);
695 } else if len < 0x100 {
696 buf.push(0x81);
697 buf.push(len as u8);
698 } else {
699 buf.push(0x82);
700 buf.push((len >> 8) as u8);
701 buf.push(len as u8);
702 }
703 }
704
705 fn base64_encode_for_test(data: &[u8]) -> String {
706 base64::engine::general_purpose::STANDARD.encode(data)
707 }
708
709 #[test]
710 fn test_parse_pem_public_key_spki() {
711 let (_sk, expected_pub, pem) = generate_test_p256_key();
712 let parsed = parse_pem_public_key(pem.as_bytes()).unwrap();
713 assert_eq!(parsed, expected_pub);
714 }
715
716 #[test]
717 fn test_parse_pem_public_key_invalid() {
718 let result = parse_pem_public_key(b"not a pem file");
719 assert!(result.is_err());
720 }
721
722 #[test]
723 fn test_verify_ecdsa_p256_valid_signature() {
724 use p256::ecdsa::{signature::Signer, SigningKey};
725
726 let signing_key = SigningKey::random(&mut rand::thread_rng());
727 let verifying_key = signing_key.verifying_key();
728 let pub_bytes = verifying_key.to_encoded_point(false).as_bytes().to_vec();
729
730 let message = b"test payload for cosign verification";
731 let sig: p256::ecdsa::DerSignature = signing_key.sign(message);
732
733 let result = verify_ecdsa_p256(&pub_bytes, message, sig.as_bytes());
734 assert!(result.is_ok());
735 }
736
737 #[test]
738 fn test_verify_ecdsa_p256_wrong_key_rejects() {
739 use p256::ecdsa::{signature::Signer, SigningKey};
740
741 let signing_key = SigningKey::random(&mut rand::thread_rng());
742 let wrong_key = SigningKey::random(&mut rand::thread_rng());
743 let wrong_pub = wrong_key
744 .verifying_key()
745 .to_encoded_point(false)
746 .as_bytes()
747 .to_vec();
748
749 let message = b"test payload";
750 let sig: p256::ecdsa::DerSignature = signing_key.sign(message);
751
752 let result = verify_ecdsa_p256(&wrong_pub, message, sig.as_bytes());
753 assert!(result.is_err());
754 }
755
756 #[test]
757 fn test_verify_ecdsa_p256_tampered_message_rejects() {
758 use p256::ecdsa::{signature::Signer, SigningKey};
759
760 let signing_key = SigningKey::random(&mut rand::thread_rng());
761 let pub_bytes = signing_key
762 .verifying_key()
763 .to_encoded_point(false)
764 .as_bytes()
765 .to_vec();
766
767 let message = b"original message";
768 let sig: p256::ecdsa::DerSignature = signing_key.sign(message);
769
770 let result = verify_ecdsa_p256(&pub_bytes, b"tampered message", sig.as_bytes());
771 assert!(result.is_err());
772 }
773
774 #[test]
775 fn test_verify_ecdsa_p256_fixed_size_signature() {
776 use p256::ecdsa::{signature::Signer, Signature, SigningKey};
777
778 let signing_key = SigningKey::random(&mut rand::thread_rng());
779 let pub_bytes = signing_key
780 .verifying_key()
781 .to_encoded_point(false)
782 .as_bytes()
783 .to_vec();
784
785 let message = b"test with fixed-size sig";
786 let sig: Signature = signing_key.sign(message);
787
788 assert_eq!(sig.to_bytes().len(), 64);
790 let result = verify_ecdsa_p256(&pub_bytes, message, &sig.to_bytes());
791 assert!(result.is_ok());
792 }
793
794 #[test]
795 fn test_cosign_key_end_to_end_with_temp_file() {
796 use p256::ecdsa::signature::Signer;
797
798 let (signing_key, _pub_bytes, pem) = generate_test_p256_key();
799
800 let dir = tempfile::tempdir().unwrap();
802 let key_path = dir.path().join("cosign.pub");
803 std::fs::write(&key_path, &pem).unwrap();
804
805 let digest = "sha256:abc123def456";
807 let payload = serde_json::json!({
808 "critical": {
809 "identity": { "docker-reference": "docker.io/library/alpine" },
810 "image": { "docker-manifest-digest": digest },
811 "type": "cosign container image signature"
812 },
813 "optional": {}
814 });
815 let payload_bytes = serde_json::to_vec(&payload).unwrap();
816 let sig: p256::ecdsa::DerSignature = signing_key.sign(&payload_bytes);
817
818 let envelope = serde_json::json!({
819 "payload": base64_encode_for_test(&payload_bytes),
820 "signature": base64_encode_for_test(sig.as_bytes()),
821 });
822 let envelope_bytes = serde_json::to_vec(&envelope).unwrap();
823
824 let env: CosignSignatureEnvelope = serde_json::from_slice(&envelope_bytes).unwrap();
827 let decoded_payload = base64_decode(&env.payload).unwrap();
828 let decoded_sig = base64_decode(&env.signature).unwrap();
829
830 let pem_bytes = std::fs::read(&key_path).unwrap();
832 let pub_key = parse_pem_public_key(&pem_bytes).unwrap();
833
834 assert!(verify_ecdsa_p256(&pub_key, &decoded_payload, &decoded_sig).is_ok());
836
837 assert!(verify_cosign_payload(&decoded_payload, digest).is_ok());
839 }
840
841 #[test]
844 fn test_cosign_payload_serde_roundtrip() {
845 let payload = CosignPayload {
846 critical: CosignCritical {
847 identity: CosignIdentity {
848 docker_reference: "ghcr.io/myorg/myimage".to_string(),
849 },
850 image: CosignImage {
851 docker_manifest_digest: "sha256:deadbeef".to_string(),
852 },
853 sig_type: "cosign container image signature".to_string(),
854 },
855 optional: serde_json::json!({"creator": "a3s-box"}),
856 };
857 let json = serde_json::to_string(&payload).unwrap();
858 let parsed: CosignPayload = serde_json::from_str(&json).unwrap();
859 assert_eq!(
860 parsed.critical.image.docker_manifest_digest,
861 "sha256:deadbeef"
862 );
863 assert_eq!(
864 parsed.critical.identity.docker_reference,
865 "ghcr.io/myorg/myimage"
866 );
867 }
868
869 #[test]
872 fn test_pem_to_der_valid() {
873 let data = vec![0x30, 0x03, 0x01, 0x01, 0xFF]; let b64 = base64_encode_for_test(&data);
876 let pem = format!(
877 "-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----\n",
878 b64
879 );
880 let der = pem_to_der(&pem).unwrap();
881 assert_eq!(der, data);
882 }
883
884 #[test]
885 fn test_pem_to_der_no_markers() {
886 let result = pem_to_der("not a pem");
887 assert!(result.is_err());
888 assert!(result.unwrap_err().contains("No PEM begin marker"));
889 }
890
891 #[test]
894 fn test_annotation_keys() {
895 assert_eq!(annotations::CERTIFICATE, "dev.sigstore.cosign/certificate");
896 assert_eq!(annotations::CHAIN, "dev.sigstore.cosign/chain");
897 assert_eq!(annotations::BUNDLE, "dev.sigstore.cosign/bundle");
898 }
899
900 #[test]
903 fn test_fulcio_issuer_oid_is_valid() {
904 let oid = der::asn1::ObjectIdentifier::new(FULCIO_ISSUER_OID);
906 assert!(oid.is_ok());
907 }
908
909 #[test]
910 fn test_extract_cert_public_key_from_self_signed() {
911 let key_pair = rcgen::KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).unwrap();
913 let params = rcgen::CertificateParams::new(vec!["test".to_string()]).unwrap();
914 let cert = params.self_signed(&key_pair).unwrap();
915 let cert_der = cert.der();
916
917 let parsed = x509_cert::Certificate::from_der(cert_der).unwrap();
918 let extracted = extract_cert_public_key(&parsed);
919 assert!(extracted.is_ok());
920 assert_eq!(extracted.unwrap().len(), 65);
922 }
923
924 #[test]
927 fn test_base64_encode_roundtrip() {
928 let data = b"hello cosign signing";
929 let encoded = base64_encode(data);
930 let decoded = base64_decode(&encoded).unwrap();
931 assert_eq!(decoded, data);
932 }
933
934 #[test]
935 fn test_cosign_signature_envelope_serde_roundtrip() {
936 let envelope = CosignSignatureEnvelope {
937 payload: base64_encode(b"test payload"),
938 signature: base64_encode(b"test signature"),
939 };
940 let json = serde_json::to_string(&envelope).unwrap();
941 let parsed: CosignSignatureEnvelope = serde_json::from_str(&json).unwrap();
942 assert_eq!(parsed.payload, envelope.payload);
943 assert_eq!(parsed.signature, envelope.signature);
944 }
945
946 #[test]
947 fn test_parse_pem_private_key_sec1() {
948 let signing_key = p256::ecdsa::SigningKey::random(&mut rand::thread_rng());
950 let secret_key = signing_key.as_nonzero_scalar();
951 let _sec1_der = secret_key.to_bytes();
952
953 use p256::pkcs8::EncodePrivateKey;
956 let pkcs8_der = p256::SecretKey::from(signing_key.clone())
957 .to_pkcs8_der()
958 .unwrap();
959 let b64 = base64_encode(pkcs8_der.as_bytes());
960 let pem = format!(
961 "-----BEGIN PRIVATE KEY-----\n{}\n-----END PRIVATE KEY-----\n",
962 b64
963 );
964
965 let parsed = parse_pem_private_key(pem.as_bytes());
966 assert!(parsed.is_ok());
967
968 use p256::ecdsa::{signature::Signer, signature::Verifier};
970 let msg = b"test message";
971 let sig: p256::ecdsa::DerSignature = parsed.unwrap().sign(msg);
972 assert!(signing_key.verifying_key().verify(msg, &sig).is_ok());
973 }
974
975 #[test]
976 fn test_parse_pem_private_key_invalid() {
977 let result = parse_pem_private_key(b"not a pem file");
978 assert!(result.is_err());
979 }
980
981 #[test]
982 fn test_sign_and_verify_roundtrip() {
983 use p256::ecdsa::{signature::Signer, SigningKey};
984
985 let signing_key = SigningKey::random(&mut rand::thread_rng());
987 let pub_bytes = signing_key
988 .verifying_key()
989 .to_encoded_point(false)
990 .as_bytes()
991 .to_vec();
992
993 let digest = "sha256:deadbeef1234";
995 let payload = CosignPayload {
996 critical: CosignCritical {
997 identity: CosignIdentity {
998 docker_reference: "ghcr.io/myorg/myimage:latest".to_string(),
999 },
1000 image: CosignImage {
1001 docker_manifest_digest: digest.to_string(),
1002 },
1003 sig_type: "cosign container image signature".to_string(),
1004 },
1005 optional: serde_json::json!({}),
1006 };
1007 let payload_bytes = serde_json::to_vec(&payload).unwrap();
1008
1009 let sig: p256::ecdsa::DerSignature = signing_key.sign(&payload_bytes);
1011
1012 let envelope = CosignSignatureEnvelope {
1014 payload: base64_encode(&payload_bytes),
1015 signature: base64_encode(sig.as_bytes()),
1016 };
1017 let envelope_bytes = serde_json::to_vec(&envelope).unwrap();
1018
1019 let parsed_env: CosignSignatureEnvelope = serde_json::from_slice(&envelope_bytes).unwrap();
1021 let decoded_payload = base64_decode(&parsed_env.payload).unwrap();
1022 let decoded_sig = base64_decode(&parsed_env.signature).unwrap();
1023
1024 assert!(verify_ecdsa_p256(&pub_bytes, &decoded_payload, &decoded_sig).is_ok());
1025 assert!(verify_cosign_payload(&decoded_payload, digest).is_ok());
1026 }
1027
1028 #[test]
1029 fn test_extract_pem_content_valid() {
1030 let data = vec![1, 2, 3, 4, 5];
1031 let b64 = base64_encode(&data);
1032 let pem = format!("-----BEGIN TEST-----\n{}\n-----END TEST-----\n", b64);
1033 let result = extract_pem_content(&pem, "-----BEGIN TEST-----", "-----END TEST-----");
1034 assert!(result.is_ok());
1035 assert_eq!(result.unwrap(), data);
1036 }
1037
1038 #[test]
1039 fn test_extract_pem_content_missing_begin() {
1040 let result = extract_pem_content("no markers", "-----BEGIN X-----", "-----END X-----");
1041 assert!(result.is_err());
1042 }
1043
1044 #[test]
1045 fn test_sign_result_structure() {
1046 let result = SignResult {
1047 signature_tag: "sha256-abc123.sig".to_string(),
1048 };
1049 assert_eq!(result.signature_tag, "sha256-abc123.sig");
1050 }
1051}