reach_attestant/
commands.rs

1// SPDX-FileCopyrightText: 2023-2025 eaon <eaon@posteo.net>
2// SPDX-License-Identifier: EUPL-1.2
3
4use std::{borrow::Cow, env, fs, fs::File, io, io::IsTerminal, path::PathBuf, process::exit};
5
6use rand_core::CryptoRngCore;
7
8use reach_aliases::*;
9use reach_core::{
10    RandomFromRng,
11    storage::{ReachablePublicKeyRing, Storable, UnsignedReachableVerifyingKeys},
12    wire::{AttestantVerifyingKeys, ReachableVerifyingKeys, Salts},
13};
14use reach_passphrase::{WORDLIST_EN_V0, generate_passphrase};
15use reach_signatures::{Sign, Verifiable};
16use reach_visual_key_identity::visual_key_identity;
17
18use crate::{
19    cli_utils::*,
20    macros::{shared_keys_random_from_rng, sign_and_store_shared_keys},
21    memory::{
22        self, AttestantSigningKeys, UnsignedSalts, UnsignedSharedPublicKeys,
23        UnsignedSharedSecretKeys, UnsignedSharedSigningKeys, UnsignedSharedVerifyingKeys,
24    },
25    net::authenticate,
26    storage,
27};
28
29pub fn generate(
30    profile_name: &str,
31    profile: &Option<String>,
32    force: bool,
33    csprng: &mut impl CryptoRngCore,
34) {
35    if let (false, Ok(locked_signing_key_path)) =
36        (force, storage::file_path(profile_name, None, "lask", false))
37    {
38        (!locked_signing_key_path.exists()).may_bail_with(
39            &format!(
40                "A locked signing key for '{profile_name}' already exists! {}",
41                "To overwrite it use --force"
42            ),
43            false,
44        );
45    }
46
47    let pac_salt = ThreeTwo::random_from_rng(csprng);
48    let key_salt = ThreeTwo::random_from_rng(csprng);
49
50    // generate passphrase
51    let passphrase = generate_passphrase(WORDLIST_EN_V0, &pac_salt, csprng);
52    print!("\u{1f537} Your generated passphrase");
53    if let Some(profile_name) = profile {
54        print!(" for profile '{profile_name}'");
55    }
56    println!(":\n");
57    print_passphrase(&passphrase);
58
59    // generate signing keys
60    let signing_keys = AttestantSigningKeys::random_from_rng(csprng);
61    let verifying_keys = signing_keys.verifying_keys();
62
63    println!("\u{2712}\u{fe0f} Successfully generated attestant signing keys");
64
65    let key = derive_unlocking_key(passphrase, &pac_salt, &key_salt, true);
66    let augmentation = [pac_salt, key_salt].concat();
67    // lock it with generated passphrase
68    memory::lock_and_store(&signing_keys, &key, augmentation, profile_name, csprng)
69        .may_bail_with("Locking and storing your signing keys has failed.", true);
70    println!("\u{1f512} Locked attestant signing keys with your generated passphrase.\n");
71
72    let verifying_keys_path = verifying_keys_path(profile_name, true);
73
74    verifying_keys
75        .store(&verifying_keys_path)
76        .may_bail_with("Couldn't store attestant verifying keys.", true);
77
78    let unsigned_salts = UnsignedSalts::random_from_rng(csprng);
79    let reach_salts = signing_keys.sign(unsigned_salts, csprng);
80
81    let reach_salts_path = storage::file_path(profile_name, None, "slt", true)
82        .may_bail_with("Couldn't determine path for salts.", true);
83
84    reach_salts
85        .store(&reach_salts_path)
86        .may_bail_with("Couldn't store salts.", true);
87
88    let (shared_signing_keys, shared_verifying_keys) = shared_keys_random_from_rng!(
89        signing_keys,
90        UnsignedSharedSigningKeys,
91        UnsignedSharedVerifyingKeys,
92        csprng,
93    );
94    sign_and_store_shared_keys!(
95        (shared_signing_keys, "signing", "ssik"),
96        (shared_verifying_keys, "verifying", "svk"),
97        profile_name,
98    );
99
100    let (shared_secret_keys, shared_public_keys) = shared_keys_random_from_rng!(
101        signing_keys,
102        UnsignedSharedSecretKeys,
103        UnsignedSharedPublicKeys,
104        csprng,
105    );
106    sign_and_store_shared_keys!(
107        (shared_secret_keys, "secret", "ssek"),
108        (shared_public_keys, "public", "spk"),
109        profile_name,
110    );
111
112    let identity = visual_key_identity(&verifying_keys, &reach_salts.attestant_identity)
113        .may_bail_with(
114            "Couldn't synthesize visual identity for your verifying keys.",
115            true,
116        );
117
118    print!("\u{1f6c2} Your attestant verifying keys' visual identity:");
119    println!("\n\n{identity}\n");
120
121    print!("\u{2139}\u{fe0f} You can publish this visual identity on your website to ");
122    println!("allow users to verify that they are communicating with the right folks.");
123}
124
125pub fn list_profiles() {
126    let config_path = storage::config_path(false).may_bail(false);
127
128    let dir_entries = config_path.read_dir().may_bail(false);
129
130    dir_entries.for_each(|f| {
131        let dir_entry = f.may_bail(false);
132        let os_file_name = dir_entry.file_name();
133        let file_name = os_file_name.to_string_lossy();
134        if file_name.ends_with(".lask") {
135            println!("{}", file_name.split(".").next().expect("Infallible"));
136        };
137    });
138}
139
140pub fn list_signed_verifying_keys() {
141    // list {alias} part of {data}/{profile}/{alias}.vfk
142    todo!();
143}
144
145pub fn offboard() {
146    // As with Onboard, may be used in an airgapped environment
147    todo!();
148}
149
150// May be used in an airgapped manner, where it onboards keys signed by a different machine.
151pub async fn onboard(
152    profile_name: &str,
153    profile: &Option<String>,
154    reachable_verifying_keys_public_key_rings: Vec<(String, String)>,
155) {
156    let url_path = url_path(profile_name, false);
157    let url = format!(
158        "{}/reach",
159        fs::read_to_string(&url_path)
160            .map_err(|_| format!("Could not read {}", url_path.to_string_lossy()))
161            .may_bail(false)
162            .trim()
163    );
164
165    let signing_keys = load_and_unlock_signing_keys(profile_name, profile);
166    let verifying_keys_path = verifying_keys_path(profile_name, false);
167    let verifying_keys = AttestantVerifyingKeys::load(&verifying_keys_path).may_bail(false);
168
169    let reachable_verifying_keys_public_key_rings = reachable_verifying_keys_public_key_rings
170        .iter()
171        .map(|(rvk, rpkr)| {
172            let vfk_aliased =
173                storage::file_path(profile_name, Some(rvk), "vfk", false).may_bail(true);
174            let vfk_path = PathBuf::from(rvk);
175            let vfk = match (vfk_aliased.exists(), vfk_path.exists()) {
176                (_, true) => ReachableVerifyingKeys::load(&vfk_path).may_bail(true),
177                (true, false) => ReachableVerifyingKeys::load(&vfk_aliased).may_bail(true),
178                (false, false) => exit(1),
179            };
180
181            vfk.verify(&verifying_keys).may_bail_with(
182                concat!(
183                    "Supplied reachable verifying keys are not verifiable with this profile's ",
184                    "attestant verifying keys."
185                ),
186                false,
187            );
188
189            let pkr_path = PathBuf::from(rpkr);
190            let ring = ReachablePublicKeyRing::load(&pkr_path).may_bail_with(
191                &format!(
192                    "Couldn't load reachable public key ring from '{}'.",
193                    pkr_path.to_string_lossy()
194                ),
195                false,
196            );
197
198            ring.reachable_public_keys
199                .iter()
200                .all(|rpk| rpk.verify(&vfk))
201                .may_bail_with(
202                    &format!(
203                        concat!(
204                            "Reachable public keys in '{}' are not signed with the verifying keys ",
205                            "you supplied."
206                        ),
207                        pkr_path.to_string_lossy()
208                    ),
209                    false,
210                );
211
212            (vfk, ring)
213        })
214        .collect::<Vec<(_, _)>>();
215
216    let mut client = authenticate(&signing_keys, &verifying_keys, &url).await;
217
218    for (vfk, pkr) in &reachable_verifying_keys_public_key_rings {
219        client
220            .send_and_wait(vfk)
221            .await
222            .map_err(|_| "Onboarding reachable verifying keys failed")
223            .may_bail(true);
224        client
225            .send_and_wait(pkr)
226            .await
227            .map_err(|_| "Adding reachable public key ring failed.")
228            .may_bail(true);
229    }
230
231    println!(
232        "Successfully onboarded {} peers!",
233        reachable_verifying_keys_public_key_rings.len()
234    );
235}
236
237pub fn revoke(profile_name: &str, profile: &Option<String>) {
238    let _signing_keys = load_and_unlock_signing_keys(profile_name, profile);
239    // sign b"revoke" + read({data}/{profile}/{alias}.vfk)
240    // save revoked key to {pwd}/{alias}.rvfk
241    // remove {data}/{profile}/{alias}.vfk
242    todo!();
243}
244
245// May be used in an airgapped manner, on a separate machine that does the signing.
246pub fn sign(
247    profile_name: &str,
248    profile: &Option<String>,
249    unsigned_verifying_keys: Vec<PathBuf>,
250    copy_to_cwd: bool,
251    csprng: &mut impl CryptoRngCore,
252) {
253    let signing_keys = load_and_unlock_signing_keys(profile_name, profile);
254
255    let data_path =
256        storage::data_path(true).may_bail_with("Data directory needs to be writeable.", false);
257
258    let aliases = unsigned_verifying_keys
259        .iter()
260        .fold(String::new(), |acc, p| {
261            let unsigned_file_name = p
262                .file_name()
263                .may_bail_with("Attempting to sign an invalid path.", false)
264                .to_string_lossy();
265            let alias = unsigned_file_name
266                .rsplit_once('.')
267                .map_or_else(|| unsigned_file_name.clone(), |(alias, _)| Cow::from(alias));
268
269            fs::copy(p, data_path.join(format!("{alias}.uvfk"))).may_bail_with(
270                "Could not copy unsigned verifying key to data directory.",
271                true,
272            );
273
274            let unsigned_reachable_verifying_keys = UnsignedReachableVerifyingKeys::load(p)
275                .may_bail_with(&format!("{p:?} is malformed, it can't be signed."), false);
276
277            let reachable_verifying_keys =
278                signing_keys.sign(unsigned_reachable_verifying_keys, csprng);
279
280            let signed_file_name = format!("{alias}.vfk");
281
282            reachable_verifying_keys
283                .store(&data_path.join(&signed_file_name))
284                .may_bail_with(
285                    "Could not store signed reachable verifying keys in data directory.",
286                    true,
287                );
288
289            if copy_to_cwd {
290                reachable_verifying_keys
291                    .store(&env::current_dir().may_bail(true).join(&signed_file_name))
292                    .may_bail_with(
293                        "Could not store signed reachable verifying keys in the current directory.",
294                        true,
295                    );
296            }
297
298            format!("{acc}, {alias}")
299        });
300    let aliases = aliases.trim_start_matches(',').trim();
301
302    println!("\u{2712}\u{FE0F} Successfully signed reachable verifying keys for: {aliases}");
303}
304
305pub fn url(profile_name: &str, force: bool, url: Option<String>) {
306    let url_path = url_path(profile_name, url.is_some());
307    let url_exists = url_path.exists();
308
309    let suffix = match profile_name {
310        "default" => String::new(),
311        profile_name => format!(" for '{profile_name}'"),
312    };
313
314    match (url, !url_exists || force) {
315        (None, _) => {
316            url_exists.may_bail_with("No URL was set for profile.", false);
317
318            let url = fs::read_to_string(url_path).may_bail_with("Could not read URL", true);
319
320            println!("{}/", url.trim().trim_matches('/'));
321        }
322        (Some(url), true) => {
323            fs::write(url_path, url.trim_end_matches('/')).may_bail(true);
324
325            println!("\u{1f517} Successfully set URL{suffix}!");
326        }
327        (Some(_), false) => {
328            err_msg(
329                format!(
330                    "URL already exists{suffix}. Rerun with --force if you want to replace it."
331                ),
332                false,
333            );
334            exit(1);
335        }
336    }
337}
338
339// Shows path to verifying keys, dumps them if piped to a file or command
340pub fn verifying_keys(profile_name: &str) {
341    let verifying_keys_path = verifying_keys_path(profile_name, false);
342
343    let stdout = io::stdout();
344    if stdout.is_terminal() {
345        println!(
346            "\u{1f6c2} Your verifying key is located at:\n\n{}",
347            verifying_keys_path.to_string_lossy()
348        );
349    } else {
350        let mut verifying_keys = File::open(&verifying_keys_path).may_bail(false);
351        let mut lock = stdout.lock();
352        io::copy(&mut verifying_keys, &mut lock).may_bail(false);
353    }
354}
355
356pub fn visual_identity(profile_name: &str) {
357    let verifying_keys_path = storage::file_path(profile_name, None, "vfk", false).may_bail(false);
358
359    let verifying_keys = AttestantVerifyingKeys::load(&verifying_keys_path).may_bail_with(
360        "Verifying keys could not be loaded.\nHave you generated signing keys for this profile?",
361        false,
362    );
363
364    let reach_salts_path = storage::file_path(profile_name, None, "slt", true)
365        .may_bail_with("Couldn't determine path for salts.", true);
366
367    let reach_salts = Salts::load(&reach_salts_path).may_bail_with("", false);
368
369    let identity = visual_key_identity(&verifying_keys, &reach_salts.attestant_identity)
370        .may_bail_with(
371            "Couldn't create visual identity for your verifying keys.",
372            true,
373        );
374
375    println!("{identity}");
376}