hsmtool/extra/
spxef.rs

1// Copyright lowRISC contributors (OpenTitan project).
2// Licensed under the Apache License, Version 2.0, see LICENSE for details.
3// SPDX-License-Identifier: Apache-2.0
4
5use acorn::{GenerateFlags, KeyEntry, KeyInfo, SpxInterface};
6use anyhow::Result;
7use cryptoki::session::Session;
8use sphincsplus::{DecodeKey, EncodeKey};
9use sphincsplus::{SphincsPlus, SpxDomain, SpxError, SpxPublicKey, SpxSecretKey};
10use std::rc::Rc;
11use std::str::FromStr;
12use zeroize::Zeroizing;
13
14use crate::error::HsmError;
15use crate::util::attribute::{AttrData, AttributeMap, AttributeType};
16use crate::util::ef::ElementaryFile;
17
18/// SpxEf implements host-based SPHINCS+ signing with elementary files stored
19/// on a PKCS#11 token.
20///
21/// This is not as secure as signing on an HSM, but allows secure storage of
22/// the key material on a token.  Every effort is made to destroy secret key
23/// material loaded to host RAM after use to prevent unintentional leaking of
24/// keys.
25pub struct SpxEf {
26    session: Rc<Session>,
27}
28
29impl SpxEf {
30    const APPLICATION: &'static str = "hsmtool-spx";
31
32    pub fn new(session: Rc<Session>) -> Box<Self> {
33        Box::new(Self { session })
34    }
35
36    fn load_key(&self, alias: &str) -> Result<SpxSecretKey> {
37        let mut search = AttributeMap::default();
38        search.insert(AttributeType::Label, AttrData::Str(alias.into()));
39        let mut ef = ElementaryFile::find(&self.session, search)?;
40        if ef.is_empty() {
41            return Err(HsmError::ObjectNotFound(alias.into()).into());
42        } else if ef.len() > 1 {
43            return Err(HsmError::TooManyObjects(ef.len(), alias.into()).into());
44        }
45        let ef = ef.remove(0);
46        if let Some(app) = &ef.application {
47            match app.split_once(':') {
48                Some((Self::APPLICATION, _algo)) => {
49                    let data = Zeroizing::new(String::from_utf8(ef.read(&self.session)?)?);
50                    return Ok(SpxSecretKey::from_pem(data.as_str())?);
51                }
52                Some((_, _)) | None => {
53                    return Err(HsmError::UnknownApplication(app.into()).into());
54                }
55            }
56        }
57        Err(HsmError::UnknownApplication("<none>".into()).into())
58    }
59}
60
61impl SpxInterface for SpxEf {
62    /// Get the version of the backend.
63    fn get_version(&self) -> Result<String> {
64        Ok(String::from("PKCS#11 ElementaryFiles 0.0.1"))
65    }
66
67    /// List keys known to the backend.
68    fn list_keys(&self) -> Result<Vec<KeyEntry>> {
69        let mut result = Vec::new();
70        for file in ElementaryFile::list(&self.session)? {
71            if let Some(app) = file.application
72                && let Some((Self::APPLICATION, algo)) = app.split_once(':')
73            {
74                result.push(KeyEntry {
75                    alias: file.name.clone(),
76                    hash: None,
77                    algorithm: algo.into(),
78                    ..Default::default()
79                });
80            }
81        }
82        Ok(result)
83    }
84
85    /// Get the public key info.
86    fn get_key_info(&self, alias: &str) -> Result<KeyInfo> {
87        let sk = self.load_key(alias)?;
88        let pk = SpxPublicKey::from(&sk);
89
90        Ok(KeyInfo {
91            hash: "".into(),
92            algorithm: pk.algorithm().to_string(),
93            domain: None,
94            public_key: pk.as_bytes().to_vec(),
95            private_blob: Vec::new(),
96        })
97    }
98
99    /// Generate a key pair.
100    fn generate_key(
101        &self,
102        alias: &str,
103        algorithm: &str,
104        _domain: SpxDomain,
105        _token: &str,
106        flags: GenerateFlags,
107    ) -> Result<KeyEntry> {
108        let mut search = AttributeMap::default();
109        search.insert(AttributeType::Label, AttrData::Str(alias.into()));
110        let ef = ElementaryFile::find(&self.session, search)?;
111        if flags.contains(GenerateFlags::OVERWRITE) {
112            if ef.len() <= 1 {
113                // delete files
114            } else {
115                return Err(HsmError::TooManyObjects(ef.len(), alias.into()).into());
116            }
117        } else if !ef.is_empty() {
118            return Err(HsmError::ObjectExists("<none>".into(), alias.into()).into());
119        }
120
121        let (sk, _) = SpxSecretKey::new_keypair(SphincsPlus::from_str(algorithm)?)?;
122        let app = format!("{}:{}", Self::APPLICATION, sk.algorithm());
123        let skf = ElementaryFile::new(alias.into())
124            .application(app)
125            .private(true);
126        let encoded = Zeroizing::new(sk.to_pem()?);
127        skf.write(&self.session, encoded.as_bytes())?;
128
129        let private_key = if flags.contains(GenerateFlags::EXPORT_PRIVATE) {
130            sk.as_bytes().to_vec()
131        } else {
132            Vec::new()
133        };
134
135        Ok(KeyEntry {
136            alias: alias.into(),
137            hash: Some("".into()),
138            algorithm: sk.algorithm().to_string(),
139            domain: None,
140            private_blob: Vec::new(),
141            private_key,
142        })
143    }
144
145    /// Import a key pair.
146    fn import_keypair(
147        &self,
148        alias: &str,
149        algorithm: &str,
150        _domain: SpxDomain,
151        _token: &str,
152        overwrite: bool,
153        public_key: &[u8],
154        private_key: &[u8],
155    ) -> Result<KeyEntry> {
156        let mut search = AttributeMap::default();
157        search.insert(AttributeType::Label, AttrData::Str(alias.into()));
158        let ef = ElementaryFile::find(&self.session, search)?;
159        if overwrite {
160            if ef.len() <= 1 {
161                // delete files
162            } else {
163                return Err(HsmError::TooManyObjects(ef.len(), alias.into()).into());
164            }
165        } else if !ef.is_empty() {
166            return Err(HsmError::ObjectExists("<none>".into(), alias.into()).into());
167        }
168
169        let sk = SpxSecretKey::from_bytes(SphincsPlus::from_str(algorithm)?, private_key)?;
170        let pk = SpxPublicKey::from(&sk);
171        if public_key != pk.as_bytes() {
172            return Err(HsmError::KeyError("secret/public key mismatch".into()).into());
173        }
174        let app = format!("{}:{}", Self::APPLICATION, sk.algorithm());
175        let skf = ElementaryFile::new(alias.into())
176            .application(app)
177            .private(true);
178        let encoded = Zeroizing::new(sk.to_pem()?);
179        skf.write(&self.session, encoded.as_bytes())?;
180
181        Ok(KeyEntry {
182            alias: alias.into(),
183            hash: None,
184            algorithm: sk.algorithm().to_string(),
185            domain: None,
186            private_blob: Vec::new(),
187            private_key: Vec::new(),
188        })
189    }
190
191    /// Sign a message.
192    fn sign(
193        &self,
194        alias: Option<&str>,
195        key_hash: Option<&str>,
196        domain: SpxDomain,
197        message: &[u8],
198    ) -> Result<Vec<u8>> {
199        let alias = alias.ok_or(HsmError::NoSearchCriteria)?;
200        if key_hash.is_some() {
201            log::warn!("ignored key_hash {key_hash:?}");
202        }
203        let sk = self.load_key(alias)?;
204        Ok(sk.sign(domain, message)?)
205    }
206
207    /// Verify a message.
208    fn verify(
209        &self,
210        alias: Option<&str>,
211        key_hash: Option<&str>,
212        domain: SpxDomain,
213        message: &[u8],
214        signature: &[u8],
215    ) -> Result<bool> {
216        let alias = alias.ok_or(HsmError::NoSearchCriteria)?;
217        if key_hash.is_some() {
218            log::warn!("ignored key_hash {key_hash:?}");
219        }
220        let sk = self.load_key(alias)?;
221        let pk = SpxPublicKey::from(&sk);
222        match pk.verify(domain, signature, message) {
223            Ok(()) => Ok(true),
224            Err(SpxError::BadSignature) => Ok(false),
225            Err(e) => Err(e.into()),
226        }
227    }
228}