nabla_cli/enterprise/attestation/
attest_binary.rs

1use 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};
12// Removed unused imports
13use 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    // For now, assume attestation is available (middleware should handle this)
27    // In a full implementation, you'd extract features from request extensions
28
29    // Extract binary file from multipart
30    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    // Check if signing key is provided (required for attestation)
62    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    // Compute SHA256 digest for reference
70    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    // Run your internal analysis logic
76    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    // Serialize to JSON Value
96    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    // Create signed attestation with the provided key
128    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    // Parse the signing key (PEM format)
149    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    // Create the signature
157    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    // Create the signed attestation
164    let mut signed_attestation = attestation.clone();
165    let signatures = json!([{
166        "keyid": "keyid123", // In a real implementation, this would be derived from the key
167        "sig": signature_b64,
168        "cert": "certificate_here" // In a real implementation, this would be the actual certificate
169    }]);
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    // Parse PEM format
180    let key_str = String::from_utf8_lossy(key_bytes);
181
182    // Remove PEM headers and decode base64
183    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        // Assume it's already base64 encoded
204        general_purpose::STANDARD.decode(key_bytes)?
205    };
206
207    // Ensure we have exactly 32 bytes for Ed25519
208    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    // Convert Vec<u8> to [u8; 32]
216    let mut key_array = [0u8; 32];
217    key_array.copy_from_slice(&pem_content);
218
219    // Create signing key from bytes
220    let signing_key = ed25519_dalek::SigningKey::from_bytes(&key_array);
221    Ok(signing_key)
222}