signatory/key/store/
fs.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
//! Filesystem-backed keystore

use crate::{Error, KeyHandle, KeyInfo, KeyName, KeyRing, LoadPkcs8, Result};
use pkcs8::der::pem::PemLabel;
use std::{
    fs,
    path::{Path, PathBuf},
};
use zeroize::Zeroizing;

#[cfg(unix)]
use std::{fs::Permissions, os::unix::fs::PermissionsExt};

/// Required filesystem mode for keystore directories (Unix-only)
#[cfg(unix)]
const REQUIRED_DIR_MODE: u32 = 0o700;

/// PEM encapsulation boundary for private keys.
const PRIVATE_KEY_BOUNDARY: &str = "-----BEGIN PRIVATE KEY-----";

/// PEM encapsulation boundary for encrypted private keys.
const ENCRYPTED_PRIVATE_KEY_BOUNDARY: &str = "-----BEGIN ENCRYPTED PRIVATE KEY-----";

/// Filesystem-backed keystore.
pub struct FsKeyStore {
    path: PathBuf,
}

impl FsKeyStore {
    /// Attempt to open a filesystem-backed keystore at the given path,
    /// creating it if it doesn't already exist.
    pub fn create_or_open(dir_path: &Path) -> Result<Self> {
        Self::open(dir_path).or_else(|_| Self::create(dir_path))
    }

    /// Create a filesystem-backed keystore at the given path, making a new
    /// directory and setting its file permissions.
    pub fn create(dir_path: &Path) -> Result<Self> {
        fs::create_dir_all(dir_path)?;

        #[cfg(unix)]
        fs::set_permissions(dir_path, Permissions::from_mode(REQUIRED_DIR_MODE))?;

        Self::open(dir_path)
    }

    /// Initialize filesystem-backed keystore, opening the directory at the
    /// provided path and checking that it has the correct filesystem
    /// permissions.
    pub fn open(dir_path: &Path) -> Result<Self> {
        let path = dir_path.canonicalize()?;
        let st = path.metadata()?;

        if !st.is_dir() {
            return Err(Error::NotADirectory);
        }

        #[cfg(unix)]
        if st.permissions().mode() & 0o777 != REQUIRED_DIR_MODE {
            return Err(Error::Permissions);
        }

        Ok(Self { path })
    }

    /// Get information about a key with the given name.
    pub fn info(&self, name: &KeyName) -> Result<KeyInfo> {
        let pem_data = Zeroizing::new(fs::read_to_string(self.key_path(name))?);

        let encrypted = if pem_data.starts_with(ENCRYPTED_PRIVATE_KEY_BOUNDARY) {
            true
        } else if pem_data.starts_with(PRIVATE_KEY_BOUNDARY) {
            false
        } else {
            return Err(pkcs8::Error::KeyMalformed.into());
        };

        let algorithm = if encrypted {
            None
        } else {
            let (label, der) = pkcs8::SecretDocument::from_pem(&pem_data)?;
            pkcs8::PrivateKeyInfo::validate_pem_label(label)?;
            der.decode_msg::<pkcs8::PrivateKeyInfo<'_>>()?
                .algorithm
                .try_into()
                .ok()
        };

        Ok(KeyInfo {
            name: name.clone(),
            algorithm,
            encrypted,
        })
    }

    /// Import a key with a given name into the provided keyring.
    pub fn import(&self, name: &KeyName, key_ring: &mut KeyRing) -> Result<KeyHandle> {
        key_ring.load_pkcs8(self.load(name)?.decode_msg()?)
    }

    /// Load a PKCS#8 key from the keystore.
    pub fn load(&self, name: &KeyName) -> Result<pkcs8::SecretDocument> {
        let (label, doc) = pkcs8::SecretDocument::read_pem_file(self.key_path(name))?;
        pkcs8::PrivateKeyInfo::validate_pem_label(&label)?;
        Ok(doc)
    }

    /// Import a PKCS#8 key into the keystore.
    pub fn store(&self, name: &KeyName, der: &pkcs8::SecretDocument) -> Result<()> {
        der.write_pem_file(
            self.key_path(name),
            pkcs8::PrivateKeyInfo::PEM_LABEL,
            Default::default(),
        )?;
        Ok(())
    }

    /// Delete a PKCS#8 key from the keystore.
    pub fn delete(&self, name: &KeyName) -> Result<()> {
        fs::remove_file(self.key_path(name))?;

        Ok(())
    }

    /// Compute the path for a key with a given name.
    fn key_path(&self, name: &KeyName) -> PathBuf {
        let mut path = self.path.join(name);
        path.set_extension("pem");
        path
    }
}

#[cfg(test)]
#[allow(unused_imports)] // TODO(tarcieri): always use imports
mod tests {
    use super::FsKeyStore;
    use crate::{Algorithm, GeneratePkcs8};

    #[cfg(feature = "secp256k1")]
    use crate::ecdsa::secp256k1;

    pub const EXAMPLE_KEY: &str = "example-key";

    pub struct FsStoreHandle {
        pub keystore: FsKeyStore,
        pub dir: tempfile::TempDir,
    }

    /// Create a keystore containing one key named `example_key` with the given content
    #[allow(dead_code)]
    fn create_example_keystore(example_key: &pkcs8::SecretDocument) -> FsStoreHandle {
        let dir = tempfile::tempdir().unwrap();
        let keystore = FsKeyStore::create_or_open(&dir.path().join("keys")).unwrap();

        keystore
            .store(&EXAMPLE_KEY.parse().unwrap(), example_key)
            .unwrap();

        FsStoreHandle { keystore, dir }
    }

    #[cfg(feature = "secp256k1")]
    #[test]
    fn import_and_delete_key() {
        let key_name = EXAMPLE_KEY.parse().unwrap();
        let example_key = secp256k1::SigningKey::generate_pkcs8();
        let ks = create_example_keystore(&example_key);

        let example_key2 = ks.keystore.load(&key_name).unwrap();
        assert_eq!(example_key.as_bytes(), example_key2.as_bytes());

        ks.keystore.delete(&key_name).unwrap();
    }

    #[cfg(feature = "secp256k1")]
    #[test]
    fn get_key_info() {
        let key_name = EXAMPLE_KEY.parse().unwrap();
        let example_key = secp256k1::SigningKey::generate_pkcs8();
        let ks = create_example_keystore(&example_key);

        let key_info = ks.keystore.info(&key_name).unwrap();
        assert_eq!(key_info.name, key_name);
        assert_eq!(key_info.algorithm, Some(Algorithm::EcdsaSecp256k1));
        assert!(!key_info.encrypted);
    }
}