atlas_cli/storage/
rekor.rs1use crate::error::{Error, Result};
2use crate::in_toto::dsse::Envelope;
3use crate::signing::signable::Signable;
4use crate::storage::traits::{ManifestMetadata, StorageBackend};
5use atlas_c2pa_lib::cose::HashAlgorithm;
6use atlas_c2pa_lib::manifest::Manifest;
7use sha2::{Digest, Sha256};
8use sigstore_rekor::body::RekorEntryBody;
9use sigstore_rekor::{DsseEntry, LogEntry, RekorClient};
10use sigstore_types::DerCertificate;
11use std::path::{Path, PathBuf};
12
13const MANIFEST_PAYLOAD_TYPE: &str = "application/vnd.atlas-cli.manifest+json";
14
15pub enum CertificateSource {
17 File(PathBuf),
19 Fulcio(String),
22}
23
24pub struct RekorStorage {
25 client: RekorClient,
26 runtime: tokio::runtime::Runtime,
27 base_url: String,
28 key_path: Option<PathBuf>,
29 cert_source: Option<CertificateSource>,
30}
31
32impl RekorStorage {
33 pub fn new() -> Result<Self> {
34 Self::new_with_url("https://rekor.sigstore.dev".to_string())
35 }
36
37 pub fn new_with_url(url: String) -> Result<Self> {
38 let runtime = tokio::runtime::Runtime::new()
39 .map_err(|e| Error::Storage(format!("Failed to create async runtime: {e}")))?;
40 let client = RekorClient::new(&url);
41 Ok(RekorStorage {
42 client,
43 runtime,
44 base_url: url,
45 key_path: None,
46 cert_source: None,
47 })
48 }
49
50 pub fn with_key(mut self, key_path: Option<PathBuf>) -> Self {
51 self.key_path = key_path;
52 self
53 }
54
55 pub fn with_cert(mut self, cert_path: Option<PathBuf>) -> Self {
56 if let Some(path) = cert_path {
57 self.cert_source = Some(CertificateSource::File(path));
58 }
59 self
60 }
61
62 pub fn with_fulcio(mut self, oidc_token: String) -> Self {
63 self.cert_source = Some(CertificateSource::Fulcio(oidc_token));
64 self
65 }
66
67 fn resolve_cert_and_sign_envelope(
68 &self,
69 manifest_json: &[u8],
70 ) -> Result<(Envelope, DerCertificate)> {
71 match &self.cert_source {
72 Some(CertificateSource::File(cert_path)) => {
73 let key_path = self.key_path.as_ref().ok_or_else(|| {
74 Error::Storage(
75 "Signing key required with --cert. Use --key to specify a private key."
76 .to_string(),
77 )
78 })?;
79 let cert = load_cert_from_file(cert_path)?;
80 let mut envelope =
81 Envelope::new(&manifest_json.to_vec(), MANIFEST_PAYLOAD_TYPE.to_string());
82 envelope.sign(key_path.clone(), HashAlgorithm::Sha256)?;
83 Ok((envelope, cert))
84 }
85 Some(CertificateSource::Fulcio(oidc_token)) => self
86 .runtime
87 .block_on(sign_with_fulcio(manifest_json, oidc_token)),
88 None => Err(Error::Storage(
89 "Certificate required for Rekor storage. \
90 Use --cert <path> or --fulcio --oidc-token <token>."
91 .to_string(),
92 )),
93 }
94 }
95
96 pub fn store_dsse_envelope(&self, envelope: &Envelope, cert_path: &Path) -> Result<LogEntry> {
98 let cert = load_cert_from_file(cert_path)?;
99 let sigstore_dsse = envelope.to_sigstore_dsse();
100 let entry = DsseEntry::new(&sigstore_dsse, &cert);
101 self.runtime
102 .block_on(self.client.create_dsse_entry(entry))
103 .map_err(|e| Error::Storage(format!("Rekor submission failed: {e}")))
104 }
105
106 pub fn get_rekor_entry(&self, uuid: &str) -> Result<LogEntry> {
108 self.runtime
109 .block_on(self.client.get_entry_by_uuid(uuid))
110 .map_err(|e| Error::Storage(format!("Rekor retrieval failed: {e}")))
111 }
112
113 pub fn search_by_hash(&self, sha256_hex: &str) -> Result<Vec<String>> {
115 self.runtime
116 .block_on(self.client.search_by_hash(sha256_hex))
117 .map_err(|e| Error::Storage(format!("Rekor search failed: {e}")))
118 }
119
120 pub fn verify_manifest(&self, manifest_bytes: &[u8], uuid: &str) -> Result<RekorVerifyResult> {
126 let log_entry = self.get_rekor_entry(uuid)?;
127 let body_b64 = log_entry.body.to_base64();
128
129 let body = RekorEntryBody::from_base64_json(&body_b64, "dsse", "0.0.1")
130 .map_err(|e| Error::Storage(format!("Failed to parse Rekor entry body: {e}")))?;
131
132 let (payload_hash_hex, signatures, cert_der) = match body {
133 RekorEntryBody::DsseV001(b) => {
134 let hash = b.spec.payload_hash.value;
135 let sigs = b.spec.signatures;
136 let cert = sigs
137 .first()
138 .ok_or_else(|| Error::Storage("No signatures in Rekor entry".to_string()))?
139 .to_certificate()
140 .map_err(|e| {
141 Error::Storage(format!("Failed to parse certificate from entry: {e}"))
142 })?;
143 (hash, sigs, cert)
144 }
145 _ => {
146 return Err(Error::Storage(
147 "Unsupported Rekor entry type for verification. Expected DSSE v0.0.1."
148 .to_string(),
149 ));
150 }
151 };
152
153 let local_hash = hex::encode(Sha256::digest(manifest_bytes));
154 let payload_hash_match = local_hash == payload_hash_hex;
155
156 let signature_valid = verify_dsse_signature(manifest_bytes, &signatures, &cert_der);
157
158 let cert_info = sigstore_crypto::parse_certificate_info(cert_der.as_bytes()).ok();
159 let signer_identity = cert_info.as_ref().and_then(|c| c.identity.clone());
160
161 Ok(RekorVerifyResult {
162 payload_hash_match,
163 signature_valid,
164 log_index: log_entry.log_index,
165 integrated_time: log_entry.integrated_time,
166 signer_identity,
167 })
168 }
169}
170
171impl StorageBackend for RekorStorage {
172 fn get_base_uri(&self) -> String {
173 self.base_url.clone()
174 }
175
176 fn store_manifest(&self, manifest: &Manifest) -> Result<String> {
177 let manifest_json = serde_json::to_vec(manifest)
178 .map_err(|e| Error::Serialization(format!("Failed to serialize manifest: {e}")))?;
179
180 let (envelope, cert) = self.resolve_cert_and_sign_envelope(&manifest_json)?;
181 let sigstore_dsse = envelope.to_sigstore_dsse();
182 let entry = DsseEntry::new(&sigstore_dsse, &cert);
183 let log_entry = self
184 .runtime
185 .block_on(self.client.create_dsse_entry(entry))
186 .map_err(|e| Error::Storage(format!("Rekor submission failed: {e}")))?;
187 Ok(log_entry.uuid.to_string())
188 }
189
190 fn retrieve_manifest(&self, _id: &str) -> Result<Manifest> {
191 Err(Error::Storage(
192 "Rekor is a transparency log that stores hashes, not full content. \
193 Use get_rekor_entry() to retrieve log entry metadata."
194 .to_string(),
195 ))
196 }
197
198 fn list_manifests(&self) -> Result<Vec<ManifestMetadata>> {
199 Err(Error::Storage(
200 "Rekor does not support listing all entries. \
201 Use search_by_hash() to find entries by artifact hash."
202 .to_string(),
203 ))
204 }
205
206 fn delete_manifest(&self, _id: &str) -> Result<()> {
207 Err(Error::Storage(
208 "Rekor is an immutable transparency log. Entries cannot be deleted.".to_string(),
209 ))
210 }
211
212 fn as_any(&self) -> &dyn std::any::Any {
213 self
214 }
215}
216
217pub struct RekorVerifyResult {
218 pub payload_hash_match: bool,
219 pub signature_valid: bool,
220 pub log_index: i64,
221 pub integrated_time: i64,
222 pub signer_identity: Option<String>,
223}
224
225fn verify_dsse_signature(
226 payload: &[u8],
227 signatures: &[sigstore_rekor::body::DsseV001Signature],
228 cert_der: &DerCertificate,
229) -> bool {
230 use crate::in_toto::dsse::pae;
231
232 let cert_info = match sigstore_crypto::parse_certificate_info(cert_der.as_bytes()) {
233 Ok(info) => info,
234 Err(_) => return false,
235 };
236
237 let vk = match sigstore_crypto::VerificationKey::from_spki(
238 &cert_info.public_key,
239 cert_info.signing_scheme,
240 ) {
241 Ok(vk) => vk,
242 Err(_) => return false,
243 };
244
245 let data_to_verify = pae(MANIFEST_PAYLOAD_TYPE, payload);
246
247 signatures
248 .iter()
249 .any(|sig| vk.verify(&data_to_verify, &sig.signature).is_ok())
250}
251
252fn load_cert_from_file(cert_path: &Path) -> Result<DerCertificate> {
253 let pem_data = std::fs::read_to_string(cert_path)
254 .map_err(|e| Error::Signing(format!("Failed to read certificate file: {e}")))?;
255 DerCertificate::from_pem(&pem_data)
256 .map_err(|e| Error::Signing(format!("Failed to parse certificate PEM: {e}")))
257}
258
259async fn sign_with_fulcio(payload: &[u8], oidc_token: &str) -> Result<(Envelope, DerCertificate)> {
261 use crate::in_toto::dsse::pae;
262 use sigstore_crypto::KeyPair;
263 use sigstore_fulcio::FulcioClient;
264 use sigstore_oidc::IdentityToken;
265 use sigstore_types::SignatureBytes;
266
267 let token = IdentityToken::from_jwt(oidc_token)
268 .map_err(|e| Error::Signing(format!("Invalid OIDC token: {e}")))?;
269
270 let key_pair = KeyPair::generate_ecdsa_p256()
271 .map_err(|e| Error::Signing(format!("Failed to generate ephemeral key: {e}")))?;
272
273 let fulcio = FulcioClient::public();
274 let cert_response = fulcio
275 .create_signing_certificate(&token, &key_pair)
276 .await
277 .map_err(|e| Error::Signing(format!("Fulcio certificate request failed: {e}")))?;
278
279 let cert = cert_response
280 .leaf_certificate()
281 .map_err(|e| Error::Signing(format!("Failed to extract Fulcio certificate: {e}")))?;
282
283 let payload_type = MANIFEST_PAYLOAD_TYPE.to_string();
284 let data_to_sign = pae(&payload_type, payload);
285 let sig: SignatureBytes = key_pair
286 .sign(&data_to_sign)
287 .map_err(|e| Error::Signing(format!("Ephemeral key signing failed: {e}")))?;
288
289 let mut envelope = Envelope::new(&payload.to_vec(), payload_type);
290 envelope.add_signature(sig.as_bytes().to_vec(), "".to_string())?;
291
292 Ok((envelope, cert))
293}