mod.rs (10365B)
1 use std::path::PathBuf; 2 use std::sync::Arc; 3 4 use async_nats::jetstream; 5 use async_nats::jetstream::AckKind; 6 use async_nats::jetstream::consumer::Consumer; 7 use async_nats::jetstream::consumer::pull::Config; 8 use c2pa::Reader; 9 use futures::StreamExt; 10 use simple_c2pa::{ 11 ApplicationInfo, Certificate, ContentCredentials, create_content_credentials_certificate, 12 create_root_certificate, ExifData, FileData, 13 }; 14 use tracing::{debug, error, info}; 15 16 const APP_NAME: &str = "MTRNord_Photography_Manager"; 17 const USERNAME: &str = "MTRNord"; 18 19 pub fn generate_certificates( 20 c2pa_folder: PathBuf, 21 ) -> color_eyre::Result<(Arc<Certificate>, Arc<Certificate>)> { 22 let c2pa_folder = c2pa_folder.join("certificates"); 23 // Create the folder if it doesn't exist 24 std::fs::create_dir_all(&c2pa_folder)?; 25 26 // Generate the certificates 27 let root_certificate = create_root_certificate(Some(USERNAME), None)?; 28 // Write the root certificate to a file 29 let root_certificate_data = root_certificate.get_certificate_bytes()?; 30 let root_private_key_data = root_certificate.get_private_key_bytes()?; 31 let root_certificate_path = c2pa_folder.join("root_certificate.crt"); 32 let root_private_key_path = c2pa_folder.join("root_private_key.pem"); 33 std::fs::write(&root_certificate_path, root_certificate_data)?; 34 std::fs::write(&root_private_key_path, root_private_key_data)?; 35 36 // Create the content credentials certificate 37 38 let content_credentials_certificate = create_content_credentials_certificate( 39 Some(root_certificate.clone()), 40 Some(USERNAME), 41 None, 42 )?; 43 44 // Write the content credentials certificate to a file 45 let content_credentials_certificate_data = 46 content_credentials_certificate.get_certificate_bytes()?; 47 let content_credentials_private_key_data = 48 content_credentials_certificate.get_private_key_bytes()?; 49 let content_credentials_certificate_path = 50 c2pa_folder.join("content_credentials_certificate.crt"); 51 let content_credentials_private_key_path = 52 c2pa_folder.join("content_credentials_private_key.pem"); 53 std::fs::write( 54 &content_credentials_certificate_path, 55 content_credentials_certificate_data, 56 )?; 57 std::fs::write( 58 &content_credentials_private_key_path, 59 content_credentials_private_key_data, 60 )?; 61 62 Ok((root_certificate, content_credentials_certificate)) 63 } 64 65 fn load_certificates( 66 c2pa_folder: PathBuf, 67 ) -> color_eyre::Result<(Arc<Certificate>, Arc<Certificate>)> { 68 let c2pa_folder = c2pa_folder.join("certificates"); 69 let root_certificate_path = c2pa_folder.join("root_certificate.crt"); 70 let root_private_key_path = c2pa_folder.join("root_private_key.pem"); 71 let content_credentials_certificate_path = 72 c2pa_folder.join("content_credentials_certificate.crt"); 73 let content_credentials_private_key_path = 74 c2pa_folder.join("content_credentials_private_key.pem"); 75 76 let root_certificate_data = std::fs::read(&root_certificate_path)?; 77 let root_private_key_data = std::fs::read(&root_private_key_path)?; 78 let content_credentials_certificate_data = 79 std::fs::read(&content_credentials_certificate_path)?; 80 let content_credentials_private_key_data = 81 std::fs::read(&content_credentials_private_key_path)?; 82 83 let root_certificate_filedata = FileData::new(None, Some(root_certificate_data), None); 84 let root_private_key_filedata = FileData::new(None, Some(root_private_key_data), None); 85 let content_credentials_certificate_filedata = 86 FileData::new(None, Some(content_credentials_certificate_data), None); 87 let content_credentials_private_key_filedata = 88 FileData::new(None, Some(content_credentials_private_key_data), None); 89 90 let root_certificate = 91 Certificate::new(root_certificate_filedata, root_private_key_filedata, None); 92 let content_credentials_certificate = Certificate::new( 93 content_credentials_certificate_filedata, 94 content_credentials_private_key_filedata, 95 Some(root_certificate.clone()), 96 ); 97 98 Ok((root_certificate, content_credentials_certificate)) 99 } 100 101 pub fn sign_image( 102 export_path: PathBuf, 103 image_data: Arc<FileData>, 104 exif_data: Option<ExifData>, 105 content_credentials_certificate: Arc<Certificate>, 106 ) -> color_eyre::Result<()> { 107 let app_info = ApplicationInfo::new(APP_NAME.to_string(), "0.1.0".to_string(), None); 108 let cc = ContentCredentials::new(content_credentials_certificate, image_data, Some(app_info)); 109 110 let gpg_fingerprint = "9768 CA63 F48D 3609 8567 A59D AEDE 3887 B155 1783"; 111 let gpg_key = "AEDE3887B1551783"; 112 113 cc.add_created_assertion()?; 114 cc.add_restricted_ai_training_assertions()?; 115 cc.add_instagram_assertion(USERNAME, USERNAME)?; 116 cc.add_website_assertion("https://mtrnord.blog".to_string())?; 117 cc.add_pgp_assertion(gpg_fingerprint, gpg_key)?; 118 119 if let Some(exif_data) = exif_data { 120 cc.add_exif_assertion(exif_data)?; 121 } 122 123 cc.embed_manifest(Some(export_path.clone()))?; 124 125 // Bugfix: Delete any residual file with the same name and extension in /tmp 126 let file_name = export_path.file_name().unwrap().to_str().unwrap(); 127 let tmp_file = std::env::temp_dir().join(file_name); 128 if tmp_file.exists() { 129 std::fs::remove_file(tmp_file)?; 130 } 131 132 Ok(()) 133 } 134 135 #[derive(Debug, Clone)] 136 pub(crate) struct C2PASigner { 137 // Export folder to move files to 138 export_folder: PathBuf, 139 // C2PA folder to store certificates 140 c2pa_folder: PathBuf, 141 // Nats consumer to work with 142 consumer: Consumer<Config>, 143 } 144 145 impl C2PASigner { 146 pub async fn new( 147 export_folder: PathBuf, 148 c2pa_folder: PathBuf, 149 import_stream: jetstream::stream::Stream, 150 ) -> color_eyre::Result<Self> { 151 let consumer = import_stream 152 .create_consumer(Config { 153 durable_name: Some("processor-2".to_string()), 154 filter_subject: "import.sign_request".to_string(), 155 ..Default::default() 156 }) 157 .await?; 158 159 Ok(Self { 160 export_folder, 161 c2pa_folder, 162 consumer, 163 }) 164 } 165 166 pub async fn consumer(&self) -> color_eyre::Result<()> { 167 let mut messages = self.consumer.messages().await?; 168 info!("Signing consumer started"); 169 let (_, content_credentials_certificate) = load_certificates(self.c2pa_folder.clone())?; 170 while let Some(msg) = messages.next().await { 171 match msg { 172 Ok(msg) => { 173 let event: super::jetstream_events::C2PASignRequest = 174 bincode::deserialize(&msg.payload).unwrap(); 175 debug!("Received message: {:#?}", event); 176 177 // Initial ack to notify that we received the message 178 msg.ack_with(AckKind::Progress).await.unwrap(); 179 180 // Check if the file still is in the export 181 182 // First get absolute export folder path 183 let export_folder = self.export_folder.canonicalize()?; 184 185 // Check if the file is still there 186 if !event.path.exists() { 187 error!("[C2PA Signer] file does not exist: {:?}", event.path); 188 // We also acknowledge the message to remove it from the queue 189 msg.ack_with(AckKind::Term).await.unwrap(); 190 continue; 191 } 192 193 // Then check if the file is in the import folder 194 if !event.path.starts_with(export_folder.clone()) { 195 error!( 196 "[C2PA Signer] file is not in the export_folder folder: {:?}", 197 event.path 198 ); 199 200 // We also acknowledge the message to remove it from the queue 201 msg.ack_with(AckKind::Term).await.unwrap(); 202 continue; 203 } 204 205 // Ignore if there is a c2pa file with the same name next to it 206 let c2pa_path = event.path.with_extension("c2pa"); 207 if c2pa_path.exists() { 208 error!("[C2PA Signer] c2pa file already exists: {:?}", c2pa_path); 209 // We also acknowledge the message to remove it from the queue 210 msg.ack_with(AckKind::Term).await.unwrap(); 211 continue; 212 } 213 214 // Another progress ack to notify that we are processing the file 215 msg.ack_with(AckKind::Progress).await.unwrap(); 216 217 let image_path = &event.path; 218 let cloned_path = image_path.clone(); 219 let filename = cloned_path.file_name().unwrap().to_str().unwrap(); 220 221 let exif_data = event.exif.map(|exif| exif.into()); 222 223 if let Ok(reader) = Reader::from_file(image_path) { 224 if let Some(manifest) = reader.active_manifest() { 225 if manifest.claim_generator.contains(APP_NAME) { 226 error!("[C2PA Signer] file already signed by us: {:?}", event.path); 227 // We also acknowledge the message to remove it from the queue 228 msg.ack_with(AckKind::Term).await.unwrap(); 229 continue; 230 } 231 } 232 } 233 234 sign_image( 235 image_path.clone(), 236 FileData::new(Some(image_path.clone()), None, Some(filename.to_string())), 237 exif_data, 238 content_credentials_certificate.clone(), 239 )?; 240 241 // Acknowledge the message to remove it from the queue 242 msg.ack_with(AckKind::Ack).await.unwrap(); 243 tokio::time::sleep(std::time::Duration::from_millis(100)).await; 244 } 245 Err(e) => { 246 error!("[C2PA Signer] error receiving message: {:?}", e); 247 } 248 } 249 } 250 error!("[C2PA Signer] Consumer stopped"); 251 Ok(()) 252 } 253 }