nabla_cli/enterprise/attestation/
attest_binary.rs1use crate::{AppState, binary::analyze_binary};
2use axum::{
3 extract::{Multipart, State},
4 http::StatusCode,
5 response::Json,
6};
7use base64::{Engine as _, engine::general_purpose};
8use chrono::Utc;
9use hex;
10use serde_json::{Value, json};
11use sha2::{Digest, Sha256};
12use ed25519_dalek::Signer;
14use serde::{Deserialize, Serialize};
15
16#[derive(Serialize, Deserialize)]
17pub struct ErrorResponse {
18 pub error: String,
19 pub message: String,
20}
21
22pub async fn attest_binary(
23 State(_state): State<AppState>,
24 mut multipart: Multipart,
25) -> Result<Json<Value>, (StatusCode, Json<ErrorResponse>)> {
26 let mut file_bytes = None;
31 let mut file_name = None;
32 let mut signing_key_bytes = None;
33
34 while let Some(field) = multipart.next_field().await.unwrap_or(None) {
35 let name = field.name().unwrap_or("").to_string();
36 match name.as_str() {
37 "file" => {
38 file_name = field.file_name().map(str::to_string);
39 file_bytes = Some(field.bytes().await.unwrap_or_default());
40 }
41 "signing_key" => {
42 signing_key_bytes = Some(field.bytes().await.unwrap_or_default());
43 }
44 _ => {}
45 }
46 }
47
48 let file_bytes = match file_bytes {
49 Some(bytes) if !bytes.is_empty() => bytes,
50 _ => {
51 return Err((
52 StatusCode::BAD_REQUEST,
53 Json(ErrorResponse {
54 error: "missing_file".to_string(),
55 message: "Missing or empty binary".to_string(),
56 }),
57 ));
58 }
59 };
60
61 if signing_key_bytes.is_none() {
63 return Err((StatusCode::BAD_REQUEST, Json(ErrorResponse {
64 error: "signing_key_required".to_string(),
65 message: "Signing key is required for binary attestation. Please provide a valid signing key.".to_string(),
66 })));
67 }
68
69 let mut hasher = Sha256::new();
71 hasher.update(&file_bytes);
72 let hash = hasher.finalize();
73 let _encoded_hash = general_purpose::STANDARD.encode(&hash);
74
75 let analysis_struct = match analyze_binary(
77 file_name.as_deref().unwrap_or("uploaded-binary"),
78 &file_bytes,
79 )
80 .await
81 {
82 Ok(data) => data,
83 Err(err) => {
84 tracing::error!("Analysis failed: {:?}", err);
85 return Err((
86 StatusCode::INTERNAL_SERVER_ERROR,
87 Json(ErrorResponse {
88 error: "analysis_failed".to_string(),
89 message: "Analysis failed".to_string(),
90 }),
91 ));
92 }
93 };
94
95 let analysis = match serde_json::to_value(&analysis_struct) {
97 Ok(val) => val,
98 Err(err) => {
99 tracing::error!("Serialization failed: {:?}", err);
100 return Err((
101 StatusCode::INTERNAL_SERVER_ERROR,
102 Json(ErrorResponse {
103 error: "serialization_failed".to_string(),
104 message: "Serialization failed".to_string(),
105 }),
106 ));
107 }
108 };
109
110 let encoded_hash = hex::encode(&hash);
111
112 let attestation = json!({
113 "_type": "https://in-toto.io/Statement/v0.1",
114 "subject": [{
115 "name": file_name.unwrap_or_else(|| "uploaded-binary".into()),
116 "digest": {
117 "sha256": encoded_hash
118 }
119 }],
120 "predicateType": "https://www.usenabla.com/attestation/v0.1",
121 "predicate": {
122 "timestamp": Utc::now().to_rfc3339(),
123 "analysis": analysis
124 }
125 });
126
127 let signing_key = signing_key_bytes.unwrap();
129 match create_signed_attestation(&attestation, &signing_key) {
130 Ok(signed_attestation) => Ok(Json(signed_attestation)),
131 Err(err) => {
132 tracing::error!("Signing failed: {:?}", err);
133 Err((
134 StatusCode::INTERNAL_SERVER_ERROR,
135 Json(ErrorResponse {
136 error: "signing_failed".to_string(),
137 message: "Signing failed".to_string(),
138 }),
139 ))
140 }
141 }
142}
143
144fn create_signed_attestation(
145 attestation: &serde_json::Value,
146 signing_key: &[u8],
147) -> anyhow::Result<serde_json::Value> {
148 let key_pair = match parse_signing_key(signing_key) {
150 Ok(key) => key,
151 Err(_) => {
152 return Err(anyhow::anyhow!("Invalid signing key format"));
153 }
154 };
155
156 let attestation_bytes = serde_json::to_vec(attestation)?;
158 let signature = key_pair
159 .try_sign(&attestation_bytes)
160 .map_err(|e| anyhow::anyhow!("Signing failed: {}", e))?;
161 let signature_b64 = general_purpose::STANDARD.encode(signature.to_bytes());
162
163 let mut signed_attestation = attestation.clone();
165 let signatures = json!([{
166 "keyid": "keyid123", "sig": signature_b64,
168 "cert": "certificate_here" }]);
170
171 if let Some(obj) = signed_attestation.as_object_mut() {
172 obj.insert("signatures".to_string(), signatures);
173 }
174
175 Ok(signed_attestation)
176}
177
178fn parse_signing_key(key_bytes: &[u8]) -> anyhow::Result<ed25519_dalek::SigningKey> {
179 let key_str = String::from_utf8_lossy(key_bytes);
181
182 let pem_content = if key_str.contains("-----BEGIN PRIVATE KEY-----") {
184 let lines: Vec<&str> = key_str.lines().collect();
185 let mut key_content = String::new();
186 let mut in_key = false;
187
188 for line in lines {
189 if line.contains("-----BEGIN PRIVATE KEY-----") {
190 in_key = true;
191 continue;
192 }
193 if line.contains("-----END PRIVATE KEY-----") {
194 break;
195 }
196 if in_key {
197 key_content.push_str(line);
198 }
199 }
200
201 general_purpose::STANDARD.decode(key_content.as_bytes())?
202 } else {
203 general_purpose::STANDARD.decode(key_bytes)?
205 };
206
207 if pem_content.len() != 32 {
209 return Err(anyhow::anyhow!(
210 "Invalid key length: expected 32 bytes, got {}",
211 pem_content.len()
212 ));
213 }
214
215 let mut key_array = [0u8; 32];
217 key_array.copy_from_slice(&pem_content);
218
219 let signing_key = ed25519_dalek::SigningKey::from_bytes(&key_array);
221 Ok(signing_key)
222}