summaryrefslogtreecommitdiffstats
path: root/src/server/setup.rs
blob: 023e6ed8c23219cfd7f1949f41658cfcca99be8d (plain) (blame)
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
//! First-time TLS identity generation for the server.

use std::path::{Path, PathBuf};
use std::process::Command;

#[derive(Debug)]
pub struct SetupReport {
    pub identity: PathBuf,
    pub key: PathBuf,
    pub cert: PathBuf,
}

/// Generate a self-signed RSA identity in `dir`.
///
/// Produces `identity.p12` (empty password, matching `native_tls`), `key.pem`,
/// and `cert.pem`. Refuses to overwrite an existing identity.
pub fn init_identity(dir: &Path, common_name: &str) -> Result<SetupReport, String> {
    let common_name = if common_name.trim().is_empty() {
        "localhost"
    } else {
        common_name.trim()
    };

    let identity = dir.join("identity.p12");
    let key = dir.join("key.pem");
    let cert = dir.join("cert.pem");

    let existing: Vec<String> = [&identity, &key, &cert]
        .into_iter()
        .filter(|path| path.exists())
        .map(|path| path.display().to_string())
        .collect();
    if !existing.is_empty() {
        return Err(format!(
            "Refusing to overwrite existing TLS files: {}\n\
             Remove them first if you really want a new identity.",
            existing.join(", ")
        ));
    }

    std::fs::create_dir_all(dir)
        .map_err(|error| format!("Could not create {}: {error}", dir.display()))?;

    let manual = manual_instructions(common_name);
    let subject = format!("/CN={common_name}");

    run_openssl(
        dir,
        &[
            "req", "-x509", "-newkey", "rsa:2048", "-keyout", "key.pem", "-out", "cert.pem",
            "-days", "3650", "-nodes", "-subj", &subject,
        ],
        "generate the certificate",
        &manual,
    )?;

    run_openssl(
        dir,
        &[
            "pkcs12",
            "-export",
            "-out",
            "identity.p12",
            "-inkey",
            "key.pem",
            "-in",
            "cert.pem",
            "-passout",
            "pass:",
            "-keypbe",
            "PBE-SHA1-3DES",
            "-certpbe",
            "PBE-SHA1-3DES",
            "-macalg",
            "sha1",
        ],
        "build identity.p12",
        &manual,
    )?;

    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;

        for path in [&key, &identity] {
            let metadata = std::fs::metadata(path)
                .map_err(|error| format!("Could not inspect {}: {error}", path.display()))?;
            let mut permissions = metadata.permissions();
            permissions.set_mode(0o600);
            std::fs::set_permissions(path, permissions)
                .map_err(|error| format!("Could not protect {}: {error}", path.display()))?;
        }
    }

    Ok(SetupReport {
        identity,
        key,
        cert,
    })
}

fn run_openssl(dir: &Path, args: &[&str], action: &str, manual: &str) -> Result<(), String> {
    let output = Command::new("openssl")
        .current_dir(dir)
        .args(args)
        .output()
        .map_err(|error| format!("Could not run openssl to {action}: {error}\n\n{manual}"))?;

    if output.status.success() {
        return Ok(());
    }

    let stderr = String::from_utf8_lossy(&output.stderr);
    Err(format!(
        "openssl failed to {action}:\n{}\n\n{manual}",
        stderr.trim()
    ))
}

fn manual_instructions(common_name: &str) -> String {
    format!(
        "Install openssl, or run these commands manually:\n\
         \x20 openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem \\\n\
         \x20   -days 3650 -nodes -subj \"/CN={common_name}\"\n\
         \x20 openssl pkcs12 -export -out identity.p12 -inkey key.pem -in cert.pem \\\n\
         \x20   -passout pass:"
    )
}