nexus_sdk/signed_http/
keys.rs1use {
29 base64::{engine::general_purpose, Engine as _},
30 ed25519_dalek::SigningKey,
31 rand::RngCore as _,
32 thiserror::Error,
33};
34
35#[derive(Clone)]
39pub struct Ed25519Keypair {
40 signing: SigningKey,
41}
42
43impl Ed25519Keypair {
44 pub fn generate() -> Self {
46 let mut sk = [0u8; 32];
47 rand::rngs::OsRng.fill_bytes(&mut sk);
48 Self {
49 signing: SigningKey::from_bytes(&sk),
50 }
51 }
52
53 pub fn from_signing_key(signing: SigningKey) -> Self {
55 Self { signing }
56 }
57
58 pub fn from_private_key_hex_or_base64(raw: &str) -> Result<Self, KeyParseError> {
60 parse_ed25519_signing_key(raw).map(Self::from_signing_key)
61 }
62
63 pub fn signing_key(&self) -> &SigningKey {
65 &self.signing
66 }
67
68 pub fn private_key_bytes(&self) -> [u8; 32] {
70 self.signing.to_bytes()
71 }
72
73 pub fn public_key_bytes(&self) -> [u8; 32] {
75 self.signing.verifying_key().to_bytes()
76 }
77
78 pub fn private_key_hex(&self) -> String {
80 hex::encode(self.private_key_bytes())
81 }
82
83 pub fn public_key_hex(&self) -> String {
85 hex::encode(self.public_key_bytes())
86 }
87}
88
89#[derive(Debug, Error)]
91pub enum KeyParseError {
92 #[error("invalid hex private key: {0}")]
93 InvalidHex(String),
94 #[error("invalid base64/base64url private key: {0}")]
95 InvalidBase64(String),
96 #[error("unsupported key scheme flag 0x{flag:02x} (expected 0x00 for ed25519)")]
97 UnsupportedSuiKeySchemeFlag { flag: u8 },
98 #[error(
99 "invalid private key length {len}, expected 32 bytes (raw ed25519) or 33 bytes (0x00 + key)"
100 )]
101 InvalidLength { len: usize },
102}
103
104pub fn parse_ed25519_signing_key(raw: &str) -> Result<SigningKey, KeyParseError> {
108 let raw = raw.trim();
109 let raw_no_0x = raw.strip_prefix("0x").unwrap_or(raw);
110
111 let looks_like_hex = raw.starts_with("0x")
114 || ((raw_no_0x.len() == 64 || raw_no_0x.len() == 66)
115 && raw_no_0x.chars().all(|c| c.is_ascii_hexdigit()));
116
117 if looks_like_hex {
118 let bytes = hex::decode(raw_no_0x).map_err(|e| KeyParseError::InvalidHex(e.to_string()))?;
119 return match bytes.len() {
120 32 => Ok(SigningKey::from_bytes(
121 &<[u8; 32]>::try_from(bytes.as_slice()).expect("length checked"),
122 )),
123 33 => signing_key_from_sui_flagged_bytes(
124 &<[u8; 33]>::try_from(bytes.as_slice()).expect("length checked"),
125 ),
126 len => Err(KeyParseError::InvalidLength { len }),
127 };
128 }
129
130 let try_b64 = |engine: &general_purpose::GeneralPurpose| -> Option<Vec<u8>> {
132 engine.decode(raw.as_bytes()).ok()
133 };
134
135 let bytes = try_b64(&general_purpose::STANDARD)
136 .or_else(|| try_b64(&general_purpose::STANDARD_NO_PAD))
137 .or_else(|| try_b64(&general_purpose::URL_SAFE))
138 .or_else(|| try_b64(&general_purpose::URL_SAFE_NO_PAD))
139 .ok_or_else(|| {
140 KeyParseError::InvalidBase64("expected base64/base64url data".to_string())
141 })?;
142
143 match bytes.len() {
144 32 => Ok(SigningKey::from_bytes(
145 &<[u8; 32]>::try_from(bytes.as_slice()).expect("length checked"),
146 )),
147 33 => signing_key_from_sui_flagged_bytes(
148 &<[u8; 33]>::try_from(bytes.as_slice()).expect("length checked"),
149 ),
150 len => Err(KeyParseError::InvalidLength { len }),
151 }
152}
153
154fn signing_key_from_sui_flagged_bytes(bytes: &[u8; 33]) -> Result<SigningKey, KeyParseError> {
155 const ED25519_FLAG: u8 = 0x00;
156 if bytes[0] != ED25519_FLAG {
157 return Err(KeyParseError::UnsupportedSuiKeySchemeFlag { flag: bytes[0] });
158 }
159 let mut sk = [0u8; 32];
160 sk.copy_from_slice(&bytes[1..]);
161 Ok(SigningKey::from_bytes(&sk))
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 fn make_key_bytes(byte: u8) -> [u8; 32] {
169 [byte; 32]
170 }
171
172 #[test]
173 fn parse_hex_roundtrip() {
174 let keypair = Ed25519Keypair::generate();
175 let hex = keypair.private_key_hex();
176 let parsed = Ed25519Keypair::from_private_key_hex_or_base64(&hex).unwrap();
177 assert_eq!(parsed.public_key_bytes(), keypair.public_key_bytes());
178 }
179
180 #[test]
181 fn parse_base64_roundtrip() {
182 let bytes = make_key_bytes(7);
183 let b64 = general_purpose::STANDARD.encode(bytes);
184 let parsed = Ed25519Keypair::from_private_key_hex_or_base64(&b64).unwrap();
185 assert_eq!(parsed.private_key_bytes(), bytes);
186 }
187
188 #[test]
189 fn parse_sui_keytool_base64_format() {
190 let bytes = make_key_bytes(9);
191 let mut flagged = [0u8; 33];
192 flagged[0] = 0x00;
193 flagged[1..].copy_from_slice(&bytes);
194 let b64 = general_purpose::STANDARD.encode(flagged);
195 let parsed = Ed25519Keypair::from_private_key_hex_or_base64(&b64).unwrap();
196 assert_eq!(parsed.private_key_bytes(), bytes);
197 }
198
199 #[test]
200 fn parse_invalid_hex_reports_error() {
201 assert!(matches!(
202 Ed25519Keypair::from_private_key_hex_or_base64("0xzzzz"),
203 Err(KeyParseError::InvalidHex(_))
204 ));
205 }
206
207 #[test]
208 fn parse_invalid_length_reports_error() {
209 let bytes = vec![0u8; 31];
210 let b64 = general_purpose::STANDARD.encode(bytes);
211 assert!(matches!(
212 Ed25519Keypair::from_private_key_hex_or_base64(&b64),
213 Err(KeyParseError::InvalidLength { len: 31 })
214 ));
215 }
216
217 #[test]
218 fn parse_unsupported_sui_flag_reports_error() {
219 let mut flagged = [0u8; 33];
220 flagged[0] = 0x01;
221 let b64 = general_purpose::STANDARD.encode(flagged);
222 assert!(matches!(
223 Ed25519Keypair::from_private_key_hex_or_base64(&b64),
224 Err(KeyParseError::UnsupportedSuiKeySchemeFlag { flag: 0x01 })
225 ));
226 }
227}