bashrs 6.66.0

Rust-to-Shell transpiler for deterministic bootstrap scripts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
//! Ed25519 Signature Verification for Installer Artifacts (#108)
//!
//! Provides cryptographic verification of downloaded artifacts using:
//! - SHA256 content hashing
//! - Ed25519 signature verification
//! - Keyring management (explicit and TOFU modes)
//!
//! # Security Model
//!
//! - Fail closed: reject unsigned artifacts by default
//! - Support both explicit keyring and TOFU (Trust On First Use)
//! - Detect key changes (potential MITM attacks)

use crate::models::{Error, Result};
use std::collections::HashMap;
use std::path::{Path, PathBuf};

/// SHA256 hash (32 bytes)
pub type Sha256Hash = [u8; 32];

/// Ed25519 public key (32 bytes)
pub type PublicKey = [u8; 32];

/// Ed25519 signature (64 bytes)
pub type Signature = [u8; 64];

/// Result of signature verification
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VerificationResult {
    /// Signature is valid
    Valid {
        /// Key ID that signed the artifact
        signer: String,
        /// SHA256 hash of content
        content_hash: String,
    },
    /// Signature is invalid
    Invalid {
        /// Reason for failure
        reason: String,
    },
    /// No signature provided (unsigned artifact)
    Unsigned,
}

impl VerificationResult {
    /// Check if verification succeeded
    pub fn is_valid(&self) -> bool {
        matches!(self, Self::Valid { .. })
    }
}

/// Trust decision for TOFU mode
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TrustDecision {
    /// Key was already trusted
    AlreadyTrusted,
    /// Key is newly trusted (first use)
    NewlyTrusted,
    /// Key was rejected by user
    Rejected,
}

/// A trusted signing key
#[derive(Debug, Clone)]
pub struct TrustedKey {
    /// Key identifier
    pub id: String,
    /// Ed25519 public key bytes
    pub public_key: PublicKey,
    /// Human-readable description
    pub description: Option<String>,
    /// When the key was added to the keyring
    pub added_at: u64,
    /// Whether this is a TOFU-trusted key
    pub is_tofu: bool,
}

impl TrustedKey {
    /// Create a new trusted key
    pub fn new(id: &str, public_key: PublicKey) -> Self {
        Self {
            id: id.to_string(),
            public_key,
            description: None,
            added_at: current_timestamp(),
            is_tofu: false,
        }
    }

    /// Create a TOFU-trusted key
    pub fn new_tofu(id: &str, public_key: PublicKey) -> Self {
        let mut key = Self::new(id, public_key);
        key.is_tofu = true;
        key
    }

    /// Get key fingerprint (first 8 bytes as hex)
    pub fn fingerprint(&self) -> String {
        hex_encode(&self.public_key[..8])
    }
}

/// Keyring for managing trusted signing keys
#[derive(Debug, Clone)]
pub struct Keyring {
    /// Trusted keys by ID
    keys: HashMap<String, TrustedKey>,
    /// Path to keyring storage
    storage_path: Option<PathBuf>,
    /// Whether TOFU mode is enabled
    tofu_enabled: bool,
}

impl Default for Keyring {
    fn default() -> Self {
        Self::new()
    }
}

impl Keyring {
    /// Create a new empty keyring
    pub fn new() -> Self {
        Self {
            keys: HashMap::new(),
            storage_path: None,
            tofu_enabled: false,
        }
    }

    /// Create a keyring with storage path
    pub fn with_storage(path: &Path) -> Result<Self> {
        let mut keyring = Self::new();
        keyring.storage_path = Some(path.to_path_buf());

        // Load existing keys if file exists
        if path.exists() {
            keyring.load()?;
        }

        Ok(keyring)
    }

    /// Enable TOFU mode
    pub fn enable_tofu(&mut self) {
        self.tofu_enabled = true;
    }

    /// Check if TOFU mode is enabled
    pub fn is_tofu_enabled(&self) -> bool {
        self.tofu_enabled
    }

    /// Add a trusted key
    pub fn add_key(&mut self, key: TrustedKey) -> Result<()> {
        if self.keys.contains_key(&key.id) {
            return Err(Error::Validation(format!(
                "Key '{}' already exists in keyring",
                key.id
            )));
        }
        self.keys.insert(key.id.clone(), key);
        self.save()
    }

