Skip to main content

ssi_tzkey/
lib.rs

1use core::convert::TryFrom;
2use ssi_jwk::{Algorithm, Base64urlUInt, OctetParams, Params, JWK};
3use ssi_jws::Error as JwsError;
4
5const EDPK_PREFIX: [u8; 4] = [13, 15, 37, 217];
6const SPPK_PREFIX: [u8; 4] = [3, 254, 226, 86];
7const P2PK_PREFIX: [u8; 4] = [3, 178, 139, 127];
8
9pub fn jwk_to_tezos_key(jwk: &JWK) -> Result<String, JwsError> {
10    let mut tzkey_prefixed = Vec::new();
11    let bytes;
12    let (prefix, bytes) = match &jwk.params {
13        Params::OKP(okp_params) if okp_params.curve == "Ed25519" => {
14            if let Some(ref _sk) = okp_params.private_key {
15                // TODO: edsk
16                return Err(JwsError::UnsupportedAlgorithm(
17                    jwk.algorithm
18                        .as_ref()
19                        .map(ToString::to_string)
20                        .unwrap_or_else(|| "OKP".to_string()),
21                ));
22            }
23            (EDPK_PREFIX, &okp_params.public_key.0)
24        }
25        Params::EC(ec_params) if ec_params.curve == Some("secp256k1".to_string()) => {
26            if let Some(ref _sk) = ec_params.ecc_private_key {
27                // TODO: spsk
28                return Err(JwsError::UnsupportedAlgorithm(
29                    jwk.algorithm
30                        .as_ref()
31                        .map(ToString::to_string)
32                        .unwrap_or_else(|| "EC".to_string()),
33                ));
34            }
35            {
36                // TODO: p2sk
37                bytes = ssi_jwk::serialize_secp256k1(ec_params)?;
38                (SPPK_PREFIX, &bytes)
39            }
40        }
41        Params::EC(ec_params) if ec_params.curve == Some("P-256".to_string()) => {
42            if let Some(ref _sk) = ec_params.ecc_private_key {
43                return Err(JwsError::UnsupportedAlgorithm(
44                    jwk.algorithm
45                        .as_ref()
46                        .map(ToString::to_string)
47                        .unwrap_or_else(|| "EC".to_string()),
48                ));
49            }
50            {
51                bytes = ssi_jwk::serialize_p256(ec_params)?;
52                (P2PK_PREFIX, &bytes)
53            }
54        }
55        _ => {
56            return Err(JwsError::UnsupportedAlgorithm(
57                jwk.algorithm
58                    .as_ref()
59                    .map(ToString::to_string)
60                    .unwrap_or_else(|| "OCT".to_string()),
61            ));
62        }
63    };
64    tzkey_prefixed.extend_from_slice(&prefix);
65    tzkey_prefixed.extend_from_slice(bytes);
66    let tzkey = bs58::encode(tzkey_prefixed).with_check().into_string();
67    Ok(tzkey)
68}
69
70#[derive(thiserror::Error, Debug)]
71pub enum DecodeTezosPkError {
72    #[error("Key Prefix")]
73    KeyPrefix,
74    #[error(transparent)]
75    B58(#[from] bs58::decode::Error),
76    #[error(transparent)]
77    JWK(#[from] ssi_jwk::Error),
78}
79
80/// Parse a Tezos key string into a JWK.
81pub fn jwk_from_tezos_key(tz_pk: &str) -> Result<JWK, DecodeTezosPkError> {
82    if tz_pk.len() < 4 {
83        return Err(DecodeTezosPkError::KeyPrefix);
84    }
85    let (alg, params) = match tz_pk.get(..4) {
86        Some("edpk") => (
87            Algorithm::EdBlake2b,
88            Params::OKP(OctetParams {
89                curve: "Ed25519".into(),
90                public_key: Base64urlUInt(
91                    bs58::decode(&tz_pk).with_check(None).into_vec()?[4..].to_owned(),
92                ),
93                private_key: None,
94            }),
95        ),
96        Some("edsk") => {
97            let sk_bytes = bs58::decode(&tz_pk).with_check(None).into_vec()?[4..].to_owned();
98            let pk_bytes;
99            {
100                let sk = ed25519_dalek::SigningKey::try_from(sk_bytes.as_slice())
101                    .map_err(ssi_jwk::Error::from)?;
102                pk_bytes = ed25519_dalek::VerifyingKey::from(&sk).as_bytes().to_vec()
103            }
104            (
105                Algorithm::EdBlake2b,
106                Params::OKP(OctetParams {
107                    curve: "Ed25519".into(),
108                    public_key: Base64urlUInt(pk_bytes),
109                    private_key: Some(Base64urlUInt(sk_bytes)),
110                }),
111            )
112        }
113        Some("sppk") => {
114            let pk_bytes = bs58::decode(&tz_pk).with_check(None).into_vec()?[4..].to_owned();
115            let jwk = ssi_jwk::secp256k1_parse(&pk_bytes)?;
116            (Algorithm::ESBlake2bK, jwk.params)
117        }
118        Some("p2pk") => {
119            let pk_bytes = bs58::decode(&tz_pk).with_check(None).into_vec()?[4..].to_owned();
120            let jwk = ssi_jwk::p256_parse(&pk_bytes)?;
121            (Algorithm::ESBlake2b, jwk.params)
122        }
123        // TODO: more secret keys
124        _ => return Err(DecodeTezosPkError::KeyPrefix),
125    };
126    Ok(JWK {
127        public_key_use: None,
128        key_operations: None,
129        algorithm: Some(alg),
130        key_id: None,
131        x509_url: None,
132        x509_certificate_chain: None,
133        x509_thumbprint_sha1: None,
134        x509_thumbprint_sha256: None,
135        params,
136    })
137}
138
139#[derive(thiserror::Error, Debug)]
140pub enum SignTezosError {
141    #[error("Unsupported algorithm for Tezos signing: {0:?}")]
142    UnsupportedAlgorithm(Algorithm),
143    #[error("Signing: {0}")]
144    Sign(String),
145}
146
147pub fn sign_tezos(data: &[u8], algorithm: Algorithm, key: &JWK) -> Result<String, SignTezosError> {
148    let sig = ssi_jws::sign_bytes(algorithm, data, key)
149        .map_err(|e| SignTezosError::Sign(e.to_string()))?;
150    let mut sig_prefixed = Vec::new();
151    const EDSIG_PREFIX: [u8; 5] = [9, 245, 205, 134, 18];
152    const SPSIG_PREFIX: [u8; 5] = [13, 115, 101, 19, 63];
153    const P2SIG_PREFIX: [u8; 4] = [54, 240, 44, 52];
154    let prefix: &[u8] = match algorithm {
155        Algorithm::EdBlake2b => &EDSIG_PREFIX,
156        Algorithm::ESBlake2bK => &SPSIG_PREFIX,
157        Algorithm::ESBlake2b => &P2SIG_PREFIX,
158        alg => return Err(SignTezosError::UnsupportedAlgorithm(alg)),
159    };
160    sig_prefixed.extend_from_slice(prefix);
161    sig_prefixed.extend_from_slice(&sig);
162    let sig_bs58 = bs58::encode(sig_prefixed).with_check().into_string();
163    Ok(sig_bs58)
164}
165
166#[derive(thiserror::Error, Debug)]
167pub enum EncodeTezosSignedMessageError {
168    #[error("Message length conversion error: {0}")]
169    Length(#[from] core::num::TryFromIntError),
170}
171
172pub fn encode_tezos_signed_message(msg: &str) -> Result<Vec<u8>, EncodeTezosSignedMessageError> {
173    const BYTES_PREFIX: [u8; 2] = [0x05, 0x01];
174    let msg_bytes = msg.as_bytes();
175    let mut bytes = Vec::with_capacity(msg_bytes.len());
176    let prefix = b"Tezos Signed Message: ";
177    let msg_len = prefix.len() + msg_bytes.len();
178
179    let len_u32 = u32::try_from(msg_len).map_err(EncodeTezosSignedMessageError::Length)?;
180    bytes.extend_from_slice(&BYTES_PREFIX);
181    bytes.extend_from_slice(&len_u32.to_be_bytes());
182    bytes.extend_from_slice(prefix);
183    bytes.extend_from_slice(msg_bytes);
184    Ok(bytes)
185}
186
187#[derive(thiserror::Error, Debug)]
188pub enum DecodeTezosSignatureError {
189    #[error("Expected signature length {0} but found {1}")]
190    SignatureLength(usize, usize),
191    #[error("Unknown signature prefix: {0}")]
192    SignaturePrefix(String),
193    #[error("Base58 decoding: {0}")]
194    Base58(#[from] bs58::decode::Error),
195}
196
197pub fn decode_tzsig(sig_bs58: &str) -> Result<(Algorithm, Vec<u8>), DecodeTezosSignatureError> {
198    let tzsig = bs58::decode(&sig_bs58).with_check(None).into_vec()?;
199    if tzsig.len() < 5 {
200        return Err(DecodeTezosSignatureError::SignaturePrefix(
201            sig_bs58.to_string(),
202        ));
203    }
204    // sig_bs58 has been checked as base58. But use the non-panicking get function anyway, for good
205    // measure.
206    let (algorithm, sig) = match sig_bs58.get(0..5) {
207        Some("edsig") => (Algorithm::EdBlake2b, tzsig[5..].to_vec()),
208        Some("spsig") => (Algorithm::ESBlake2bK, tzsig[5..].to_vec()),
209        Some("p2sig") => (Algorithm::ESBlake2b, tzsig[4..].to_vec()),
210        _ => {
211            return Err(DecodeTezosSignatureError::SignaturePrefix(
212                sig_bs58.to_string(),
213            ))
214        }
215    };
216    if sig.len() != 64 {
217        return Err(DecodeTezosSignatureError::SignatureLength(64, sig.len()));
218    }
219    Ok((algorithm, sig))
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use serde_json::json;
226    use ssi_jwk::blakesig::hash_public_key;
227
228    #[test]
229    fn test_jwk_from_tezos_key_glyph_split() {
230        // Attempt to decode tzsig that would involve subslicing
231        // through a char boundary.
232        let bad_tzk = "xx💣️";
233        jwk_from_tezos_key(bad_tzk).unwrap_err();
234    }
235
236    #[test]
237    fn edpk_jwk_tz_edsig() {
238        let tzpk = "edpkuxZ5AQVCeEJ9inUG3w6VFhio5KBwC22ekPLBzcvub3QY2DvJ7n";
239        let jwk = jwk_from_tezos_key(tzpk).unwrap();
240        assert_eq!(jwk_to_tezos_key(&jwk).unwrap(), tzpk);
241        let jwk_expected: JWK = serde_json::from_value(json!(
242            {"alg":"EdBlake2b","kty":"OKP","crv":"Ed25519","x":"rVEB0Icbomw1Ir-ck52iCZl1SICc5lCg2pxI8AmydDw"}
243        )).unwrap();
244        assert_eq!(jwk, jwk_expected);
245        let hash = hash_public_key(&jwk).unwrap();
246        assert_eq!(hash, "tz1TwZZZSShtM73oEr74aDtDcns3UmFqaca6");
247        let mut tsm =
248            encode_tezos_signed_message("example.org 2021-05-25T18:54:42Z Signed with Temple tz1")
249                .unwrap();
250        let tsm_expected_hex = "05010000004d54657a6f73205369676e6564204d6573736167653a206578616d706c652e6f726720323032312d30352d32355431383a35343a34325a205369676e656420776974682054656d706c6520747a31";
251        assert_eq!(hex::encode(&tsm), tsm_expected_hex);
252
253        let sig_bs58 = "edsigtpfAeN8PqB9dpYdzDTpbCFPFuNe6Vfo4pokAQWYzFczZCjkWtfmWH3zxsse1KbxSc2NksTfoAMJzpqCAee4PQL6gydVWyy";
254        let (_, sig) = decode_tzsig(sig_bs58).unwrap();
255        ssi_jws::verify_bytes(Algorithm::EdBlake2b, &tsm, &jwk, &sig).unwrap();
256
257        // Negative test: alter signing input
258        tsm[1] ^= 1;
259        ssi_jws::verify_bytes(Algorithm::EdBlake2b, &tsm, &jwk, &sig).unwrap_err();
260    }
261
262    // Signature produced by Kukai wallet
263    #[test]
264    fn sppk_jwk_tz_spsig() {
265        let tzpk = "sppk7bYNanLcEPRpvLc231GBC8i6YfLBbQjiQMbz8kriz9qxASf5wHw";
266        let jwk = jwk_from_tezos_key(tzpk).unwrap();
267        assert_eq!(jwk_to_tezos_key(&jwk).unwrap(), tzpk);
268        let jwk_expected: JWK = serde_json::from_value(json!(
269            {"alg":"ESBlake2bK","kty":"EC","crv":"secp256k1","x":"JpVAlV0nDVVmPnSNdZTqes8YXoQqzyBq9R1VHWhBdgY","y":"G2jCkm3F3uu-TqtgrqCji13-MR-tlND2Tqt8rh7ZPN8"}
270        )).unwrap();
271        assert_eq!(jwk, jwk_expected);
272        let hash = hash_public_key(&jwk).unwrap();
273        assert_eq!(hash, "tz2EzZh8dhciPgTh4azWhgKCNz3HRh2ZvUhA");
274        let mut tsm =
275            encode_tezos_signed_message("example.org 2021-05-25T20:10:03Z Signed with Kukai tz2")
276                .unwrap();
277        let tsm_expected_hex = "05010000004c54657a6f73205369676e6564204d6573736167653a206578616d706c652e6f726720323032312d30352d32355432303a31303a30335a205369676e65642077697468204b756b616920747a32";
278        assert_eq!(hex::encode(&tsm), tsm_expected_hex);
279
280        let sig_bs58 = "spsig1TJjahaMVSCyvSBPvHJFXQ1WxASgHsygTvxgJxAWbYsv5R9nH1yzj5BEeHoqmHCogVYioVCbeKDNDwP17hMaM9foFdF8SS";
281        let (_, sig) = decode_tzsig(sig_bs58).unwrap();
282        ssi_jws::verify_bytes(Algorithm::ESBlake2bK, &tsm, &jwk, &sig).unwrap();
283        tsm[1] ^= 1;
284        ssi_jws::verify_bytes(Algorithm::ESBlake2bK, &tsm, &jwk, &sig).unwrap_err();
285    }
286
287    #[test]
288    fn p2pk_jwk() {
289        let tzpk = "p2pk679D18uQNkdjpRxuBXL5CqcDKTKzsiXVtc9oCUT6xb82zQmgUks";
290        let jwk = jwk_from_tezos_key(tzpk).unwrap();
291        let jwk_expected: JWK = serde_json::from_value(json!(
292            {"alg":"ESBlake2b","kty":"EC","crv":"P-256","x":"UmzXjEZzlGmpaM_CmFEJtOO5JBntW8yl_fM1LEQlWQ4","y":"OmoZmcbUadg7dEC8bg5kXryN968CJqv2UFMUKRERZ6s"}
293        )).unwrap();
294        assert_eq!(jwk, jwk_expected);
295        assert_eq!(jwk_to_tezos_key(&jwk).unwrap(), tzpk);
296        // TODO: verify signature made by another implementation
297    }
298
299    #[test]
300    fn edsk_sign() {
301        let mut key: JWK =
302            serde_json::from_str(include_str!("../../../tests/ed25519-2020-10-18.json")).unwrap();
303        key.algorithm = Some(Algorithm::EdBlake2b);
304        eprintln!("key: {:?}", key);
305        let hash = hash_public_key(&key).unwrap();
306        assert_eq!(hash, "tz1NcJyMQzUw7h85baBA6vwRGmpwPnM1fz83");
307        let tsm = encode_tezos_signed_message("example.org 2021-05-26T18:28:26Z Signed with ssi")
308            .unwrap();
309        eprintln!("msg: {:?}", tsm);
310        let sig = sign_tezos(&tsm, Algorithm::EdBlake2b, &key).unwrap();
311        let sig_expected = "edsigtvvyq6uFWyeoSNZq4Jq2AvsNGZ9hHYDgt4Hzdou4FVkaBLX34tWRyL9MsapFBg3RFXReJ4bNCaAg2F1XWAMgetCLU9AACo";
312        assert_eq!(sig, sig_expected);
313    }
314
315    #[test]
316    fn spsk_sign() {
317        let key: JWK = serde_json::from_value(json!({
318            "alg": "ESBlake2bK",
319            "kty": "EC",
320            "crv": "secp256k1",
321            "x": "yclqMZ0MtyVkKm1eBh2AyaUtsqT0l5RJM3g4SzRT96A",
322            "y": "yQzUwKnftWCJPGs-faGaHiYi1sxA6fGJVw2Px_LCNe8",
323            "d": "meTmccmR_6ZsOa2YuTTkKkJ4ZPYsKdAH1Wx_RRf2j_E"
324        }))
325        .unwrap();
326        eprintln!("key {:?}", key);
327        let hash = hash_public_key(&key).unwrap();
328        assert_eq!(hash, "tz2CA2f3SWWcqbWsjHsMZPZxCY5iafSN3nDz");
329        let tsm = encode_tezos_signed_message("example.org 2021-05-26T17:01:41Z Signed with ssi")
330            .unwrap();
331        eprintln!("msg: {:x?}", tsm);
332        let sig = sign_tezos(&tsm, Algorithm::ESBlake2bK, &key).unwrap();
333        let sig_expected = "spsig1NRgjYaq8jeaWTMPUSsxkawWUzW1C3RoMfczWY2JAZSkNQQGM9QvCkxtRMcauJRaSUNcKgkj6WfpzLh1upXwjcfLh4wqqX";
334        assert_eq!(sig, sig_expected);
335    }
336}