hsmtool/spxef/
mod.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            public_key: pk.as_bytes().to_vec(),
94            private_blob: Vec::new(),
95        })
96    }
97
98    /// Generate a key pair.
99    fn generate_key(
100        &self,
101        alias: &str,
102        algorithm: &str,
103        _token: &str,
104        flags: GenerateFlags,
105    ) -> Result<KeyEntry> {
106        let mut search = AttributeMap::default();
107        search.insert(AttributeType::Label, AttrData::Str(alias.into()));
108        let ef = ElementaryFile::find(&self.session, search)?;
109        if flags.contains(GenerateFlags::OVERWRITE) {
110            if ef.len() <= 1 {
111                // delete files
112            } else {
113                return Err(HsmError::TooManyObjects(ef.len(), alias.into()).into());
114            }
115        } else if !ef.is_empty() {
116            return Err(HsmError::ObjectExists("<none>".into(), alias.into()).into());
117        }
118
119        let (sk, _) = SpxSecretKey::new_keypair(SphincsPlus::from_str(algorithm)?)?;
120        let app = format!("{}:{}", Self::APPLICATION, sk.algorithm());
121        let skf = ElementaryFile::new(alias.into())
122            .application(app)
123            .private(true);
124        let encoded = Zeroizing::new(sk.to_pem()?);
125        skf.write(&self.session, encoded.as_bytes())?;
126
127        let private_key = if flags.contains(GenerateFlags::EXPORT_PRIVATE) {
128            sk.as_bytes().to_vec()
129        } else {
130            Vec::new()
131        };
132
133        Ok(KeyEntry {
134            alias: alias.into(),
135            hash: Some("".into()),
136            algorithm: sk.algorithm().to_string(),
137            private_blob: Vec::new(),
138            private_key,
139        })
140    }
141
142    /// Import a key pair.
143    fn import_keypair(
144        &self,
145        alias: &str,
146        algorithm: &str,
147        _token: &str,
148        overwrite: bool,
149        public_key: &[u8],
150        private_key: &[u8],
151    ) -> Result<KeyEntry> {
152        let mut search = AttributeMap::default();
153        search.insert(AttributeType::Label, AttrData::Str(alias.into()));
154        let ef = ElementaryFile::find(&self.session, search)?;
155        if overwrite {
156            if ef.len() <= 1 {
157                // delete files
158            } else {
159                return Err(HsmError::TooManyObjects(ef.len(), alias.into()).into());
160            }
161        } else if !ef.is_empty() {
162            return Err(HsmError::ObjectExists("<none>".into(), alias.into()).into());
163        }
164
165        let sk = SpxSecretKey::from_bytes(SphincsPlus::from_str(algorithm)?, private_key)?;
166        let pk = SpxPublicKey::from(&sk);
167        if public_key != pk.as_bytes() {
168            return Err(HsmError::KeyError("secret/public key mismatch".into()).into());
169        }
170        let app = format!("{}:{}", Self::APPLICATION, sk.algorithm());
171        let skf = ElementaryFile::new(alias.into())
172            .application(app)
173            .private(true);
174        let encoded = Zeroizing::new(sk.to_pem()?);
175        skf.write(&self.session, encoded.as_bytes())?;
176
177        Ok(KeyEntry {
178            alias: alias.into(),
179            hash: None,
180            algorithm: sk.algorithm().to_string(),
181            private_blob: Vec::new(),
182            private_key: Vec::new(),
183        })
184    }
185
186    /// Sign a message.
187    fn sign(&self, alias: Option<&str>, key_hash: Option<&str>, message: &[u8]) -> Result<Vec<u8>> {
188        let alias = alias.ok_or(HsmError::NoSearchCriteria)?;
189        if key_hash.is_some() {
190            log::warn!("ignored key_hash {key_hash:?}");
191        }
192        let sk = self.load_key(alias)?;
193        Ok(sk.sign(SpxDomain::None, message)?)
194    }
195
196    /// Verify a message.
197    fn verify(
198        &self,
199        alias: Option<&str>,
200        key_hash: Option<&str>,
201        message: &[u8],
202        signature: &[u8],
203    ) -> Result<bool> {
204        let alias = alias.ok_or(HsmError::NoSearchCriteria)?;
205        if key_hash.is_some() {
206            log::warn!("ignored key_hash {key_hash:?}");
207        }
208        let sk = self.load_key(alias)?;
209        let pk = SpxPublicKey::from(&sk);
210        match pk.verify(SpxDomain::None, signature, message) {
211            Ok(()) => Ok(true),
212            Err(SpxError::BadSignature) => Ok(false),
213            Err(e) => Err(e.into()),
214        }
215    }
216}