Skip to main content

atlas_cli/storage/
rekor.rs

1use 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
15/// How the certificate is obtained for Rekor submissions.
16pub enum CertificateSource {
17    /// Load from a PEM file on disk.
18    File(PathBuf),
19    /// Obtain from Fulcio using an OIDC identity token.
20    /// The DSSE envelope will be signed with an ephemeral ECDSA P-256 key.
21    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    /// Submit a pre-built DSSE envelope to Rekor with a certificate loaded from file.
97    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    /// Retrieve a log entry from Rekor by its UUID.
107    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    /// Search Rekor entries by SHA-256 hash (hex-encoded).
114    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    /// Verify a local manifest against a Rekor transparency log entry.
121    ///
122    /// Checks:
123    /// 1. Payload hash matches the SHA-256 of the provided manifest bytes
124    /// 2. DSSE signature is valid against the certificate stored in the entry
125    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
259/// Sign a DSSE envelope using an ephemeral ECDSA P-256 key and obtain a Fulcio certificate.
260async 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}