nabla_cli/enterprise/attestation/
attest_binary.rs

1use 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};
12// Removed unused imports
13use 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    // 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        _ => 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    // Check if signing key is provided (required for attestation)
57    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    // Compute SHA256 digest for reference
65    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    // Run your internal analysis logic
71    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    // Serialize to JSON Value
87    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    // Create signed attestation with the provided key
116    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    // Parse the signing key (PEM format)
133    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    // Create the signature
141    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    // Create the signed attestation
147    let mut signed_attestation = attestation.clone();
148    let signatures = json!([{
149        "keyid": "keyid123", // In a real implementation, this would be derived from the key
150        "sig": signature_b64,
151        "cert": "certificate_here" // In a real implementation, this would be the actual certificate
152    }]);
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    // Parse PEM format
163    let key_str = String::from_utf8_lossy(key_bytes);
164    
165    // Remove PEM headers and decode base64
166    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        // Assume it's already base64 encoded
187        general_purpose::STANDARD.decode(key_bytes)?
188    };
189
190    // Ensure we have exactly 32 bytes for Ed25519
191    if pem_content.len() != 32 {
192        return Err(anyhow::anyhow!("Invalid key length: expected 32 bytes, got {}", pem_content.len()));
193    }
194
195    // Convert Vec<u8> to [u8; 32]
196    let mut key_array = [0u8; 32];
197    key_array.copy_from_slice(&pem_content);
198
199    // Create signing key from bytes
200    let signing_key = ed25519_dalek::SigningKey::from_bytes(&key_array);
201    Ok(signing_key)
202}