sharpie/
ed.rs

1use ring::signature;
2use snafu::ResultExt;
3
4/// A private key source for `ED25519`
5pub enum PrivateKey {
6    ///  openssl genpkey -algorithm ED25519 -out private-key-ed.pem
7    PEM(String),
8    ///  openssl genpkey -algorithm ED25519 -outform DER -out private-key-ed.pem
9    DER(Vec<u8>),
10    ///  raw key bytes created with `ring` to be loaded with PKCS8v2 only
11    Raw(Vec<u8>),
12}
13
14impl PrivateKey {
15    /// Reads a [`PrivateKey`].
16    ///
17    /// # Errors
18    ///
19    /// This function will return an error if IO fails
20    pub fn read(&self) -> crate::Result<PrivateKeyBytes> {
21        Ok(match self {
22            Self::PEM(s) => {
23                let p = pem::parse(s).context(crate::InvalidPemSnafu)?;
24                PrivateKeyBytes::PKCS8v1v2(p.contents)
25            }
26            Self::DER(b) => PrivateKeyBytes::PKCS8v1v2(b.clone()),
27            Self::Raw(b) => PrivateKeyBytes::PKCS8v2(b.clone()),
28        })
29    }
30}
31
32/// Binary form of a raw ED25519 key
33pub enum PrivateKeyBytes {
34    /// represents the encoding used normally by openssl generated keys
35    PKCS8v1v2(Vec<u8>),
36    /// represents the encoding used by ring or more modern tools
37    PKCS8v2(Vec<u8>),
38}
39
40/// A public key source for `ED25519`
41pub enum PublicKey {
42    /// openssl pkey -in private-key-ed.pem -pubout -out public-key-ed.pem
43    PEM(String),
44    /// openssl pkey -in private-key-ed.pem -pubout -outform DER -out public-key-ed.pem
45    DER(Vec<u8>),
46    ///  raw key bytes created with `ring`
47    Raw(Vec<u8>),
48}
49
50impl PublicKey {
51    /// Reads a [`PublicKey`].
52    ///
53    /// # Errors
54    ///
55    /// This function will return an error if decoding fails
56    pub fn read(&self) -> crate::Result<PublicKeyBytes> {
57        Ok(PublicKeyBytes::Raw(match self {
58            Self::PEM(s) => {
59                let p = pem::parse(s).context(crate::InvalidPemSnafu)?;
60                p.contents.iter().skip(12).copied().collect::<Vec<_>>()
61            }
62            Self::DER(bs) => bs.iter().skip(12).copied().collect::<Vec<_>>(),
63            Self::Raw(bs) => bs.clone(),
64        }))
65    }
66}
67
68/// Binary form of a raw ED25519 public key
69pub enum PublicKeyBytes {
70    /// the precise key bytes without header or encoding
71    Raw(Vec<u8>),
72}
73
74/// Verify an ED25519 signature
75///
76/// # Errors
77///
78/// This function will return an error if verify failed
79pub fn verify(msg: &[u8], sig: &[u8], pubkey: &PublicKeyBytes) -> crate::Result<()> {
80    // ASN.1 encoding for ed25519 always has a 12 byte header which we cut off to get
81    // to the raw key
82    // see https://mta.openssl.org/pipermail/openssl-users/2018-March/007777.html
83    let PublicKeyBytes::Raw(key_bytes) = pubkey;
84
85    let peer_public_key = signature::UnparsedPublicKey::new(&signature::ED25519, &key_bytes);
86    peer_public_key
87        .verify(msg, sig)
88        .context(crate::SignatureFailedSnafu)?;
89
90    Ok(())
91}
92
93/// Sign a message using ED25519
94///
95/// # Errors
96///
97/// This function will return an error if signing failed
98pub fn sign(msg: &[u8], prkey: &PrivateKeyBytes) -> crate::Result<Vec<u8>> {
99    let key_pair = match prkey {
100        PrivateKeyBytes::PKCS8v1v2(b) => {
101            signature::Ed25519KeyPair::from_pkcs8_maybe_unchecked(b).context(crate::InvalidKeySnafu)
102        }
103        PrivateKeyBytes::PKCS8v2(b) => {
104            signature::Ed25519KeyPair::from_pkcs8(b).context(crate::InvalidKeySnafu)
105        }
106    }?;
107
108    let sig = key_pair.sign(msg);
109    Ok(sig.as_ref().to_vec())
110}
111
112/// Sign a message using ED25519 and encode as base64
113///
114/// # Errors
115///
116/// This function will return an error if signing failed
117#[cfg(feature = "base64")]
118pub fn sign_base64(msg: &[u8], privkey: &PrivateKeyBytes) -> crate::Result<String> {
119    use base64::{prelude::BASE64_STANDARD, Engine};
120    let sig = sign(msg, privkey)?;
121    Ok(BASE64_STANDARD.encode(sig))
122}
123
124/// Verify an ED25519 signature, where the signature is base64 encoded
125///
126/// # Errors
127///
128/// This function will return an error if verify failed
129#[cfg(feature = "base64")]
130pub fn verify_base64(msg: &[u8], sig: &str, pubkey: &PublicKeyBytes) -> crate::Result<()> {
131    use base64::{prelude::BASE64_STANDARD, Engine};
132    let sig = BASE64_STANDARD
133        .decode(sig)
134        .context(crate::DecodeFailedSnafu)?;
135    verify(msg, &sig, pubkey)
136}
137
138#[cfg(test)]
139mod tests {
140    use ring::{rand, signature::KeyPair};
141
142    use super::*;
143    use std::{fs, path::Path, process::Command};
144
145    #[test]
146    fn test_negatives() {
147        let privkey = PrivateKey::PEM(
148            String::from_utf8_lossy(&fs::read("fixtures/ed.private.pem").unwrap()).to_string(),
149        )
150        .read()
151        .unwrap();
152        let sig = sign(b"hello world", &privkey).expect("should sign");
153        let pubkey = PublicKey::PEM(
154            String::from_utf8_lossy(&fs::read("fixtures/ed.public.pem").unwrap()).to_string(),
155        );
156
157        verify(b"hello world_", &sig, &pubkey.read().unwrap()).expect_err("should fail");
158
159        // wrong key
160        let pubkey = PublicKey::PEM(
161            String::from_utf8_lossy(&fs::read("fixtures/rsa.public.pem").unwrap()).to_string(),
162        );
163        verify(b"hello world", &sig, &pubkey.read().unwrap()).expect_err("should fail");
164
165        // wrong format
166        let pubkey = PublicKey::DER(fs::read("fixtures/ed.public.pem").unwrap());
167        verify(b"hello world", &sig, &pubkey.read().unwrap()).expect_err("should fail");
168    }
169
170    #[test]
171    fn test_pem() {
172        let privkey = PrivateKey::PEM(
173            String::from_utf8_lossy(&fs::read("fixtures/ed.private.pem").unwrap()).to_string(),
174        )
175        .read()
176        .unwrap();
177        let sig = sign(b"hello world", &privkey).expect("should sign");
178        let pubkey = PublicKey::PEM(
179            String::from_utf8_lossy(&fs::read("fixtures/ed.public.pem").unwrap()).to_string(),
180        );
181        verify(b"hello world", &sig, &pubkey.read().unwrap()).expect("should verify");
182    }
183
184    #[cfg(feature = "base64")]
185    #[test]
186    fn test_base64() {
187        let privkey = PrivateKey::PEM(
188            String::from_utf8_lossy(&fs::read("fixtures/ed.private.pem").unwrap()).to_string(),
189        )
190        .read()
191        .unwrap();
192        let sig = sign_base64(b"hello world", &privkey).expect("should sign");
193        let pubkey = PublicKey::PEM(
194            String::from_utf8_lossy(&fs::read("fixtures/ed.public.pem").unwrap()).to_string(),
195        );
196        verify_base64(b"hello world", &sig, &pubkey.read().unwrap()).expect("should verify");
197    }
198
199    #[test]
200    fn test_pkcs8v2() {
201        // Generate a key pair in PKCS#8 (v2) format.
202        let rng = rand::SystemRandom::new();
203        let pkcs8_bytes = signature::Ed25519KeyPair::generate_pkcs8(&rng)
204            .unwrap()
205            .as_ref()
206            .to_vec();
207        let key_pair = signature::Ed25519KeyPair::from_pkcs8(pkcs8_bytes.as_ref()).unwrap();
208
209        let sig = sign(
210            b"hello world",
211            &PrivateKey::Raw(pkcs8_bytes).read().unwrap(),
212        )
213        .expect("should sign");
214        let pubkey = PublicKey::Raw(key_pair.public_key().as_ref().to_vec());
215        verify(b"hello world", &sig, &pubkey.read().unwrap()).expect("should verify");
216    }
217
218    #[test]
219    fn test_nodejs_signed() {
220        let sig = fs::read("fixtures/ed.sign-me.txt.nodejs-sig").expect("file should exist");
221        let pubkey = PublicKey::PEM(
222            String::from_utf8_lossy(&fs::read("fixtures/ed.public.pem").unwrap()).to_string(),
223        );
224        let msg = fs::read("fixtures/sign-me.txt").expect("file should exist");
225        verify(&msg, &sig, &pubkey.read().unwrap()).expect("should verify");
226    }
227
228    #[test]
229    fn test_nodejs_verified() {
230        let privkey = PrivateKey::PEM(
231            String::from_utf8_lossy(&fs::read("fixtures/ed.private.pem").unwrap()).to_string(),
232        )
233        .read()
234        .unwrap();
235        let msg = fs::read("fixtures/sign-me.txt").expect("file should exist");
236        let sig = sign(&msg, &privkey).expect("should sign");
237
238        let sigfile = "fixtures/ed.sign-me.txt.ring-sig";
239        if Path::new(sigfile).exists() {
240            fs::remove_file(sigfile).expect("should remove");
241        }
242        fs::write(sigfile, sig).expect("should write sig file");
243
244        // uses a generic wrapped public key `ed.public-wrap.pem` (SubjectPublicKeyInfo)
245        let out = Command::new("node")
246            .args(["fixtures/verify.js"])
247            .output()
248            .expect("failed to execute process");
249        let ok = String::from_utf8_lossy(&out.stdout);
250
251        println!("ok: {ok}");
252        assert!(ok.contains("OK"));
253    }
254}