Skip to main content

triblespace_core/value/schemas/
ed25519.rs

1use ed25519::ComponentBytes;
2use ed25519::Signature;
3use ed25519_dalek::SignatureError;
4/// Re-export of the Ed25519 verifying (public) key type from [`ed25519_dalek`].
5pub use ed25519_dalek::VerifyingKey;
6
7use crate::id::ExclusiveId;
8use crate::id::Id;
9use crate::id_hex;
10use crate::macros::entity;
11use crate::metadata;
12use crate::metadata::{ConstDescribe, ConstId};
13use crate::repo::BlobStore;
14use crate::trible::Fragment;
15use crate::value::schemas::hash::Blake3;
16use crate::value::ToValue;
17use crate::value::TryFromValue;
18use crate::value::Value;
19use crate::value::ValueSchema;
20use std::convert::Infallible;
21
22/// A value schema for the R component of an Ed25519 signature.
23pub struct ED25519RComponent;
24
25impl ConstId for ED25519RComponent {
26    const ID: Id = id_hex!("995A86FFC83DB95ECEAA17E226208897");
27}
28
29/// A value schema for the S component of an Ed25519 signature.
30pub struct ED25519SComponent;
31
32impl ConstId for ED25519SComponent {
33    const ID: Id = id_hex!("10D35B0B628E9E409C549D8EC1FB3598");
34}
35
36/// A value schema for an Ed25519 public key.
37pub struct ED25519PublicKey;
38
39impl ConstId for ED25519PublicKey {
40    const ID: Id = id_hex!("69A872254E01B4C1ED36E08E40445E93");
41}
42
43impl ConstDescribe for ED25519RComponent {
44    fn describe<B>(blobs: &mut B) -> Result<Fragment, B::PutError>
45    where
46        B: BlobStore<Blake3>,
47    {
48        let id = Self::ID;
49        let description = blobs.put(
50            "Ed25519 signature R component stored as a 32-byte field. This is one half of the standard 64-byte Ed25519 signature.\n\nUse when you store signatures as structured values or need to index the components separately. Pair with the S component to reconstruct or verify the full signature.\n\nIf you prefer storing the signature as a single binary blob, use a blob schema (for example LongString with base64 or a custom blob schema).",
51        )?;
52        let tribles = entity! {
53            ExclusiveId::force_ref(&id) @
54                metadata::name: blobs.put("ed25519:r")?,
55                metadata::description: description,
56                metadata::tag: metadata::KIND_VALUE_SCHEMA,
57        };
58
59        #[cfg(feature = "wasm")]
60        let tribles = {
61            let mut tribles = tribles;
62            tribles += entity! { ExclusiveId::force_ref(&id) @
63                metadata::value_formatter: blobs.put(wasm_formatter::ED25519_R_WASM)?,
64            };
65            tribles
66        };
67        Ok(tribles)
68    }
69}
70impl ValueSchema for ED25519RComponent {
71    type ValidationError = Infallible;
72}
73impl ConstDescribe for ED25519SComponent {
74    fn describe<B>(blobs: &mut B) -> Result<Fragment, B::PutError>
75    where
76        B: BlobStore<Blake3>,
77    {
78        let id = Self::ID;
79        let description = blobs.put(
80            "Ed25519 signature S component stored as a 32-byte field. This is the second half of the standard Ed25519 signature.\n\nUse when storing or querying signatures in a structured form. Pair with the R component to reconstruct or verify the full signature.\n\nAs with the R component, treat this as public data; private signing keys should be stored separately and securely.",
81        )?;
82        let tribles = entity! {
83            ExclusiveId::force_ref(&id) @
84                metadata::name: blobs.put("ed25519:s")?,
85                metadata::description: description,
86                metadata::tag: metadata::KIND_VALUE_SCHEMA,
87        };
88
89        #[cfg(feature = "wasm")]
90        let tribles = {
91            let mut tribles = tribles;
92            tribles += entity! { ExclusiveId::force_ref(&id) @
93                metadata::value_formatter: blobs.put(wasm_formatter::ED25519_S_WASM)?,
94            };
95            tribles
96        };
97        Ok(tribles)
98    }
99}
100impl ValueSchema for ED25519SComponent {
101    type ValidationError = Infallible;
102}
103impl ConstDescribe for ED25519PublicKey {
104    fn describe<B>(blobs: &mut B) -> Result<Fragment, B::PutError>
105    where
106        B: BlobStore<Blake3>,
107    {
108        let id = Self::ID;
109        let description = blobs.put(
110            "Ed25519 public key stored as a 32-byte field. Public keys verify signatures and identify signing identities.\n\nUse for signer registries, verification records, or key references associated with signatures. Private keys are not represented by a built-in schema and should be handled separately.\n\nEd25519 is widely supported and deterministic; if you need another scheme, define a custom schema with its own metadata.",
111        )?;
112        let tribles = entity! {
113            ExclusiveId::force_ref(&id) @
114                metadata::name: blobs.put("ed25519:pubkey")?,
115                metadata::description: description,
116                metadata::tag: metadata::KIND_VALUE_SCHEMA,
117        };
118
119        #[cfg(feature = "wasm")]
120        let tribles = {
121            let mut tribles = tribles;
122            tribles += entity! { ExclusiveId::force_ref(&id) @
123                metadata::value_formatter: blobs.put(wasm_formatter::ED25519_PUBKEY_WASM)?,
124            };
125            tribles
126        };
127        Ok(tribles)
128    }
129}
130impl ValueSchema for ED25519PublicKey {
131    type ValidationError = Infallible;
132}
133
134#[cfg(feature = "wasm")]
135mod wasm_formatter {
136    use core::fmt::Write;
137
138    use triblespace_core_macros::value_formatter;
139
140    #[value_formatter(const_wasm = ED25519_R_WASM)]
141    pub(crate) fn ed25519_r(raw: &[u8; 32], out: &mut impl Write) -> Result<(), u32> {
142        out.write_str("ed25519:r:").map_err(|_| 1u32)?;
143        const TABLE: &[u8; 16] = b"0123456789ABCDEF";
144        for &byte in raw {
145            let hi = (byte >> 4) as usize;
146            let lo = (byte & 0x0F) as usize;
147            out.write_char(TABLE[hi] as char).map_err(|_| 1u32)?;
148            out.write_char(TABLE[lo] as char).map_err(|_| 1u32)?;
149        }
150        Ok(())
151    }
152
153    #[value_formatter(const_wasm = ED25519_S_WASM)]
154    pub(crate) fn ed25519_s(raw: &[u8; 32], out: &mut impl Write) -> Result<(), u32> {
155        out.write_str("ed25519:s:").map_err(|_| 1u32)?;
156        const TABLE: &[u8; 16] = b"0123456789ABCDEF";
157        for &byte in raw {
158            let hi = (byte >> 4) as usize;
159            let lo = (byte & 0x0F) as usize;
160            out.write_char(TABLE[hi] as char).map_err(|_| 1u32)?;
161            out.write_char(TABLE[lo] as char).map_err(|_| 1u32)?;
162        }
163        Ok(())
164    }
165
166    #[value_formatter(const_wasm = ED25519_PUBKEY_WASM)]
167    pub(crate) fn ed25519_pubkey(raw: &[u8; 32], out: &mut impl Write) -> Result<(), u32> {
168        out.write_str("ed25519:pubkey:").map_err(|_| 1u32)?;
169        const TABLE: &[u8; 16] = b"0123456789ABCDEF";
170        for &byte in raw {
171            let hi = (byte >> 4) as usize;
172            let lo = (byte & 0x0F) as usize;
173            out.write_char(TABLE[hi] as char).map_err(|_| 1u32)?;
174            out.write_char(TABLE[lo] as char).map_err(|_| 1u32)?;
175        }
176        Ok(())
177    }
178}
179
180impl ED25519RComponent {
181    /// Extracts the R component from a full Ed25519 signature.
182    pub fn from_signature(s: Signature) -> Value<ED25519RComponent> {
183        Value::new(*s.r_bytes())
184    }
185}
186
187impl ED25519SComponent {
188    /// Extracts the S component from a full Ed25519 signature.
189    pub fn from_signature(s: Signature) -> Value<ED25519SComponent> {
190        Value::new(*s.s_bytes())
191    }
192}
193
194impl ToValue<ED25519RComponent> for Signature {
195    fn to_value(self) -> Value<ED25519RComponent> {
196        ED25519RComponent::from_signature(self)
197    }
198}
199
200impl ToValue<ED25519SComponent> for Signature {
201    fn to_value(self) -> Value<ED25519SComponent> {
202        ED25519SComponent::from_signature(self)
203    }
204}
205
206impl ToValue<ED25519RComponent> for ComponentBytes {
207    fn to_value(self) -> Value<ED25519RComponent> {
208        Value::new(self)
209    }
210}
211
212impl TryFromValue<'_, ED25519RComponent> for ComponentBytes {
213    type Error = Infallible;
214    fn try_from_value(v: &Value<ED25519RComponent>) -> Result<Self, Infallible> {
215        Ok(v.raw)
216    }
217}
218
219impl ToValue<ED25519SComponent> for ComponentBytes {
220    fn to_value(self) -> Value<ED25519SComponent> {
221        Value::new(self)
222    }
223}
224
225impl TryFromValue<'_, ED25519SComponent> for ComponentBytes {
226    type Error = Infallible;
227    fn try_from_value(v: &Value<ED25519SComponent>) -> Result<Self, Infallible> {
228        Ok(v.raw)
229    }
230}
231
232impl ToValue<ED25519PublicKey> for VerifyingKey {
233    fn to_value(self) -> Value<ED25519PublicKey> {
234        Value::new(self.to_bytes())
235    }
236}
237
238impl TryFromValue<'_, ED25519PublicKey> for VerifyingKey {
239    type Error = SignatureError;
240
241    fn try_from_value(v: &Value<ED25519PublicKey>) -> Result<Self, Self::Error> {
242        VerifyingKey::from_bytes(&v.raw)
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249    use crate::value::{ToValue, TryFromValue};
250    use ed25519::Signature;
251    use ed25519_dalek::SigningKey;
252    use proptest::prelude::*;
253
254    proptest! {
255        #[test]
256        fn r_component_bytes_roundtrip(input in prop::array::uniform32(any::<u8>())) {
257            let value: Value<ED25519RComponent> = input.to_value();
258            let output: ComponentBytes = value.from_value();
259            prop_assert_eq!(input, output);
260        }
261
262        #[test]
263        fn s_component_bytes_roundtrip(input in prop::array::uniform32(any::<u8>())) {
264            let value: Value<ED25519SComponent> = input.to_value();
265            let output: ComponentBytes = value.from_value();
266            prop_assert_eq!(input, output);
267        }
268
269        #[test]
270        fn r_component_validates(input in prop::array::uniform32(any::<u8>())) {
271            let value: Value<ED25519RComponent> = input.to_value();
272            prop_assert!(ED25519RComponent::validate(value).is_ok());
273        }
274
275        #[test]
276        fn s_component_validates(input in prop::array::uniform32(any::<u8>())) {
277            let value: Value<ED25519SComponent> = input.to_value();
278            prop_assert!(ED25519SComponent::validate(value).is_ok());
279        }
280
281        #[test]
282        fn pubkey_validates(seed in prop::array::uniform32(any::<u8>())) {
283            let key = SigningKey::from_bytes(&seed).verifying_key();
284            let value: Value<ED25519PublicKey> = key.to_value();
285            prop_assert!(ED25519PublicKey::validate(value).is_ok());
286        }
287
288        #[test]
289        fn verifying_key_roundtrip(seed in prop::array::uniform32(any::<u8>())) {
290            let key = SigningKey::from_bytes(&seed).verifying_key();
291            let value: Value<ED25519PublicKey> = key.to_value();
292            let recovered = VerifyingKey::try_from_value(&value).expect("valid key");
293            prop_assert_eq!(key, recovered);
294        }
295
296        #[test]
297        fn signature_r_component_roundtrip(seed in prop::array::uniform32(any::<u8>()), msg in prop::collection::vec(any::<u8>(), 0..256)) {
298            use ed25519_dalek::Signer;
299            let signing_key = SigningKey::from_bytes(&seed);
300            let sig = Signature::from_bytes(&signing_key.sign(&msg).to_bytes());
301            let value: Value<ED25519RComponent> = sig.to_value();
302            let bytes: ComponentBytes = value.from_value();
303            prop_assert_eq!(&bytes, sig.r_bytes());
304        }
305
306        #[test]
307        fn signature_s_component_roundtrip(seed in prop::array::uniform32(any::<u8>()), msg in prop::collection::vec(any::<u8>(), 0..256)) {
308            use ed25519_dalek::Signer;
309            let signing_key = SigningKey::from_bytes(&seed);
310            let sig = Signature::from_bytes(&signing_key.sign(&msg).to_bytes());
311            let value: Value<ED25519SComponent> = sig.to_value();
312            let bytes: ComponentBytes = value.from_value();
313            prop_assert_eq!(&bytes, sig.s_bytes());
314        }
315
316        #[test]
317        fn signature_r_s_reconstruct(seed in prop::array::uniform32(any::<u8>()), msg in prop::collection::vec(any::<u8>(), 0..256)) {
318            use ed25519_dalek::Signer;
319            let signing_key = SigningKey::from_bytes(&seed);
320            let sig = Signature::from_bytes(&signing_key.sign(&msg).to_bytes());
321            let r_val: Value<ED25519RComponent> = sig.to_value();
322            let s_val: Value<ED25519SComponent> = sig.to_value();
323            let r_bytes: ComponentBytes = r_val.from_value();
324            let s_bytes: ComponentBytes = s_val.from_value();
325            let mut combined = [0u8; 64];
326            combined[..32].copy_from_slice(&r_bytes);
327            combined[32..].copy_from_slice(&s_bytes);
328            let reconstructed = Signature::from_bytes(&combined);
329            prop_assert_eq!(sig, reconstructed);
330        }
331    }
332
333    // Invalid key bytes: specific error-case test.
334    #[test]
335    fn verifying_key_invalid_bytes() {
336        let mut raw = [0u8; 32];
337        raw[0] = 2;
338        let value: Value<ED25519PublicKey> = Value::new(raw);
339        assert!(VerifyingKey::try_from_value(&value).is_err());
340    }
341}