1use crate::site::SiteDir;
2use ed25519_dalek::pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey};
3use ed25519_dalek::{Signer, SigningKey, VerifyingKey};
4use rand::RngExt;
5use serde::Serialize;
6use std::fs;
7use substrate::{
8 KeyAlgorithm, SignatureAlgorithm, compute_signature, format_key, format_signature,
9};
10use zeroize::Zeroize;
11
12#[cfg(unix)]
13use std::os::unix::fs::PermissionsExt;
14
15#[derive(serde::Serialize)]
16pub struct IdentityInfo {
17 pub domain: String,
18 pub public_key: String,
19 pub newly_created: bool,
21}
22
23#[derive(Debug, thiserror::Error)]
24pub enum JsonSignError {
25 #[error("JCS serialization failed: {0}")]
26 Jcs(String),
27 #[error(transparent)]
28 Sign(#[from] anyhow::Error),
29}
30
31pub fn init_identity_with_site(domain: &str, site: &SiteDir) -> anyhow::Result<IdentityInfo> {
32 site.create_dirs()?;
33 crate::cache::fs::with_dir_lock(&site.keys, || {
34 init_identity_locked(domain, site)
35 .map_err(|e| crate::HyphaError::new("identity_init_failed", e.to_string()))
36 })
37 .map_err(anyhow::Error::from)
38}
39
40fn init_identity_locked(domain: &str, site: &SiteDir) -> anyhow::Result<IdentityInfo> {
41 let newly_created = !site.private_key_path().exists();
42 let verifying_key = if !newly_created {
43 load_signing_key_with_site(site)?.verifying_key()
46 } else {
47 let mut secret_bytes = [0u8; 32];
49 rand::rng().fill(&mut secret_bytes[..]);
50 let signing_key = SigningKey::from_bytes(&secret_bytes);
51 secret_bytes.zeroize(); let verifying_key = signing_key.verifying_key();
53 let private_key_path = site.private_key_path();
57 let private_pem = signing_key
58 .to_pkcs8_pem(ed25519_dalek::pkcs8::spki::der::pem::LineEnding::LF)
59 .map_err(|e| anyhow::anyhow!("Failed to encode private key: {}", e))?;
60
61 crate::cache::fs::atomic_write_file(
62 &private_key_path,
63 private_pem.as_bytes(),
64 &site.keys.join(".cmn"),
65 Some(0o600),
66 )?;
67
68 verifying_key
69 };
70
71 let public_key = format_key(KeyAlgorithm::Ed25519, &verifying_key.to_bytes());
73 let public_pem = verifying_key
74 .to_public_key_pem(ed25519_dalek::pkcs8::spki::der::pem::LineEnding::LF)
75 .map_err(|e| anyhow::anyhow!("Failed to encode public key: {}", e))?;
76 crate::cache::fs::atomic_write_file(
77 &site.public_key_path(),
78 public_pem.as_bytes(),
79 &site.keys.join(".cmn"),
80 Some(0o644),
81 )?;
82
83 Ok(IdentityInfo {
84 domain: domain.to_string(),
85 public_key,
86 newly_created,
87 })
88}
89
90pub fn get_identity_with_site(domain: &str, site: &SiteDir) -> anyhow::Result<IdentityInfo> {
91 if !site.public_key_path().exists() {
92 anyhow::bail!("No identity found at {}", site.root.display());
93 }
94
95 let pem_content = fs::read_to_string(site.public_key_path())?;
97 let verifying_key = VerifyingKey::from_public_key_pem(&pem_content)
98 .map_err(|e| anyhow::anyhow!("Invalid public key PEM: {}", e))?;
99
100 let public_key = format_key(KeyAlgorithm::Ed25519, &verifying_key.to_bytes());
101
102 Ok(IdentityInfo {
103 domain: domain.to_string(),
104 public_key,
105 newly_created: false,
106 })
107}
108
109pub fn sign_json_with_site<T: Serialize>(
110 site: &SiteDir,
111 value: &T,
112) -> Result<String, JsonSignError> {
113 let signing_key = load_signing_key_with_site(site)?;
114 let key_bytes = zeroize::Zeroizing::new(signing_key.to_bytes());
116 compute_signature(value, SignatureAlgorithm::Ed25519, &*key_bytes)
117 .map_err(|e| JsonSignError::Jcs(e.to_string()))
118}
119
120pub fn sign_data_with_site(site: &SiteDir, data: &[u8]) -> anyhow::Result<String> {
125 let signing_key = load_signing_key_with_site(site)?;
126 sign_ed25519(&signing_key, data)
127}
128
129fn load_signing_key_with_site(site: &SiteDir) -> anyhow::Result<SigningKey> {
130 let private_key_path = site.private_key_path();
131
132 if !private_key_path.exists() {
133 anyhow::bail!("No private key found at {}", private_key_path.display());
134 }
135
136 #[cfg(unix)]
137 {
138 let metadata = fs::metadata(&private_key_path)?;
139 let mode = metadata.permissions().mode() & 0o777;
140 if mode != 0o600 {
141 anyhow::bail!(
142 "Private key has insecure permissions {:04o} (expected 0600).\n\
143 Fix with: chmod 600 {}",
144 mode,
145 private_key_path.display()
146 );
147 }
148 }
149
150 let pem_content = zeroize::Zeroizing::new(fs::read_to_string(private_key_path)?);
152 SigningKey::from_pkcs8_pem(&pem_content)
153 .map_err(|e| anyhow::anyhow!("Invalid private key PEM: {}", e))
154}
155
156fn sign_ed25519(signing_key: &SigningKey, data: &[u8]) -> anyhow::Result<String> {
157 let signature = signing_key.sign(data);
158 Ok(format_signature(
159 SignatureAlgorithm::Ed25519,
160 &signature.to_bytes(),
161 ))
162}
163
164#[cfg(test)]
165#[allow(clippy::unwrap_used)]
166mod tests {
167 use super::*;
168 use std::io::{Read, Write};
169 use std::process::{Command, Stdio};
170
171 #[test]
172 fn identity_init_subprocess() {
173 let Some(root) = std::env::var_os("CMN_HYPHA_TEST_IDENTITY_SITE") else {
174 return;
175 };
176 let result_path = std::env::var_os("CMN_HYPHA_TEST_IDENTITY_RESULT").unwrap();
177 let mut start = [0];
178 std::io::stdin().read_exact(&mut start).unwrap();
179 let site = SiteDir::with_path(root.into());
180 let identity = init_identity_with_site("example.com", &site).unwrap();
181 std::fs::write(result_path, serde_json::to_vec(&identity).unwrap()).unwrap();
182 }
183
184 #[test]
185 fn concurrent_init_keeps_one_identity_and_matching_keypair() {
186 let root = tempfile::tempdir().unwrap();
187 let site = SiteDir::with_path(root.path().join("site"));
188 let mut children = Vec::new();
189 let mut result_paths = Vec::new();
190 for index in 0..2 {
191 let result_path = root.path().join(format!("identity-{index}.json"));
192 let child = Command::new(std::env::current_exe().unwrap())
193 .args(["--exact", "auth::tests::identity_init_subprocess"])
194 .env("CMN_HYPHA_TEST_IDENTITY_SITE", &site.root)
195 .env("CMN_HYPHA_TEST_IDENTITY_RESULT", &result_path)
196 .stdin(Stdio::piped())
197 .stdout(Stdio::null())
198 .stderr(Stdio::piped())
199 .spawn()
200 .unwrap();
201 children.push(child);
202 result_paths.push(result_path);
203 }
204 for child in &mut children {
205 child.stdin.take().unwrap().write_all(b"1").unwrap();
206 }
207 for child in children {
208 let output = child.wait_with_output().unwrap();
209 assert!(
210 output.status.success(),
211 "{}",
212 String::from_utf8_lossy(&output.stderr)
213 );
214 }
215 let identities: Vec<serde_json::Value> = result_paths
216 .iter()
217 .map(|path| serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap())
218 .collect();
219 assert_eq!(identities[0]["public_key"], identities[1]["public_key"]);
220 assert_ne!(
221 identities[0]["newly_created"],
222 identities[1]["newly_created"]
223 );
224 let private = std::fs::read(site.private_key_path()).unwrap();
225 let repeated = init_identity_with_site("example.com", &site).unwrap();
226 assert!(!repeated.newly_created);
227 assert_eq!(std::fs::read(site.private_key_path()).unwrap(), private);
228 assert_eq!(
229 load_signing_key_with_site(&site).unwrap().verifying_key(),
230 VerifyingKey::from_public_key_pem(
231 &std::fs::read_to_string(site.public_key_path()).unwrap()
232 )
233 .unwrap()
234 );
235 #[cfg(unix)]
236 assert_eq!(
237 std::fs::metadata(site.private_key_path())
238 .unwrap()
239 .permissions()
240 .mode()
241 & 0o777,
242 0o600
243 );
244 }
245}