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_ec_public_key_raw_point() {
718 let (_sk, expected_pub, _pem) = generate_test_p256_key();
719 let pem = format!(
720 "-----BEGIN EC PUBLIC KEY-----\n{}\n-----END EC PUBLIC KEY-----\n",
721 base64_encode(&expected_pub)
722 );
723
724 let parsed = parse_pem_public_key(pem.as_bytes()).unwrap();
725 assert_eq!(parsed, expected_pub);
726 }
727
728 #[test]
729 fn test_parse_pem_public_key_invalid() {
730 let result = parse_pem_public_key(b"not a pem file");
731 assert!(result.is_err());
732 }
733
734 #[test]
735 fn test_parse_pem_public_key_rejects_invalid_utf8() {
736 let result = parse_pem_public_key(&[0xff, 0xfe]);
737 assert!(result.is_err());
738 assert!(result.unwrap_err().contains("not valid UTF-8"));
739 }
740
741 #[test]
742 fn test_parse_pem_public_key_rejects_bad_spki_der() {
743 let pem = format!(
744 "-----BEGIN PUBLIC KEY-----\n{}\n-----END PUBLIC KEY-----\n",
745 base64_encode(b"not der")
746 );
747
748 let result = parse_pem_public_key(pem.as_bytes());
749 assert!(result.is_err());
750 assert!(result.unwrap_err().contains("Failed to parse SPKI"));
751 }
752
753 #[test]
754 fn test_verify_ecdsa_p256_valid_signature() {
755 use p256::ecdsa::{signature::Signer, SigningKey};
756
757 let signing_key = SigningKey::random(&mut rand::thread_rng());
758 let verifying_key = signing_key.verifying_key();
759 let pub_bytes = verifying_key.to_encoded_point(false).as_bytes().to_vec();
760
761 let message = b"test payload for cosign verification";
762 let sig: p256::ecdsa::DerSignature = signing_key.sign(message);
763
764 let result = verify_ecdsa_p256(&pub_bytes, message, sig.as_bytes());
765 assert!(result.is_ok());
766 }
767
768 #[test]
769 fn test_verify_ecdsa_p256_wrong_key_rejects() {
770 use p256::ecdsa::{signature::Signer, SigningKey};
771
772 let signing_key = SigningKey::random(&mut rand::thread_rng());
773 let wrong_key = SigningKey::random(&mut rand::thread_rng());
774 let wrong_pub = wrong_key
775 .verifying_key()
776 .to_encoded_point(false)
777 .as_bytes()
778 .to_vec();
779
780 let message = b"test payload";
781 let sig: p256::ecdsa::DerSignature = signing_key.sign(message);
782
783 let result = verify_ecdsa_p256(&wrong_pub, message, sig.as_bytes());
784 assert!(result.is_err());
785 }
786
787 #[test]
788 fn test_verify_ecdsa_p256_tampered_message_rejects() {
789 use p256::ecdsa::{signature::Signer, SigningKey};
790
791 let signing_key = SigningKey::random(&mut rand::thread_rng());
792 let pub_bytes = signing_key
793 .verifying_key()
794 .to_encoded_point(false)
795 .as_bytes()
796 .to_vec();
797
798 let message = b"original message";
799 let sig: p256::ecdsa::DerSignature = signing_key.sign(message);
800
801 let result = verify_ecdsa_p256(&pub_bytes, b"tampered message", sig.as_bytes());
802 assert!(result.is_err());
803 }
804
805 #[test]
806 fn test_verify_ecdsa_p256_fixed_size_signature() {
807 use p256::ecdsa::{signature::Signer, Signature, SigningKey};
808
809 let signing_key = SigningKey::random(&mut rand::thread_rng());
810 let pub_bytes = signing_key
811 .verifying_key()
812 .to_encoded_point(false)
813 .as_bytes()
814 .to_vec();
815
816 let message = b"test with fixed-size sig";
817 let sig: Signature = signing_key.sign(message);
818
819 assert_eq!(sig.to_bytes().len(), 64);
821 let result = verify_ecdsa_p256(&pub_bytes, message, &sig.to_bytes());
822 assert!(result.is_ok());
823 }
824
825 #[test]
826 fn test_verify_ecdsa_p256_rejects_unknown_signature_length() {
827 let (_sk, pub_bytes, _pem) = generate_test_p256_key();
828 let result = verify_ecdsa_p256(&pub_bytes, b"message", b"short");
829
830 assert!(result.is_err());
831 assert!(result
832 .unwrap_err()
833 .contains("Unrecognized signature format"));
834 }
835
836 #[test]
837 fn test_cosign_key_end_to_end_with_temp_file() {
838 use p256::ecdsa::signature::Signer;
839
840 let (signing_key, _pub_bytes, pem) = generate_test_p256_key();
841
842 let dir = tempfile::tempdir().unwrap();
844 let key_path = dir.path().join("cosign.pub");
845 std::fs::write(&key_path, &pem).unwrap();
846
847 let digest = "sha256:abc123def456";
849 let payload = serde_json::json!({
850 "critical": {
851 "identity": { "docker-reference": "docker.io/library/alpine" },
852 "image": { "docker-manifest-digest": digest },
853 "type": "cosign container image signature"
854 },
855 "optional": {}
856 });
857 let payload_bytes = serde_json::to_vec(&payload).unwrap();
858 let sig: p256::ecdsa::DerSignature = signing_key.sign(&payload_bytes);
859
860 let envelope = serde_json::json!({
861 "payload": base64_encode_for_test(&payload_bytes),
862 "signature": base64_encode_for_test(sig.as_bytes()),
863 });
864 let envelope_bytes = serde_json::to_vec(&envelope).unwrap();
865
866 let env: CosignSignatureEnvelope = serde_json::from_slice(&envelope_bytes).unwrap();
869 let decoded_payload = base64_decode(&env.payload).unwrap();
870 let decoded_sig = base64_decode(&env.signature).unwrap();
871
872 let pem_bytes = std::fs::read(&key_path).unwrap();
874 let pub_key = parse_pem_public_key(&pem_bytes).unwrap();
875
876 assert!(verify_ecdsa_p256(&pub_key, &decoded_payload, &decoded_sig).is_ok());
878
879 assert!(verify_cosign_payload(&decoded_payload, digest).is_ok());
881 }
882
883 #[test]
886 fn test_cosign_payload_serde_roundtrip() {
887 let payload = CosignPayload {
888 critical: CosignCritical {
889 identity: CosignIdentity {
890 docker_reference: "ghcr.io/myorg/myimage".to_string(),
891 },
892 image: CosignImage {
893 docker_manifest_digest: "sha256:deadbeef".to_string(),
894 },
895 sig_type: "cosign container image signature".to_string(),
896 },
897 optional: serde_json::json!({"creator": "a3s-box"}),
898 };
899 let json = serde_json::to_string(&payload).unwrap();
900 let parsed: CosignPayload = serde_json::from_str(&json).unwrap();
901 assert_eq!(
902 parsed.critical.image.docker_manifest_digest,
903 "sha256:deadbeef"
904 );
905 assert_eq!(
906 parsed.critical.identity.docker_reference,
907 "ghcr.io/myorg/myimage"
908 );
909 }
910
911 #[test]
914 fn test_pem_to_der_valid() {
915 let data = vec![0x30, 0x03, 0x01, 0x01, 0xFF]; let b64 = base64_encode_for_test(&data);
918 let pem = format!(
919 "-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----\n",
920 b64
921 );
922 let der = pem_to_der(&pem).unwrap();
923 assert_eq!(der, data);
924 }
925
926 #[test]
927 fn test_pem_to_der_accepts_crlf_and_inner_whitespace() {
928 let data = vec![0x30, 0x03, 0x01, 0x01, 0xFF];
929 let b64 = base64_encode_for_test(&data);
930 let pem = format!(
931 "-----BEGIN CERTIFICATE-----\r\n{}\r\n {}\r\n-----END CERTIFICATE-----\r\n",
932 &b64[..4],
933 &b64[4..]
934 );
935
936 let der = pem_to_der(&pem).unwrap();
937 assert_eq!(der, data);
938 }
939
940 #[test]
941 fn test_pem_to_der_no_markers() {
942 let result = pem_to_der("not a pem");
943 assert!(result.is_err());
944 assert!(result.unwrap_err().contains("No PEM begin marker"));
945 }
946
947 #[test]
948 fn test_pem_to_der_missing_end_marker() {
949 let result = pem_to_der("-----BEGIN CERTIFICATE-----\nAQID\n");
950 assert!(result.is_err());
951 assert!(result.unwrap_err().contains("No PEM end marker"));
952 }
953
954 #[test]
957 fn test_annotation_keys() {
958 assert_eq!(annotations::CERTIFICATE, "dev.sigstore.cosign/certificate");
959 assert_eq!(annotations::CHAIN, "dev.sigstore.cosign/chain");
960 assert_eq!(annotations::BUNDLE, "dev.sigstore.cosign/bundle");
961 }
962
963 #[test]
966 fn test_fulcio_issuer_oid_is_valid() {
967 let oid = der::asn1::ObjectIdentifier::new(FULCIO_ISSUER_OID);
969 assert!(oid.is_ok());
970 }
971
972 #[test]
973 fn test_extract_cert_public_key_from_self_signed() {
974 let key_pair = rcgen::KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).unwrap();
976 let params = rcgen::CertificateParams::new(vec!["test".to_string()]).unwrap();
977 let cert = params.self_signed(&key_pair).unwrap();
978 let cert_der = cert.der();
979
980 let parsed = x509_cert::Certificate::from_der(cert_der).unwrap();
981 let extracted = extract_cert_public_key(&parsed);
982 assert!(extracted.is_ok());
983 assert_eq!(extracted.unwrap().len(), 65);
985 }
986
987 fn private_key_pem_for_signing_test() -> String {
990 use p256::ecdsa::SigningKey;
991 use p256::pkcs8::EncodePrivateKey;
992
993 let signing_key = SigningKey::random(&mut rand::thread_rng());
994 let pkcs8_der = p256::SecretKey::from(signing_key).to_pkcs8_der().unwrap();
995 let b64 = base64_encode(pkcs8_der.as_bytes());
996 format!(
997 "-----BEGIN PRIVATE KEY-----\n{}\n-----END PRIVATE KEY-----\n",
998 b64
999 )
1000 }
1001
1002 #[tokio::test]
1003 async fn test_sign_image_missing_key_file_errors_before_registry() {
1004 let dir = tempfile::tempdir().unwrap();
1005 let missing = dir.path().join("missing.key");
1006
1007 let err = sign_image(
1008 missing.to_str().unwrap(),
1009 "registry.example.com",
1010 "a3s/app",
1011 "sha256:abc123",
1012 "registry.example.com/a3s/app:latest",
1013 )
1014 .await
1015 .unwrap_err();
1016
1017 assert!(err.to_string().contains("Failed to read signing key"));
1018 }
1019
1020 #[tokio::test]
1021 async fn test_sign_image_invalid_key_file_errors_before_registry() {
1022 let dir = tempfile::tempdir().unwrap();
1023 let key_path = dir.path().join("cosign.key");
1024 std::fs::write(&key_path, "not a pem").unwrap();
1025
1026 let err = sign_image(
1027 key_path.to_str().unwrap(),
1028 "registry.example.com",
1029 "a3s/app",
1030 "sha256:abc123",
1031 "registry.example.com/a3s/app:latest",
1032 )
1033 .await
1034 .unwrap_err();
1035
1036 assert!(err.to_string().contains("Failed to parse signing key"));
1037 }
1038
1039 #[tokio::test]
1040 async fn test_sign_image_invalid_signature_reference_after_payload_signing() {
1041 let dir = tempfile::tempdir().unwrap();
1042 let key_path = dir.path().join("cosign.key");
1043 std::fs::write(&key_path, private_key_pem_for_signing_test()).unwrap();
1044
1045 let err = sign_image(
1046 key_path.to_str().unwrap(),
1047 "bad registry",
1048 "a3s/app",
1049 "sha256:abc123",
1050 "registry.example.com/a3s/app:latest",
1051 )
1052 .await
1053 .unwrap_err();
1054
1055 assert!(err.to_string().contains("Invalid signature reference"));
1056 }
1057
1058 #[test]
1059 fn test_base64_encode_roundtrip() {
1060 let data = b"hello cosign signing";
1061 let encoded = base64_encode(data);
1062 let decoded = base64_decode(&encoded).unwrap();
1063 assert_eq!(decoded, data);
1064 }
1065
1066 #[test]
1067 fn test_cosign_signature_envelope_serde_roundtrip() {
1068 let envelope = CosignSignatureEnvelope {
1069 payload: base64_encode(b"test payload"),
1070 signature: base64_encode(b"test signature"),
1071 };
1072 let json = serde_json::to_string(&envelope).unwrap();
1073 let parsed: CosignSignatureEnvelope = serde_json::from_str(&json).unwrap();
1074 assert_eq!(parsed.payload, envelope.payload);
1075 assert_eq!(parsed.signature, envelope.signature);
1076 }
1077
1078 #[test]
1079 fn test_parse_pem_private_key_sec1() {
1080 let signing_key = p256::ecdsa::SigningKey::random(&mut rand::thread_rng());
1082 let secret_key = signing_key.as_nonzero_scalar();
1083 let _sec1_der = secret_key.to_bytes();
1084
1085 use p256::pkcs8::EncodePrivateKey;
1088 let pkcs8_der = p256::SecretKey::from(signing_key.clone())
1089 .to_pkcs8_der()
1090 .unwrap();
1091 let b64 = base64_encode(pkcs8_der.as_bytes());
1092 let pem = format!(
1093 "-----BEGIN PRIVATE KEY-----\n{}\n-----END PRIVATE KEY-----\n",
1094 b64
1095 );
1096
1097 let parsed = parse_pem_private_key(pem.as_bytes());
1098 assert!(parsed.is_ok());
1099
1100 use p256::ecdsa::{signature::Signer, signature::Verifier};
1102 let msg = b"test message";
1103 let sig: p256::ecdsa::DerSignature = parsed.unwrap().sign(msg);
1104 assert!(signing_key.verifying_key().verify(msg, &sig).is_ok());
1105 }
1106
1107 #[test]
1108 fn test_parse_pem_private_key_invalid() {
1109 let result = parse_pem_private_key(b"not a pem file");
1110 assert!(result.is_err());
1111 }
1112
1113 #[test]
1114 fn test_parse_pem_private_key_rejects_invalid_utf8() {
1115 match parse_pem_private_key(&[0xff, 0xfe]) {
1116 Ok(_) => panic!("invalid UTF-8 must not parse as a private key"),
1117 Err(err) => assert!(err.contains("not valid UTF-8")),
1118 }
1119 }
1120
1121 #[test]
1122 fn test_sign_and_verify_roundtrip() {
1123 use p256::ecdsa::{signature::Signer, SigningKey};
1124
1125 let signing_key = SigningKey::random(&mut rand::thread_rng());
1127 let pub_bytes = signing_key
1128 .verifying_key()
1129 .to_encoded_point(false)
1130 .as_bytes()
1131 .to_vec();
1132
1133 let digest = "sha256:deadbeef1234";
1135 let payload = CosignPayload {
1136 critical: CosignCritical {
1137 identity: CosignIdentity {
1138 docker_reference: "ghcr.io/myorg/myimage:latest".to_string(),
1139 },
1140 image: CosignImage {
1141 docker_manifest_digest: digest.to_string(),
1142 },
1143 sig_type: "cosign container image signature".to_string(),
1144 },
1145 optional: serde_json::json!({}),
1146 };
1147 let payload_bytes = serde_json::to_vec(&payload).unwrap();
1148
1149 let sig: p256::ecdsa::DerSignature = signing_key.sign(&payload_bytes);
1151
1152 let envelope = CosignSignatureEnvelope {
1154 payload: base64_encode(&payload_bytes),
1155 signature: base64_encode(sig.as_bytes()),
1156 };
1157 let envelope_bytes = serde_json::to_vec(&envelope).unwrap();
1158
1159 let parsed_env: CosignSignatureEnvelope = serde_json::from_slice(&envelope_bytes).unwrap();
1161 let decoded_payload = base64_decode(&parsed_env.payload).unwrap();
1162 let decoded_sig = base64_decode(&parsed_env.signature).unwrap();
1163
1164 assert!(verify_ecdsa_p256(&pub_bytes, &decoded_payload, &decoded_sig).is_ok());
1165 assert!(verify_cosign_payload(&decoded_payload, digest).is_ok());
1166 }
1167
1168 #[test]
1169 fn test_extract_pem_content_valid() {
1170 let data = vec![1, 2, 3, 4, 5];
1171 let b64 = base64_encode(&data);
1172 let pem = format!("-----BEGIN TEST-----\n{}\n-----END TEST-----\n", b64);
1173 let result = extract_pem_content(&pem, "-----BEGIN TEST-----", "-----END TEST-----");
1174 assert!(result.is_ok());
1175 assert_eq!(result.unwrap(), data);
1176 }
1177
1178 #[test]
1179 fn test_extract_pem_content_missing_begin() {
1180 let result = extract_pem_content("no markers", "-----BEGIN X-----", "-----END X-----");
1181 assert!(result.is_err());
1182 }
1183
1184 #[test]
1185 fn test_extract_pem_content_missing_end() {
1186 let result = extract_pem_content(
1187 "-----BEGIN X-----\nAQID",
1188 "-----BEGIN X-----",
1189 "-----END X-----",
1190 );
1191 assert!(result.is_err());
1192 assert!(result.unwrap_err().contains("Missing PEM end marker"));
1193 }
1194
1195 #[test]
1196 fn test_sign_result_structure() {
1197 let result = SignResult {
1198 signature_tag: "sha256-abc123.sig".to_string(),
1199 };
1200 assert_eq!(result.signature_tag, "sha256-abc123.sig");
1201 }
1202}