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 compute_signature, format_key, format_signature, KeyAlgorithm, SignatureAlgorithm,
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()?;
34
35 let newly_created = !site.private_key_path().exists();
36 let verifying_key = if !newly_created {
37 load_signing_key_with_site(site)?.verifying_key()
40 } else {
41 let mut secret_bytes = [0u8; 32];
43 rand::rng().fill(&mut secret_bytes[..]);
44 let signing_key = SigningKey::from_bytes(&secret_bytes);
45 secret_bytes.zeroize(); let verifying_key = signing_key.verifying_key();
47 let private_key_path = site.private_key_path();
51 let private_pem = signing_key
52 .to_pkcs8_pem(ed25519_dalek::pkcs8::spki::der::pem::LineEnding::LF)
53 .map_err(|e| anyhow::anyhow!("Failed to encode private key: {}", e))?;
54
55 #[cfg(unix)]
57 {
58 use std::io::Write;
59 use std::os::unix::fs::OpenOptionsExt;
60 let mut f = fs::OpenOptions::new()
61 .write(true)
62 .create(true)
63 .truncate(true)
64 .mode(0o600)
65 .open(&private_key_path)?;
66 f.write_all(private_pem.as_bytes())?;
67 }
68 #[cfg(not(unix))]
69 fs::write(&private_key_path, private_pem.as_bytes())?;
70
71 verifying_key
72 };
73
74 let public_key = format_key(KeyAlgorithm::Ed25519, &verifying_key.to_bytes());
76 let public_pem = verifying_key
77 .to_public_key_pem(ed25519_dalek::pkcs8::spki::der::pem::LineEnding::LF)
78 .map_err(|e| anyhow::anyhow!("Failed to encode public key: {}", e))?;
79 fs::write(site.public_key_path(), &public_pem)?;
80
81 Ok(IdentityInfo {
82 domain: domain.to_string(),
83 public_key,
84 newly_created,
85 })
86}
87
88pub fn get_identity_with_site(domain: &str, site: &SiteDir) -> anyhow::Result<IdentityInfo> {
89 if !site.public_key_path().exists() {
90 anyhow::bail!("No identity found at {}", site.root.display());
91 }
92
93 let pem_content = fs::read_to_string(site.public_key_path())?;
95 let verifying_key = VerifyingKey::from_public_key_pem(&pem_content)
96 .map_err(|e| anyhow::anyhow!("Invalid public key PEM: {}", e))?;
97
98 let public_key = format_key(KeyAlgorithm::Ed25519, &verifying_key.to_bytes());
99
100 Ok(IdentityInfo {
101 domain: domain.to_string(),
102 public_key,
103 newly_created: false,
104 })
105}
106
107pub fn sign_json_with_site<T: Serialize>(
108 site: &SiteDir,
109 value: &T,
110) -> Result<String, JsonSignError> {
111 let signing_key = load_signing_key_with_site(site)?;
112 let key_bytes = zeroize::Zeroizing::new(signing_key.to_bytes());
114 compute_signature(value, SignatureAlgorithm::Ed25519, &*key_bytes)
115 .map_err(|e| JsonSignError::Jcs(e.to_string()))
116}
117
118pub fn sign_data_with_site(site: &SiteDir, data: &[u8]) -> anyhow::Result<String> {
123 let signing_key = load_signing_key_with_site(site)?;
124 sign_ed25519(&signing_key, data)
125}
126
127fn load_signing_key_with_site(site: &SiteDir) -> anyhow::Result<SigningKey> {
128 let private_key_path = site.private_key_path();
129
130 if !private_key_path.exists() {
131 anyhow::bail!("No private key found at {}", private_key_path.display());
132 }
133
134 #[cfg(unix)]
135 {
136 let metadata = fs::metadata(&private_key_path)?;
137 let mode = metadata.permissions().mode() & 0o777;
138 if mode != 0o600 {
139 anyhow::bail!(
140 "Private key has insecure permissions {:04o} (expected 0600).\n\
141 Fix with: chmod 600 {}",
142 mode,
143 private_key_path.display()
144 );
145 }
146 }
147
148 let pem_content = zeroize::Zeroizing::new(fs::read_to_string(private_key_path)?);
150 SigningKey::from_pkcs8_pem(&pem_content)
151 .map_err(|e| anyhow::anyhow!("Invalid private key PEM: {}", e))
152}
153
154fn sign_ed25519(signing_key: &SigningKey, data: &[u8]) -> anyhow::Result<String> {
155 let signature = signing_key.sign(data);
156 Ok(format_signature(
157 SignatureAlgorithm::Ed25519,
158 &signature.to_bytes(),
159 ))
160}