ssi/
public.rs

1// Self-sovereign identity
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Written in 2024 by
6//     Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
7//
8// Copyright (C) 2024 LNP/BP Standards Association. All rights reserved.
9//
10// Licensed under the Apache License, Version 2.0 (the "License");
11// you may not use this file except in compliance with the License.
12// You may obtain a copy of the License at
13//
14//     http://www.apache.org/licenses/LICENSE-2.0
15//
16// Unless required by applicable law or agreed to in writing, software
17// distributed under the License is distributed on an "AS IS" BASIS,
18// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19// See the License for the specific language governing permissions and
20// limitations under the License.
21
22use std::fmt::{self, Display, Formatter};
23use std::hash::Hash;
24use std::str::FromStr;
25
26use amplify::{hex, Bytes, Bytes32, Display};
27use baid64::{Baid64ParseError, DisplayBaid64, FromBaid64Str};
28use sha2::{Digest, Sha256};
29
30#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Display, Default)]
31#[non_exhaustive]
32pub enum Algo {
33    #[default]
34    #[display("ed25519")]
35    Ed25519,
36    #[display("bip340")]
37    Bip340,
38    #[display("other({0})")]
39    Other(u8),
40}
41
42#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display, Error)]
43#[display("unknown algorithm '{0}'")]
44pub struct UnknownAlgo(String);
45
46impl FromStr for Algo {
47    type Err = UnknownAlgo;
48
49    fn from_str(s: &str) -> Result<Self, Self::Err> {
50        match s {
51            "ed25519" | "Ed25519" | "ED25519" => Ok(Algo::Ed25519),
52            "bip340" | "Bip340" | "BIP340" => Ok(Algo::Bip340),
53            s => Err(UnknownAlgo(s.to_owned())),
54        }
55    }
56}
57
58impl From<Algo> for u8 {
59    fn from(algo: Algo) -> Self {
60        match algo {
61            Algo::Ed25519 => 0x13,
62            Algo::Bip340 => 0,
63            Algo::Other(v) => v,
64        }
65    }
66}
67
68impl From<u8> for Algo {
69    fn from(value: u8) -> Self {
70        match value {
71            0x13 => Algo::Ed25519,
72            0 => Algo::Bip340,
73            n => Algo::Other(n),
74        }
75    }
76}
77
78#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Display)]
79#[display(lowercase)]
80#[non_exhaustive]
81pub enum Chain {
82    #[default]
83    Bitcoin,
84    Liquid,
85    #[display("other({0})")]
86    Other(u8),
87}
88
89#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display, Error)]
90#[display("unknown chain '{0}'")]
91pub struct UnknownChain(String);
92
93impl FromStr for Chain {
94    type Err = UnknownChain;
95
96    fn from_str(s: &str) -> Result<Self, Self::Err> {
97        match s {
98            "bitcoin" => Ok(Chain::Bitcoin),
99            "liquid" => Ok(Chain::Liquid),
100            s => Err(UnknownChain(s.to_owned())),
101        }
102    }
103}
104
105impl From<Chain> for u8 {
106    fn from(chain: Chain) -> Self {
107        match chain {
108            Chain::Bitcoin => 0xB7,
109            Chain::Liquid => 0x10,
110            Chain::Other(v) => v,
111        }
112    }
113}
114
115impl From<u8> for Chain {
116    fn from(value: u8) -> Self {
117        match value {
118            0xB7 => Chain::Bitcoin,
119            0x10 => Chain::Liquid,
120            n => Chain::Other(n),
121        }
122    }
123}
124
125#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
126pub struct SsiPub {
127    pub(crate) chain: Chain,
128    pub(crate) algo: Algo,
129    pub(crate) key: Bytes<30>,
130}
131
132impl DisplayBaid64 for SsiPub {
133    const HRI: &'static str = "ssi";
134    const CHUNKING: bool = true;
135    const PREFIX: bool = true;
136    const EMBED_CHECKSUM: bool = false;
137    const MNEMONIC: bool = false;
138
139    fn to_baid64_payload(&self) -> [u8; 32] { <[u8; 32]>::from(*self) }
140}
141
142impl FromBaid64Str for SsiPub {}
143
144impl From<SsiPub> for [u8; 32] {
145    fn from(ssi: SsiPub) -> Self { ssi.to_byte_array() }
146}
147
148impl From<[u8; 32]> for SsiPub {
149    fn from(value: [u8; 32]) -> Self {
150        let key = Bytes::from_slice_unsafe(&value[0..30]);
151        let algo = Algo::from(value[30]);
152        let chain = Chain::from(value[31]);
153        Self { algo, key, chain }
154    }
155}
156
157impl SsiPub {
158    pub fn verify_text(self, text: &str, sig: SsiSig) -> Result<(), InvalidSig> {
159        let msg = Sha256::digest(text);
160        let digest = Sha256::digest(msg);
161        self.verify(digest.into(), sig)
162    }
163
164    pub fn verify(self, msg: [u8; 32], sig: SsiSig) -> Result<(), InvalidSig> {
165        match self.algo {
166            Algo::Ed25519 => self.verify_ed25519(msg, sig),
167            Algo::Bip340 => self.verify_bip360(msg, sig),
168            Algo::Other(other) => Err(InvalidSig::UnsupportedAlgo(other)),
169        }
170    }
171
172    pub fn fingerprint(self) -> Fingerprint {
173        Fingerprint([self.key[0], self.key[1], self.key[2], self.key[3], self.key[4], self.key[5]])
174    }
175
176    pub fn to_byte_array(&self) -> [u8; 32] {
177        let mut buf = [0u8; 32];
178        buf[0..30].copy_from_slice(self.key.as_slice());
179        buf[30] = self.algo.into();
180        buf[31] = self.chain.into();
181        buf
182    }
183}
184
185impl Display for SsiPub {
186    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
187        if !f.alternate() {
188            self.fmt_baid64(f)
189        } else {
190            write!(f, "{}", self.fingerprint())
191        }
192    }
193}
194
195impl FromStr for SsiPub {
196    type Err = Baid64ParseError;
197    fn from_str(s: &str) -> Result<Self, Self::Err> { Self::from_baid64_str(s) }
198}
199
200#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, From)]
201pub struct SsiSig(pub(crate) [u8; 64]);
202
203impl DisplayBaid64<64> for SsiSig {
204    const HRI: &'static str = "";
205    const CHUNKING: bool = false;
206    const PREFIX: bool = false;
207    const EMBED_CHECKSUM: bool = false;
208    const MNEMONIC: bool = false;
209
210    fn to_baid64_payload(&self) -> [u8; 64] { self.0 }
211}
212
213impl FromBaid64Str<64> for SsiSig {}
214
215impl FromStr for SsiSig {
216    type Err = Baid64ParseError;
217    fn from_str(s: &str) -> Result<Self, Self::Err> { Self::from_baid64_str(s) }
218}
219
220impl Display for SsiSig {
221    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_baid64(f) }
222}
223
224#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Display, Error)]
225#[display("invalid public key")]
226pub struct InvalidPubkey;
227
228#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Display, Error, From)]
229#[display(doc_comments)]
230pub enum InvalidSig {
231    /// invalid signature data.
232    InvalidData,
233
234    /// invalid identity public key.
235    #[from(InvalidPubkey)]
236    InvalidPubkey,
237
238    /// signature doesn't match the given identity and a message.
239    InvalidSig,
240
241    /// can't verify signature - unsupported signature method {0}.
242    UnsupportedAlgo(u8),
243}
244
245#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Display, From)]
246#[display(inner)]
247pub enum SsiQuery {
248    #[from]
249    Pub(SsiPub),
250    #[from]
251    Fp(Fingerprint),
252    #[from]
253    Id(String),
254}
255
256impl FromStr for SsiQuery {
257    type Err = Baid64ParseError;
258
259    fn from_str(s: &str) -> Result<Self, Self::Err> {
260        if s.len() == 8 {
261            Fingerprint::from_str(s).map(Self::Fp)
262        } else if s.starts_with("ssi:") || (s.contains('-') && (s.len() == 48 || s.len() == 52)) {
263            SsiPub::from_str(s).map(Self::Pub)
264        } else {
265            Ok(SsiQuery::Id(s.to_owned()))
266        }
267    }
268}
269
270#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, From)]
271pub struct Fingerprint([u8; 6]);
272
273impl DisplayBaid64<6> for Fingerprint {
274    const HRI: &'static str = "";
275    const CHUNKING: bool = false;
276    const PREFIX: bool = false;
277    const EMBED_CHECKSUM: bool = false;
278    const MNEMONIC: bool = false;
279
280    fn to_baid64_payload(&self) -> [u8; 6] { self.0 }
281}
282
283impl FromBaid64Str<6> for Fingerprint {}
284
285impl Display for Fingerprint {
286    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_baid64(f) }
287}
288
289impl FromStr for Fingerprint {
290    type Err = Baid64ParseError;
291
292    fn from_str(s: &str) -> Result<Self, Self::Err> { Self::from_baid64_str(s) }
293}
294
295#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
296pub struct SsiCert {
297    pub fp: Fingerprint,
298    pub pk: Option<SsiPub>,
299    pub msg: Bytes32,
300    pub sig: SsiSig,
301}
302
303#[derive(Debug, Display, Error, From)]
304#[display(inner)]
305pub enum VerifyError {
306    #[display("the certificate has no identity, verification impossible.")]
307    NoIdentity,
308    #[from]
309    InvalidSig(InvalidSig),
310    #[display("the provided text doesn't match the signed message")]
311    MessageMismatch,
312}
313
314impl SsiCert {
315    pub fn verify(&self) -> Result<(), VerifyError> {
316        let Some(pk) = self.pk else {
317            return Err(VerifyError::NoIdentity);
318        };
319        Ok(pk.verify(self.msg.to_byte_array(), self.sig)?)
320    }
321
322    pub fn verify_text(&self, text: &str) -> Result<(), VerifyError> {
323        let Some(pk) = self.pk else {
324            return Err(VerifyError::NoIdentity);
325        };
326        let msg = Sha256::digest(text);
327        let digest = Sha256::digest(msg);
328        let msg = <[u8; 32]>::from(digest);
329        if self.msg.to_byte_array() != msg {
330            return Err(VerifyError::MessageMismatch);
331        }
332        Ok(pk.verify(digest.into(), self.sig)?)
333    }
334}
335
336#[derive(Debug, Display, Error, From)]
337#[display(doc_comments)]
338pub enum CertParseError {
339    /// SSI URI lacks signature or message information.
340    DataMissed,
341    /// invalid certificate identity fingerprint - {0}.
342    InvalidFingerprint(Baid64ParseError),
343    /// invalid certificate identity key - {0}.
344    InvalidPub(Baid64ParseError),
345    /// invalid message digest - {0}.
346    #[from]
347    InvalidMessage(hex::Error),
348    #[from]
349    /// invalid signature data - {0}
350    InvalidSig(Baid64ParseError),
351}
352
353impl FromStr for SsiCert {
354    type Err = CertParseError;
355
356    fn from_str(s: &str) -> Result<Self, Self::Err> {
357        let (fp, rest) = s
358            .trim_start_matches("ssi:")
359            .split_once('?')
360            .ok_or(CertParseError::DataMissed)?;
361        let (msg, rest) = rest
362            .trim_start_matches("msg=")
363            .split_once('&')
364            .ok_or(CertParseError::DataMissed)?;
365        let sig = rest.trim_start_matches("sig=");
366        let (fp, pk) = match fp.len() {
367            8 => (Fingerprint::from_str(fp).map_err(CertParseError::InvalidFingerprint)?, None),
368            _ => {
369                let pk = SsiPub::from_str(fp).map_err(CertParseError::InvalidPub)?;
370                (pk.fingerprint(), Some(pk))
371            }
372        };
373        let msg = Bytes32::from_str(msg)?;
374        let sig = SsiSig::from_str(sig)?;
375        Ok(SsiCert { fp, pk, msg, sig })
376    }
377}
378
379impl Display for SsiCert {
380    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
381        if f.alternate() {
382            if let Some(pk) = self.pk {
383                return write!(f, "{pk}?msg={msg}&sig={sig}", msg = self.msg, sig = self.sig);
384            }
385        }
386        write!(f, "ssi:{fp}?msg={msg}&sig={sig}", fp = self.fp, msg = self.msg, sig = self.sig)
387    }
388}