nabla_cli/enterprise/attestation/
attest_binary.rs1use axum::{
2 extract::{Multipart, State},
3 http::StatusCode,
4 response::Json,
5};
6use serde_json::{json, Value};
7use chrono::Utc;
8use sha2::{Sha256, Digest};
9use base64::{engine::general_purpose, Engine as _};
10use hex;
11use crate::{AppState, binary::analyze_binary};
12use ed25519_dalek::Signer;
14use serde::{Serialize, Deserialize};
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 _ => return Err((StatusCode::BAD_REQUEST, Json(ErrorResponse {
51 error: "missing_file".to_string(),
52 message: "Missing or empty binary".to_string(),
53 }))),
54 };
55
56 if signing_key_bytes.is_none() {
58 return Err((StatusCode::BAD_REQUEST, Json(ErrorResponse {
59 error: "signing_key_required".to_string(),
60 message: "Signing key is required for binary attestation. Please provide a valid signing key.".to_string(),
61 })));
62 }
63
64 let mut hasher = Sha256::new();
66 hasher.update(&file_bytes);
67 let hash = hasher.finalize();
68 let _encoded_hash = general_purpose::STANDARD.encode(&hash);
69
70 let analysis_struct = match analyze_binary(
72 file_name.as_deref().unwrap_or("uploaded-binary"),
73 &file_bytes,
74 &state.crypto_provider
75 ).await {
76 Ok(data) => data,
77 Err(err) => {
78 tracing::error!("Analysis failed: {:?}", err);
79 return Err((StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse {
80 error: "analysis_failed".to_string(),
81 message: "Analysis failed".to_string(),
82 })));
83 }
84 };
85
86 let analysis = match serde_json::to_value(&analysis_struct) {
88 Ok(val) => val,
89 Err(err) => {
90 tracing::error!("Serialization failed: {:?}", err);
91 return Err((StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse {
92 error: "serialization_failed".to_string(),
93 message: "Serialization failed".to_string(),
94 })));
95 }
96 };
97
98 let encoded_hash = hex::encode(&hash);
99
100 let attestation = json!({
101 "_type": "https://in-toto.io/Statement/v0.1",
102 "subject": [{
103 "name": file_name.unwrap_or_else(|| "uploaded-binary".into()),
104 "digest": {
105 "sha256": encoded_hash
106 }
107 }],
108 "predicateType": "https://nabla.sh/attestation/v0.1",
109 "predicate": {
110 "timestamp": Utc::now().to_rfc3339(),
111 "analysis": analysis
112 }
113 });
114
115 let signing_key = signing_key_bytes.unwrap();
117 match create_signed_attestation(&attestation, &signing_key) {
118 Ok(signed_attestation) => {
119 Ok(Json(signed_attestation))
120 }
121 Err(err) => {
122 tracing::error!("Signing failed: {:?}", err);
123 Err((StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse {
124 error: "signing_failed".to_string(),
125 message: "Signing failed".to_string(),
126 })))
127 }
128 }
129}
130
131fn create_signed_attestation(attestation: &serde_json::Value, signing_key: &[u8]) -> anyhow::Result<serde_json::Value> {
132 let key_pair = match parse_signing_key(signing_key) {
134 Ok(key) => key,
135 Err(_) => {
136 return Err(anyhow::anyhow!("Invalid signing key format"));
137 }
138 };
139
140 let attestation_bytes = serde_json::to_vec(attestation)?;
142 let signature = key_pair.try_sign(&attestation_bytes)
143 .map_err(|e| anyhow::anyhow!("Signing failed: {}", e))?;
144 let signature_b64 = general_purpose::STANDARD.encode(signature.to_bytes());
145
146 let mut signed_attestation = attestation.clone();
148 let signatures = json!([{
149 "keyid": "keyid123", "sig": signature_b64,
151 "cert": "certificate_here" }]);
153
154 if let Some(obj) = signed_attestation.as_object_mut() {
155 obj.insert("signatures".to_string(), signatures);
156 }
157
158 Ok(signed_attestation)
159}
160
161fn parse_signing_key(key_bytes: &[u8]) -> anyhow::Result<ed25519_dalek::SigningKey> {
162 let key_str = String::from_utf8_lossy(key_bytes);
164
165 let pem_content = if key_str.contains("-----BEGIN PRIVATE KEY-----") {
167 let lines: Vec<&str> = key_str.lines().collect();
168 let mut key_content = String::new();
169 let mut in_key = false;
170
171 for line in lines {
172 if line.contains("-----BEGIN PRIVATE KEY-----") {
173 in_key = true;
174 continue;
175 }
176 if line.contains("-----END PRIVATE KEY-----") {
177 break;
178 }
179 if in_key {
180 key_content.push_str(line);
181 }
182 }
183
184 general_purpose::STANDARD.decode(key_content.as_bytes())?
185 } else {
186 general_purpose::STANDARD.decode(key_bytes)?
188 };
189
190 if pem_content.len() != 32 {
192 return Err(anyhow::anyhow!("Invalid key length: expected 32 bytes, got {}", pem_content.len()));
193 }
194
195 let mut key_array = [0u8; 32];
197 key_array.copy_from_slice(&pem_content);
198
199 let signing_key = ed25519_dalek::SigningKey::from_bytes(&key_array);
201 Ok(signing_key)
202}