1use base64::engine::general_purpose::STANDARD as B64;
18use base64::Engine;
19use p256::ecdsa::signature::Verifier;
20use p256::ecdsa::{Signature, VerifyingKey};
21use p256::pkcs8::DecodePublicKey;
22use serde::{Deserialize, Serialize};
23
24pub fn companion_sig_tag(image_digest: &str) -> Option<String> {
27 image_digest
28 .strip_prefix("sha256:")
29 .map(|hex| format!("sha256-{hex}.sig"))
30}
31
32#[derive(Clone, Debug, Default, Serialize, Deserialize)]
37pub struct TrustedKey {
38 pub key_id: String,
39 pub pem: String,
40 pub algorithm: String,
44}
45
46#[derive(Debug)]
47pub enum VerifyError {
48 InvalidPemKey,
49 SignatureDecode,
50 SignatureVerify,
51}
52
53pub fn verify_cosign_signature(
57 key_pem: &str,
58 payload: &[u8],
59 signature_b64: &str,
60) -> Result<(), VerifyError> {
61 let verifying_key =
62 VerifyingKey::from_public_key_pem(key_pem).map_err(|_| VerifyError::InvalidPemKey)?;
63 let sig_bytes = B64
64 .decode(signature_b64.trim().as_bytes())
65 .map_err(|_| VerifyError::SignatureDecode)?;
66 let sig = Signature::from_der(&sig_bytes).map_err(|_| VerifyError::SignatureDecode)?;
67 verifying_key
68 .verify(payload, &sig)
69 .map_err(|_| VerifyError::SignatureVerify)
70}
71
72pub fn extract_signature_annotation(manifest_json: &serde_json::Value) -> Option<(String, String)> {
77 let layer = manifest_json.get("layers")?.as_array()?.first()?;
78 let digest = layer.get("digest")?.as_str()?.to_string();
79 let sig = layer
80 .get("annotations")?
81 .as_object()?
82 .get("dev.cosignproject.cosign/signature")?
83 .as_str()?
84 .to_string();
85 Some((digest, sig))
86}
87
88pub fn referenced_image_digest(payload_bytes: &[u8]) -> Option<String> {
92 let v: serde_json::Value = serde_json::from_slice(payload_bytes).ok()?;
93 v.get("critical")?
94 .get("image")?
95 .get("docker-manifest-digest")?
96 .as_str()
97 .map(String::from)
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use p256::ecdsa::signature::Signer;
104 use p256::ecdsa::SigningKey;
105 use p256::pkcs8::EncodePublicKey;
106
107 fn keypair_pem() -> (SigningKey, String) {
108 let bytes = [7u8; 32];
111 let sk = SigningKey::from_bytes((&bytes).into()).unwrap();
112 let pem = sk
113 .verifying_key()
114 .to_public_key_pem(Default::default())
115 .unwrap();
116 (sk, pem)
117 }
118
119 #[test]
120 fn verify_roundtrip() {
121 let (sk, pem) = keypair_pem();
122 let payload = br#"{"critical":{"image":{"docker-manifest-digest":"sha256:abc"}}}"#;
123 let sig: Signature = sk.sign(payload);
124 let sig_b64 = B64.encode(sig.to_der());
125 verify_cosign_signature(&pem, payload, &sig_b64).unwrap();
126 }
127
128 #[test]
129 fn wrong_payload_fails() {
130 let (sk, pem) = keypair_pem();
131 let payload = b"original";
132 let sig: Signature = sk.sign(payload);
133 let sig_b64 = B64.encode(sig.to_der());
134 assert!(matches!(
135 verify_cosign_signature(&pem, b"tampered", &sig_b64),
136 Err(VerifyError::SignatureVerify)
137 ));
138 }
139
140 #[test]
141 fn malformed_pem_rejected() {
142 assert!(matches!(
143 verify_cosign_signature("not a pem", b"payload", "ignored"),
144 Err(VerifyError::InvalidPemKey)
145 ));
146 }
147
148 #[test]
149 fn companion_tag_shape() {
150 assert_eq!(
151 companion_sig_tag("sha256:abc123"),
152 Some("sha256-abc123.sig".to_string())
153 );
154 assert_eq!(companion_sig_tag("bare-tag"), None);
155 }
156
157 #[test]
158 fn extracts_layer_annotation() {
159 let m = serde_json::json!({
160 "layers": [{
161 "digest": "sha256:deadbeef",
162 "annotations": {
163 "dev.cosignproject.cosign/signature": "sig-b64"
164 }
165 }]
166 });
167 assert_eq!(
168 extract_signature_annotation(&m),
169 Some(("sha256:deadbeef".into(), "sig-b64".into()))
170 );
171 }
172
173 #[test]
174 fn parses_payload_referenced_digest() {
175 let p = br#"{"critical":{"image":{"docker-manifest-digest":"sha256:target"}}}"#;
176 assert_eq!(referenced_image_digest(p).as_deref(), Some("sha256:target"));
177 }
178}