Skip to main content

a3s_box_runtime/oci/signing/
mod.rs

1//! Image signature verification for OCI images.
2//!
3//! Supports cosign-compatible signature verification:
4//! - Key-based: verify against a PEM-encoded public key
5//! - Keyless: verify Fulcio certificate identity (OIDC issuer + SAN) and signature
6
7use a3s_box_core::error::{BoxError, Result};
8// `base64::Engine` is only needed by the test module now that the base64
9// helpers moved to `crypto`; `der::Decode` is still used by cert parsing here.
10#[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/// Image signature verification policy.
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
29pub enum SignaturePolicy {
30    /// Skip signature verification (default for backward compatibility).
31    #[default]
32    Skip,
33    /// Require a valid cosign signature verified against a public key.
34    CosignKey {
35        /// Path to the PEM-encoded public key file.
36        public_key: String,
37    },
38    /// Require a valid cosign keyless signature (Fulcio + Rekor transparency log).
39    CosignKeyless {
40        /// Expected OIDC issuer (e.g., "https://accounts.google.com").
41        issuer: String,
42        /// Expected certificate identity (e.g., "user@example.com").
43        identity: String,
44    },
45}
46
47/// Result of a signature verification check.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum VerifyResult {
50    /// Signature is valid.
51    Verified,
52    /// Verification was skipped (policy = Skip).
53    Skipped,
54    /// No signature found for the image.
55    NoSignature,
56    /// Signature found but verification failed.
57    Failed(String),
58}
59
60impl VerifyResult {
61    /// Returns true if the result is acceptable (Verified or Skipped).
62    pub fn is_ok(&self) -> bool {
63        matches!(self, Self::Verified | Self::Skipped)
64    }
65}
66
67/// Cosign signature payload (SimpleSigning format).
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub(super) struct CosignPayload {
70    /// The critical section containing image identity.
71    pub(super) critical: CosignCritical,
72    /// Optional annotations.
73    #[serde(default)]
74    pub(super) optional: serde_json::Value,
75}
76
77/// Critical section of a cosign signature payload.
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub(super) struct CosignCritical {
80    /// Identity of the signed image.
81    pub(super) identity: CosignIdentity,
82    /// Image reference being signed.
83    pub(super) image: CosignImage,
84    /// Type of signature (always "cosign container image signature").
85    #[serde(rename = "type")]
86    pub(super) sig_type: String,
87}
88
89/// Identity in a cosign signature.
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub(super) struct CosignIdentity {
92    /// Docker reference of the signed image.
93    #[serde(rename = "docker-reference")]
94    pub(super) docker_reference: String,
95}
96
97/// Image reference in a cosign signature.
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub(super) struct CosignImage {
100    /// Digest of the signed manifest.
101    #[serde(rename = "docker-manifest-digest")]
102    pub(super) docker_manifest_digest: String,
103}
104
105/// Cosign OCI annotation keys for keyless signatures.
106mod annotations {
107    pub const CERTIFICATE: &str = "dev.sigstore.cosign/certificate";
108    /// Certificate chain (intermediate + root). Reserved for future chain validation.
109    #[allow(dead_code)]
110    pub const CHAIN: &str = "dev.sigstore.cosign/chain";
111    /// Rekor transparency log bundle. Reserved for future SET verification.
112    #[allow(dead_code)]
113    pub const BUNDLE: &str = "dev.sigstore.cosign/bundle";
114}
115
116/// Cosign signature tag convention: `sha256-<hex>.sig`
117fn 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
124/// Fetched cosign signature data from the registry.
125struct CosignSignatureData {
126    /// Raw signature layer bytes.
127    layer_data: Vec<u8>,
128    /// OCI manifest annotations (contains Fulcio cert, chain, bundle for keyless).
129    annotations: std::collections::HashMap<String, String>,
130}
131
132/// Check if a cosign signature exists for the given image in the registry.
133///
134/// Returns the signature layer data and OCI manifest annotations.
135async 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    // Try to pull the signature manifest
155    match client
156        .pull_image_manifest(&reference, &RegistryAuth::Anonymous)
157        .await
158    {
159        Ok((manifest, _digest)) => {
160            // Collect annotations from the manifest layers
161            let mut all_annotations = std::collections::HashMap::new();
162
163            // Annotations can be on the manifest itself or on individual layers
164            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            // Pull the first layer (the signature payload)
174            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            // Distinguish "no signature" (manifest not found) from actual errors
196            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
216/// Verify a cosign signature payload against a public key.
217///
218/// The payload is a JSON SimpleSigning document. The signature is
219/// verified using the provided PEM-encoded public key (ECDSA P-256 or RSA).
220fn verify_cosign_payload(payload: &[u8], manifest_digest: &str) -> Result<CosignPayload> {
221    // Parse the payload
222    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    // Verify the digest matches
229    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
242/// Verify an image signature according to the given policy.
243pub 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
262/// Verify a cosign signature using a PEM-encoded public key.
263///
264/// Steps:
265/// 1. Read the PEM public key from disk
266/// 2. Fetch the cosign signature artifact from the registry
267/// 3. Extract the SimpleSigning payload and base64-encoded signature from the OCI layer
268/// 4. Verify the ECDSA P-256 signature over the payload using the public key
269/// 5. Validate the payload digest matches the manifest digest
270async fn verify_cosign_key(
271    public_key_path: &str,
272    registry: &str,
273    repository: &str,
274    manifest_digest: &str,
275) -> VerifyResult {
276    // 1. Read the PEM public key
277    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    // 2. Fetch the cosign signature artifact
295    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    // 3. Parse the signature layer.
304    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    // 4. Verify the ECDSA P-256 signature over the payload
329    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    // 5. Validate the payload digest matches
334    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
340/// Verify a cosign keyless signature using Fulcio certificate + Rekor bundle.
341///
342/// Steps:
343/// 1. Fetch the cosign signature artifact from the registry
344/// 2. Extract the Fulcio certificate and chain from OCI annotations
345/// 3. Verify the certificate's OIDC issuer and identity (SAN) match expectations
346/// 4. Extract the public key from the Fulcio certificate
347/// 5. Verify the ECDSA signature over the payload using the cert's public key
348/// 6. Validate the payload digest matches the manifest digest
349async 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    // 1. Fetch the cosign signature artifact
357    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    // 2. Extract the Fulcio certificate from annotations
366    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    // 3. Parse the Fulcio certificate and verify identity claims
378    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    // Check OIDC issuer from certificate extension (OID 1.3.6.1.4.1.57264.1.1)
393    if let Err(e) = verify_fulcio_issuer(&cert, expected_issuer) {
394        return VerifyResult::Failed(format!("Fulcio issuer mismatch: {}", e));
395    }
396
397    // Check identity from Subject Alternative Name (email or URI)
398    if let Err(e) = verify_fulcio_identity(&cert, expected_identity) {
399        return VerifyResult::Failed(format!("Fulcio identity mismatch: {}", e));
400    }
401
402    // 4. Extract the public key from the certificate
403    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    // 5. Parse the signature envelope and verify
414    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    // Verify the ECDSA P-256 signature using the Fulcio cert's public key
439    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    // 6. Validate the payload digest matches
444    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
458/// Fulcio OIDC issuer extension OID: 1.3.6.1.4.1.57264.1.1
459const FULCIO_ISSUER_OID: &str = "1.3.6.1.4.1.57264.1.1";
460
461/// Cosign signature envelope stored in the OCI layer.
462#[derive(Debug, Serialize, Deserialize)]
463struct CosignSignatureEnvelope {
464    /// Base64-encoded SimpleSigning payload.
465    payload: String,
466    /// Base64-encoded ECDSA signature over the payload.
467    signature: String,
468}
469
470#[cfg(test)]
471mod tests {
472    use super::*;
473
474    // --- SignaturePolicy tests ---
475
476    #[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    // --- VerifyResult tests ---
511
512    #[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    // --- Cosign tag convention tests ---
527
528    #[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    // --- Cosign payload tests ---
541
542    #[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    // --- Async verification tests ---
599
600    #[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        // Keyless verification now attempts to fetch from registry.
629        // With a fake digest, it should return NoSignature or Failed (network error).
630        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        // Should not be Verified (no real signature exists)
637        assert!(!result.is_ok());
638    }
639
640    // --- ECDSA P-256 crypto verification tests ---
641
642    /// Generate a P-256 key pair and return (private_key, public_key_sec1_bytes, pem_string).
643    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        // Build SPKI DER manually for the PEM
651        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    /// Build a minimal SPKI DER for a P-256 public key.
662    fn build_p256_spki_der(pub_key_bytes: &[u8]) -> Vec<u8> {
663        // OID for id-ecPublicKey: 1.2.840.10045.2.1
664        let ec_oid: &[u8] = &[0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01];
665        // OID for prime256v1 (P-256): 1.2.840.10045.3.1.7
666        let p256_oid: &[u8] = &[0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07];
667
668        // AlgorithmIdentifier SEQUENCE
669        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        // BIT STRING wrapping the public key
676        let bit_string_len = 1 + pub_key_bytes.len(); // 1 byte for unused bits count
677        let mut bit_string = vec![0x03];
678        encode_der_length(&mut bit_string, bit_string_len);
679        bit_string.push(0x00); // no unused bits
680        bit_string.extend_from_slice(pub_key_bytes);
681
682        // Outer SEQUENCE
683        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        // Fixed-size signature is 64 bytes (32 r + 32 s)
789        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        // Write PEM to temp file
801        let dir = tempfile::tempdir().unwrap();
802        let key_path = dir.path().join("cosign.pub");
803        std::fs::write(&key_path, &pem).unwrap();
804
805        // Create a signed cosign envelope
806        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        // Parse and verify the envelope manually (simulating what verify_cosign_key does
825        // after fetching from registry)
826        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        // Read the key
831        let pem_bytes = std::fs::read(&key_path).unwrap();
832        let pub_key = parse_pem_public_key(&pem_bytes).unwrap();
833
834        // Verify signature
835        assert!(verify_ecdsa_p256(&pub_key, &decoded_payload, &decoded_sig).is_ok());
836
837        // Verify payload
838        assert!(verify_cosign_payload(&decoded_payload, digest).is_ok());
839    }
840
841    // --- CosignPayload serde tests ---
842
843    #[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    // --- PEM decoding tests ---
870
871    #[test]
872    fn test_pem_to_der_valid() {
873        // Create a minimal PEM block
874        let data = vec![0x30, 0x03, 0x01, 0x01, 0xFF]; // minimal DER
875        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    // --- Annotation constants tests ---
892
893    #[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    // --- Keyless verification unit tests ---
901
902    #[test]
903    fn test_fulcio_issuer_oid_is_valid() {
904        // Verify the OID string parses correctly
905        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        // Build a self-signed X.509 cert using rcgen and verify key extraction
912        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        // P-256 uncompressed point is 65 bytes (0x04 + 32 + 32)
921        assert_eq!(extracted.unwrap().len(), 65);
922    }
923
924    // --- Image signing tests ---
925
926    #[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        // Generate a P-256 key and export as SEC1 PEM
949        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        // Build a minimal SEC1 PEM (just the raw scalar isn't valid SEC1 DER,
954        // so use pkcs8 instead for this test)
955        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        // Verify the parsed key can sign and the original key can verify
969        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        // Generate key pair
986        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        // Build payload
994        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        // Sign
1010        let sig: p256::ecdsa::DerSignature = signing_key.sign(&payload_bytes);
1011
1012        // Build envelope
1013        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        // Verify: parse envelope, decode, verify signature, verify payload
1020        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}