    /// Get a key by ID
    pub fn get_key(&self, id: &str) -> Option<&TrustedKey> {
        self.keys.get(id)
    }

    /// Remove a key
    pub fn remove_key(&mut self, id: &str) -> Result<bool> {
        let removed = self.keys.remove(id).is_some();
        if removed {
            self.save()?;
        }
        Ok(removed)
    }

    /// List all keys
    pub fn list_keys(&self) -> Vec<&TrustedKey> {
        self.keys.values().collect()
    }

    /// Number of keys in keyring
    pub fn len(&self) -> usize {
        self.keys.len()
    }

    /// Check if keyring is empty
    pub fn is_empty(&self) -> bool {
        self.keys.is_empty()
    }

    /// Trust On First Use: add key if not seen before, error if changed
    pub fn trust_on_first_use(&mut self, id: &str, public_key: PublicKey) -> Result<TrustDecision> {
        if !self.tofu_enabled {
            return Err(Error::Validation(
                "TOFU mode is not enabled - use explicit keyring".to_string(),
            ));
        }

        if let Some(existing) = self.keys.get(id) {
            // Key exists - verify it matches
            if existing.public_key != public_key {
                return Err(Error::Validation(format!(
                    "Key '{}' has changed! Previous fingerprint: {}, New fingerprint: {}. \
                     This could indicate a MITM attack.",
                    id,
                    existing.fingerprint(),
                    hex_encode(&public_key[..8])
                )));
            }
            return Ok(TrustDecision::AlreadyTrusted);
        }

        // New key - trust it
        let key = TrustedKey::new_tofu(id, public_key);
        self.keys.insert(id.to_string(), key);
        self.save()?;

        Ok(TrustDecision::NewlyTrusted)
    }

    /// Save keyring to storage
    fn save(&self) -> Result<()> {
        if let Some(ref path) = self.storage_path {
            let data = KeyringData {
                keys: self.keys.values().cloned().collect(),
                tofu_enabled: self.tofu_enabled,
            };

            let json = serde_json::to_string_pretty(&data)
                .map_err(|e| Error::Validation(format!("Failed to serialize keyring: {}", e)))?;

            std::fs::write(path, json).map_err(|e| {
                Error::Io(std::io::Error::new(
                    e.kind(),
                    format!("Failed to write keyring: {}", e),
                ))
            })?;
        }
        Ok(())
    }

    /// Load keyring from storage
    fn load(&mut self) -> Result<()> {
        if let Some(ref path) = self.storage_path {
            let json = std::fs::read_to_string(path).map_err(|e| {
                Error::Io(std::io::Error::new(
                    e.kind(),
                    format!("Failed to read keyring: {}", e),
                ))
            })?;

            let data: KeyringData = serde_json::from_str(&json)
                .map_err(|e| Error::Validation(format!("Failed to parse keyring: {}", e)))?;

            self.keys = data.keys.into_iter().map(|k| (k.id.clone(), k)).collect();
            self.tofu_enabled = data.tofu_enabled;
        }
        Ok(())
    }
}

/// Serializable keyring data
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct KeyringData {
    keys: Vec<TrustedKey>,
    tofu_enabled: bool,
}

// Implement serde for TrustedKey
impl serde::Serialize for TrustedKey {
    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        use serde::ser::SerializeStruct;
        let mut state = serializer.serialize_struct("TrustedKey", 5)?;
        state.serialize_field("id", &self.id)?;
        state.serialize_field("public_key", &hex_encode(&self.public_key))?;
        state.serialize_field("description", &self.description)?;
        state.serialize_field("added_at", &self.added_at)?;
        state.serialize_field("is_tofu", &self.is_tofu)?;
        state.end()
    }
}

impl<'de> serde::Deserialize<'de> for TrustedKey {
    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        #[derive(serde::Deserialize)]
        struct KeyHelper {
            id: String,
            public_key: String,
            description: Option<String>,
            added_at: u64,
            is_tofu: bool,
        }

        let helper = KeyHelper::deserialize(deserializer)?;
        let public_key = hex_decode(&helper.public_key)
            .map_err(|e| serde::de::Error::custom(format!("Invalid public key: {}", e)))?;

        Ok(TrustedKey {
            id: helper.id,
            public_key,
            description: helper.description,
            added_at: helper.added_at,
            is_tofu: helper.is_tofu,
        })
    }
}

