Skip to main content

triblespace_core/inline/encodings/
ed25519.rs

1use crate::inline::Encodes;
2use ed25519::ComponentBytes;
3use ed25519::Signature;
4use ed25519_dalek::SignatureError;
5/// Re-export of the Ed25519 verifying (public) key type from [`ed25519_dalek`].
6pub use ed25519_dalek::VerifyingKey;
7
8use crate::id::ExclusiveId;
9use crate::id::Id;
10use crate::id_hex;
11use crate::macros::entity;
12use crate::metadata;
13use crate::metadata::MetaDescribe;
14use crate::trible::Fragment;
15use crate::inline::TryFromInline;
16use crate::inline::Inline;
17use crate::inline::InlineEncoding;
18use std::convert::Infallible;
19
20/// A inline encoding for the R component of an Ed25519 signature.
21pub struct ED25519RComponent;
22
23/// A inline encoding for the S component of an Ed25519 signature.
24pub struct ED25519SComponent;
25
26/// A inline encoding for an Ed25519 public key.
27pub struct ED25519PublicKey;
28
29impl MetaDescribe for ED25519RComponent {
30    fn describe() -> Fragment {
31        let id: Id = id_hex!("995A86FFC83DB95ECEAA17E226208897");
32        #[allow(unused_mut)]
33        let mut tribles = entity! {
34            ExclusiveId::force_ref(&id) @
35                metadata::name: "ed25519:r",
36                metadata::description: "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 encoding (for example LongString with base64 or a custom blob encoding).",
37                metadata::tag: metadata::KIND_INLINE_ENCODING,
38        };
39
40        #[cfg(feature = "wasm")]
41        {
42            tribles += entity! { ExclusiveId::force_ref(&id) @
43                metadata::value_formatter: wasm_formatter::ED25519_R_WASM,
44            };
45        }
46        tribles
47    }
48}
49impl InlineEncoding for ED25519RComponent {
50    type ValidationError = Infallible;
51    type Encoding = Self;
52}
53impl MetaDescribe for ED25519SComponent {
54    fn describe() -> Fragment {
55        let id: Id = id_hex!("10D35B0B628E9E409C549D8EC1FB3598");
56        #[allow(unused_mut)]
57        let mut tribles = entity! {
58            ExclusiveId::force_ref(&id) @
59                metadata::name: "ed25519:s",
60                metadata::description: "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.",
61                metadata::tag: metadata::KIND_INLINE_ENCODING,
62        };
63
64        #[cfg(feature = "wasm")]
65        {
66            tribles += entity! { ExclusiveId::force_ref(&id) @
67                metadata::value_formatter: wasm_formatter::ED25519_S_WASM,
68            };
69        }
70        tribles
71    }
72}
73impl InlineEncoding for ED25519SComponent {
74    type ValidationError = Infallible;
75    type Encoding = Self;
76}
77impl MetaDescribe for ED25519PublicKey {
78    fn describe() -> Fragment {
79        let id: Id = id_hex!("69A872254E01B4C1ED36E08E40445E93");
80        #[allow(unused_mut)]
81        let mut tribles = entity! {
82            ExclusiveId::force_ref(&id) @
83                metadata::name: "ed25519:pubkey",
84                metadata::description: "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.",
85                metadata::tag: metadata::KIND_INLINE_ENCODING,
86        };
87
88        #[cfg(feature = "wasm")]
89        {
90            tribles += entity! { ExclusiveId::force_ref(&id) @
91                metadata::value_formatter: wasm_formatter::ED25519_PUBKEY_WASM,
92            };
93        }
94        tribles
95    }
96}
97impl InlineEncoding for ED25519PublicKey {
98    type ValidationError = Infallible;
99    type Encoding = Self;
100}
101
102#[cfg(feature = "wasm")]
103mod wasm_formatter {
104    use core::fmt::Write;
105
106    use triblespace_core_macros::value_formatter;
107
108    #[value_formatter(const_wasm = ED25519_R_WASM)]
109    pub(crate) fn ed25519_r(raw: &[u8; 32], out: &mut impl Write) -> Result<(), u32> {
110        out.write_str("ed25519:r:").map_err(|_| 1u32)?;
111        const TABLE: &[u8; 16] = b"0123456789ABCDEF";
112        for &byte in raw {
113            let hi = (byte >> 4) as usize;
114            let lo = (byte & 0x0F) as usize;
115            out.write_char(TABLE[hi] as char).map_err(|_| 1u32)?;
116            out.write_char(TABLE[lo] as char).map_err(|_| 1u32)?;
117        }
118        Ok(())
119    }
120
121    #[value_formatter(const_wasm = ED25519_S_WASM)]
122    pub(crate) fn ed25519_s(raw: &[u8; 32], out: &mut impl Write) -> Result<(), u32> {
123        out.write_str("ed25519:s:").map_err(|_| 1u32)?;
124        const TABLE: &[u8; 16] = b"0123456789ABCDEF";
125        for &byte in raw {
126            let hi = (byte >> 4) as usize;
127            let lo = (byte & 0x0F) as usize;
128            out.write_char(TABLE[hi] as char).map_err(|_| 1u32)?;
129            out.write_char(TABLE[lo] as char).map_err(|_| 1u32)?;
130        }
131        Ok(())
132    }
133
134    #[value_formatter(const_wasm = ED25519_PUBKEY_WASM)]
135    pub(crate) fn ed25519_pubkey(raw: &[u8; 32], out: &mut impl Write) -> Result<(), u32> {
136        out.write_str("ed25519:pubkey:").map_err(|_| 1u32)?;
137        const TABLE: &[u8; 16] = b"0123456789ABCDEF";
138        for &byte in raw {
139            let hi = (byte >> 4) as usize;
140            let lo = (byte & 0x0F) as usize;
141            out.write_char(TABLE[hi] as char).map_err(|_| 1u32)?;
142            out.write_char(TABLE[lo] as char).map_err(|_| 1u32)?;
143        }
144        Ok(())
145    }
146}
147
148impl ED25519RComponent {
149    /// Extracts the R component from a full Ed25519 signature.
150    pub fn from_signature(s: Signature) -> Inline<ED25519RComponent> {
151        Inline::new(*s.r_bytes())
152    }
153}
154
155impl ED25519SComponent {
156    /// Extracts the S component from a full Ed25519 signature.
157    pub fn from_signature(s: Signature) -> Inline<ED25519SComponent> {
158        Inline::new(*s.s_bytes())
159    }
160}
161
162impl Encodes<Signature> for ED25519RComponent
163{
164    type Output = Inline<ED25519RComponent>;
165    fn encode(source: Signature) -> Inline<ED25519RComponent> {
166        ED25519RComponent::from_signature(source)
167    }
168}
169
170impl Encodes<Signature> for ED25519SComponent
171{
172    type Output = Inline<ED25519SComponent>;
173    fn encode(source: Signature) -> Inline<ED25519SComponent> {
174        ED25519SComponent::from_signature(source)
175    }
176}
177
178impl Encodes<ComponentBytes> for ED25519RComponent
179{
180    type Output = Inline<ED25519RComponent>;
181    fn encode(source: ComponentBytes) -> Inline<ED25519RComponent> {
182        Inline::new(source)
183    }
184}
185
186impl TryFromInline<'_, ED25519RComponent> for ComponentBytes {
187    type Error = Infallible;
188    fn try_from_inline(v: &Inline<ED25519RComponent>) -> Result<Self, Infallible> {
189        Ok(v.raw)
190    }
191}
192
193impl Encodes<ComponentBytes> for ED25519SComponent
194{
195    type Output = Inline<ED25519SComponent>;
196    fn encode(source: ComponentBytes) -> Inline<ED25519SComponent> {
197        Inline::new(source)
198    }
199}
200
201impl TryFromInline<'_, ED25519SComponent> for ComponentBytes {
202    type Error = Infallible;
203    fn try_from_inline(v: &Inline<ED25519SComponent>) -> Result<Self, Infallible> {
204        Ok(v.raw)
205    }
206}
207
208impl Encodes<VerifyingKey> for ED25519PublicKey
209{
210    type Output = Inline<ED25519PublicKey>;
211    fn encode(source: VerifyingKey) -> Inline<ED25519PublicKey> {
212        Inline::new(source.to_bytes())
213    }
214}
215
216impl TryFromInline<'_, ED25519PublicKey> for VerifyingKey {
217    type Error = SignatureError;
218
219    fn try_from_inline(v: &Inline<ED25519PublicKey>) -> Result<Self, Self::Error> {
220        VerifyingKey::from_bytes(&v.raw)
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227    use crate::inline::{IntoInline, TryFromInline};
228    use ed25519::Signature;
229    use ed25519_dalek::SigningKey;
230    use proptest::prelude::*;
231
232    proptest! {
233        #[test]
234        fn r_component_bytes_roundtrip(input in prop::array::uniform32(any::<u8>())) {
235            let value: Inline<ED25519RComponent> = input.to_inline();
236            let output: ComponentBytes = value.from_inline();
237            prop_assert_eq!(input, output);
238        }
239
240        #[test]
241        fn s_component_bytes_roundtrip(input in prop::array::uniform32(any::<u8>())) {
242            let value: Inline<ED25519SComponent> = input.to_inline();
243            let output: ComponentBytes = value.from_inline();
244            prop_assert_eq!(input, output);
245        }
246
247        #[test]
248        fn r_component_validates(input in prop::array::uniform32(any::<u8>())) {
249            let value: Inline<ED25519RComponent> = input.to_inline();
250            prop_assert!(ED25519RComponent::validate(value).is_ok());
251        }
252
253        #[test]
254        fn s_component_validates(input in prop::array::uniform32(any::<u8>())) {
255            let value: Inline<ED25519SComponent> = input.to_inline();
256            prop_assert!(ED25519SComponent::validate(value).is_ok());
257        }
258
259        #[test]
260        fn pubkey_validates(seed in prop::array::uniform32(any::<u8>())) {
261            let key = SigningKey::from_bytes(&seed).verifying_key();
262            let value: Inline<ED25519PublicKey> = key.to_inline();
263            prop_assert!(ED25519PublicKey::validate(value).is_ok());
264        }
265
266        #[test]
267        fn verifying_key_roundtrip(seed in prop::array::uniform32(any::<u8>())) {
268            let key = SigningKey::from_bytes(&seed).verifying_key();
269            let value: Inline<ED25519PublicKey> = key.to_inline();
270            let recovered = VerifyingKey::try_from_inline(&value).expect("valid key");
271            prop_assert_eq!(key, recovered);
272        }
273
274        #[test]
275        fn signature_r_component_roundtrip(seed in prop::array::uniform32(any::<u8>()), msg in prop::collection::vec(any::<u8>(), 0..256)) {
276            use ed25519_dalek::Signer;
277            let signing_key = SigningKey::from_bytes(&seed);
278            let sig = Signature::from_bytes(&signing_key.sign(&msg).to_bytes());
279            let value: Inline<ED25519RComponent> = sig.to_inline();
280            let bytes: ComponentBytes = value.from_inline();
281            prop_assert_eq!(&bytes, sig.r_bytes());
282        }
283
284        #[test]
285        fn signature_s_component_roundtrip(seed in prop::array::uniform32(any::<u8>()), msg in prop::collection::vec(any::<u8>(), 0..256)) {
286            use ed25519_dalek::Signer;
287            let signing_key = SigningKey::from_bytes(&seed);
288            let sig = Signature::from_bytes(&signing_key.sign(&msg).to_bytes());
289            let value: Inline<ED25519SComponent> = sig.to_inline();
290            let bytes: ComponentBytes = value.from_inline();
291            prop_assert_eq!(&bytes, sig.s_bytes());
292        }
293
294        #[test]
295        fn signature_r_s_reconstruct(seed in prop::array::uniform32(any::<u8>()), msg in prop::collection::vec(any::<u8>(), 0..256)) {
296            use ed25519_dalek::Signer;
297            let signing_key = SigningKey::from_bytes(&seed);
298            let sig = Signature::from_bytes(&signing_key.sign(&msg).to_bytes());
299            let r_val: Inline<ED25519RComponent> = sig.to_inline();
300            let s_val: Inline<ED25519SComponent> = sig.to_inline();
301            let r_bytes: ComponentBytes = r_val.from_inline();
302            let s_bytes: ComponentBytes = s_val.from_inline();
303            let mut combined = [0u8; 64];
304            combined[..32].copy_from_slice(&r_bytes);
305            combined[32..].copy_from_slice(&s_bytes);
306            let reconstructed = Signature::from_bytes(&combined);
307            prop_assert_eq!(sig, reconstructed);
308        }
309    }
310
311    // Invalid key bytes: specific error-case test.
312    #[test]
313    fn verifying_key_invalid_bytes() {
314        let mut raw = [0u8; 32];
315        raw[0] = 2;
316        let value: Inline<ED25519PublicKey> = Inline::new(raw);
317        assert!(VerifyingKey::try_from_inline(&value).is_err());
318    }
319}