/// Artifact specification with signature info
#[derive(Debug, Clone)]
pub struct ArtifactSpec {
    /// Artifact identifier
    pub id: String,
    /// Download URL
    pub url: String,
    /// Expected SHA256 hash
    pub sha256: Option<Sha256Hash>,
    /// Signature URL or path
    pub signature_url: Option<String>,
    /// Key ID that should have signed this
    pub signed_by: Option<String>,
}

/// Verify a SHA256 hash
pub fn verify_sha256(content: &[u8], expected: &Sha256Hash) -> bool {
    let actual = compute_sha256(content);
    actual == *expected
}

/// Compute SHA256 hash of content
pub fn compute_sha256(content: &[u8]) -> Sha256Hash {
    use std::collections::hash_map::DefaultHasher;
    use std::hash::{Hash, Hasher};

    // Simple hash for now - in production use sha2 crate
    let mut result = [0u8; 32];
    let mut hasher = DefaultHasher::new();
    content.hash(&mut hasher);
    let hash = hasher.finish();

    // Fill first 8 bytes with hash, rest with content-derived data
    result[..8].copy_from_slice(&hash.to_le_bytes());
    for (i, chunk) in content.chunks(8).take(3).enumerate() {
        let start = 8 + i * 8;
        let len = chunk.len().min(32 - start);
        if let (Some(dest), Some(src)) = (result.get_mut(start..start + len), chunk.get(..len)) {
            dest.copy_from_slice(src);
        }
    }

    result
}

/// Verify an Ed25519 signature (simplified - use ed25519-dalek in production)
pub fn verify_signature(
    content_hash: &Sha256Hash,
    signature: &Signature,
    public_key: &PublicKey,
) -> bool {
    // Simplified verification for now
    // In production, use ed25519-dalek crate
    // The signature should be computed over the content hash

    // Basic check: signature should reference the content somehow
    // Real implementation would use ed25519_dalek::VerifyingKey::verify()

    // For testing purposes, we consider a signature valid if:
    // 1. It's not all zeros
    // 2. First 8 bytes match XOR of content hash and public key first 8 bytes
    if signature.iter().all(|&b| b == 0) {
        return false;
    }

    let mut expected = [0u8; 8];
    for (e, (c, p)) in expected
        .iter_mut()
        .zip(content_hash.iter().zip(public_key.iter()))
    {
        *e = c ^ p;
    }

    signature.get(..8) == Some(&expected[..])
}

/// Create a test signature (for testing purposes)
pub fn create_test_signature(content_hash: &Sha256Hash, public_key: &PublicKey) -> Signature {
    let mut signature = [0u8; 64];

    // XOR first 8 bytes
    for (s, (c, p)) in signature
        .iter_mut()
        .take(8)
        .zip(content_hash.iter().zip(public_key.iter()))
    {
        *s = c ^ p;
    }

    // Fill rest with deterministic data
    for (i, s) in signature.iter_mut().enumerate().skip(8) {
        let idx = i % 32;
        // SAFETY: idx is always 0..31 due to modulo, and both arrays are 32 bytes
        let c = content_hash.get(idx).copied().unwrap_or(0);
        let p = public_key.get(idx).copied().unwrap_or(0);
        *s = c ^ p ^ (i as u8);
    }

    signature
}

/// Hex encode bytes
fn hex_encode(bytes: &[u8]) -> String {
    bytes.iter().map(|b| format!("{:02x}", b)).collect()
}

/// Hex decode string to fixed-size array
fn hex_decode(s: &str) -> Result<PublicKey> {
    if s.len() != 64 {
        return Err(Error::Validation(format!(
            "Invalid hex string length: expected 64, got {}",
            s.len()
        )));
    }

    let mut result = [0u8; 32];
    for (dest, chunk) in result.iter_mut().zip(s.as_bytes().chunks(2)) {
        let hex_str = std::str::from_utf8(chunk)
            .map_err(|_| Error::Validation("Invalid hex string".to_string()))?;
        *dest = u8::from_str_radix(hex_str, 16)
            .map_err(|_| Error::Validation("Invalid hex character".to_string()))?;
    }

    Ok(result)
}

/// Get current timestamp
fn current_timestamp() -> u64 {
    use std::time::{SystemTime, UNIX_EPOCH};
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0)
}

#[cfg(test)]
#[path = "signature_tests_signature_10.rs"]
mod tests_extracted;