Merge branch 'key-mgmt-api-updates-v2' into 'main'

dev docs: key-management.md updates and clarifications

See merge request tpo/core/arti!1185
This commit is contained in:
gabi-250 2023-05-16 15:53:57 +00:00
commit 3c34c84f3f
1 changed files with 208 additions and 81 deletions

View File

@ -17,19 +17,19 @@ See also: [#728]
## Usage example
```rust
let client_id = HsClientIdentity::from_str("alice")?;
let client_spec = HsClientSpecifier::from_str("alice")?;
let intro_auth_key_id: HsClientSecretKeyIdentity =
(client_id, hs_id, HsClientKeyRole::IntroAuth).into();
let intro_auth_key_spec: HsClientSecretKeySpecifier =
(client_spec, hs_id, HsClientKeyRole::IntroAuth).into();
// Get KP_hsc_intro_auth
let sk: Option<ed25519::SecretKey> = keymgr.get::<ed25519::SecretKey>(&intro_auth_key_id)?;
let sk: Option<ed25519::SecretKey> = keymgr.get::<ed25519::SecretKey>(&intro_auth_key_spec)?;
// Alternatively, instead of returning a type-erased value, KeyStore::get could return a `Key`
// (TODO hs: come up with a better name), `Key` being an enum over all supported key types.
// `Key` could then have a `Key::as` convenience function that returns the underlying key (or
// an error if none of the variants have the requested type):
// let sk: Option<ed25519::SecretKey> = keymgr.get(&key_id)?.as::<ed25519::SecretKey>().unwrap();
// let sk: Option<ed25519::SecretKey> = keymgr.get(&key_spec)?.as::<ed25519::SecretKey>().unwrap();
```
## Key stores
@ -45,12 +45,6 @@ Supported key stores:
In the future we plan to also support HSM-based key stores.
## Key passphrases
OpenSSH keys can have passphrases. While the first version of the key manager
won't be able to handle such keys, we will add passphrase support at some point
in the future.
## Proposed configuration changes
We introduce a new `[keys]` section for configuring the key stores. The
@ -83,6 +77,8 @@ configuring a key store (for example, when running Arti as a client).
TODO hs: pick reasonable default values for the various top-level key store
directories (`root_dir`, `client_dir`, `key_dir`).
### Arti key store configuration
```toml
# Key store options
[keys]
@ -116,7 +112,30 @@ directories (`root_dir`, `client_dir`, `key_dir`).
# The root of the key store. All keys are stored somewhere in the `root_dir`
# heirarchy
root_dir = ""
```
### C Tor key store configuration
The client and relay keys are stored in a different part of the config than the
onion service keys: the client/relay key directories are read from the
`[keys.ctor_store]` section, whereas the onion service ones are read from the
`[onion_service.hs_service_dirs]` of each `[[onion_service]]` section (note
there can be multiple `[[onion_service]]` sections, one for each hidden service
configured). As a result, the C Tor key store is not rooted at a specific
directory (unlike the Arti key store). Instead, it is configured with:
* (for each onion service configured) a `hs_service_dir`, for onion service keys
* a `client_dir`, for onion service client authorization keys.
* a `key_dir`, for relay and directory authority keys
The exact structure of the `[[onion_service]]` config is not yet
specified, see [#699].
A downside of this approach is that there is no `CTorKeyStoreConfig` to speak
of: the `CTorKeyStore` is created from various bits of information taken from
different parts of the Arti config (`CTorKeyStore::new(client_dir, key_dir,
hs_service_dirs)`).
```toml
# The C Tor key store.
[keys.ctor_store]
# The client authorization key directory (if running Arti as a client).
@ -127,13 +146,35 @@ client_dir = ""
#
# This corresponds to C Tor's KeyDirectory option.
key_dir = ""
```
## Key identities
# Hidden service options
[[onion_service]]
# This corresponds to C Tor's HiddenServiceDir option.
hs_service_dir = "/home/bob/hs1"
# The maximum number of syteams per rendezvous circuit.
#
# This corresponds to C Tor's HiddenServiceMaxStreams.
max_streams = 0
# TODO arti#699: figure out what the rest of the options are
...
We introduce the concept of a "key identity" (specified for each supported key
type via the `KeyIdentity` trait). A "key identity" uniquely identifies an
instance of a type of key. From an implementation standpoint, `KeyIdentity`
# Hidden service options
[[onion_service]]
# This corresponds to C Tor's HiddenServiceDir option.
hs_service_dir = "/home/bob/hs2"
# The maximum number of syteams per rendezvous circuit.
#
# This corresponds to C Tor's HiddenServiceMaxStreams.
max_streams = 9000
# TODO arti#699: figure out what the rest of the options are
...
```
## Key specifiers
We introduce the concept of a "key specifier" (specified for each supported key
type via the `KeySpecifier` trait). A "key specifier" uniquely identifies an
instance of a type of key. From an implementation standpoint, `KeySpecifier`
implementers must specify:
* `arti_path`: the location of the key in the Arti key store. This also serves
as a unique identifier for a particular instance of a key.
@ -145,7 +186,7 @@ is the `arti_path` of a particular key):
```
<keys.arti_store.root_dir>
├── client
│ ├── alice # HS client identity "alice"
│ ├── alice # HS client specifier "alice"
│ │   ├── foo.onion
│ │ │   ├── hsc_desc_enc.arti_priv # arti_path = "client/alice/foo.onion/hsc_desc_enc"
│ │ │ │ # (HS client Alice's x25519 hsc_desc_enc keypair for decrypting the HS
@ -161,7 +202,7 @@ is the `arti_path` of a particular key):
│ │    └── hsc_intro_auth.arti_priv # arti_path = "client/alice/bar.onion/hsc_intro_auth"
│ │ # (HS client Alice's ed25519 hsc_intro_auth keypair for computing
│ │ # signatures to prove to bar.onion she is authorized")
│ └── bob # HS client identity "bob"
│ └── bob # HS client specifier "bob"
   └── foo.onion
   ├── hsc_desc_enc.arti_priv # arti_path = "client/bob/foo.onion/hsc_desc_enc"
│ │ # (HS client Bob's x25519 hsc_desc_enc keypair for decrypting the HS
@ -189,14 +230,16 @@ is the `arti_path` of a particular key):
...
```
### The `KeyIdentity` trait
### The `KeySpecifier` trait
```rust
/// The path of a key in the Arti key store.
/// The path of a key in the Arti key store,
/// relative to the root of the store.
/// This path does not contain double-dot (..) elements.
///
/// NOTE: There is a 1:1 mapping between a value that implements
/// `KeyIdentity` and its corresponding `ArtiPath`.
/// A `KeyIdentity` can be converted to an `ArtiPath`,
/// `KeySpecifier` and its corresponding `ArtiPath`.
/// A `KeySpecifier` can be converted to an `ArtiPath`,
/// but the reverse conversion is not supported.
//
// TODO hs: restrict the character set and syntax for values of this type
@ -206,26 +249,55 @@ is the `arti_path` of a particular key):
pub struct ArtiPath(PathBuf);
/// The path of a key in the C Tor key store.
///
/// To construct the path of the key on disk, the `CTorPath` is appended to the
/// `hs_service_dir`/`client_dir`/`key_dir` (depending on the role of the
/// requested key) followed by the extension.
///
/// This path does not contain double-dot (..) elements.
pub struct CTorPath(PathBuf);
/// The "identity" of a key.
/// The "specifier" of a key.
///
/// `KeyIdentity::arti_path()` uniquely identifies an instance of a key.
pub trait KeyIdentity {
/// `KeySpecifier::arti_path()` uniquely identifies an instance of a key.
pub trait KeySpecifier {
/// The location of the key in the Arti key store.
///
/// This also acts as a unique identifier for a specific key instance.
fn arti_path(&self) -> ArtiPath;
/// The file extension for a key of this type in an Arti key store.
///
/// The Arti key store will ignore any files that don't have a recognized extension.
fn arti_extension(&self) -> &'static str;
/// The location of the key in the C Tor key store (if supported).
fn ctor_path(&self) -> Option<CTorPath>;
/// The file extension for a key of this type in a C Tor key store.
///
/// The C Tor key store will ignore any files that don't have a recognized extension.
fn ctor_extension(&self) -> &'static str;
/// The type of user (client, service, relay, etc.) that uses this key.
fn user_kind(&self) -> UserKind;
}
/// The type of user (client, service, relay, etc.) that uses a key.
#[non_exhaustive]
pub enum UserKind {
Client,
Service(HsId),
Relay,
DirAuth,
...
}
/// An identifier for an HS client.
#[derive(AsRef, Into, ...)]
struct HsClientIdentity(String);
struct HsClientSpecifier(String);
impl FromStr for HsClientIdentity { /* check syntax rules */ }
impl FromStr for HsClientSpecifier { /* check syntax rules */ }
/// The role of a HS client key.
enum HsClientKeyRole {
@ -235,27 +307,41 @@ enum HsClientKeyRole {
IntroAuth,
}
struct HsClientSecretKeyIdentity {
struct HsClientSecretKeySpecifier {
/// The client associated with this key.
client_id: HsClientIdentity,
client_spec: HsClientSpecifier,
/// The hidden service this authorization key is for.
hs_id: HsId,
/// The role of the key.
role: HsClientKeyRole,
}
impl KeyIdentity for HsClientSecretKeyIdentity {
impl KeySpecifier for HsClientSecretKeySpecifier {
fn arti_path(&self) -> ArtiPath {
ArtiPath(
Path::new("client")
.join(self.client_id.to_string())
.join(self.client_spec.to_string())
.join(self.hs_id.to_string())
.join(self.role.to_string()),
)
}
fn arti_extension(&self) -> &'static str {
// We use nonstandard extensions to prevent keys from being used in
// unexpected ways (e.g. if the user renames a key from
// KP_hsc_intro_auth.arti_priv to KP_hsc_intro_auth.arti_priv.old, the
// arti key store should disregard the backup file).
"arti_priv"
}
fn ctor_path(&self) -> Option<CTorPath> {
...
Some(CTorPath(
Path::new("client").join(self.client_spec.to_string()),
))
}
fn ctor_extension(&self) -> &'static str {
"auth_private"
}
}
@ -289,10 +375,10 @@ struct KeyMgr {
impl KeyMgr {
/// Read a key from the key store, attempting to deserialize it as `K`.
pub fn get<K: Any + EncodableKey>(&self, key_id: &dyn KeyIdentity) -> Result<Option<K>> {
// Check if the requested key identity exists in any of the key stores:
pub fn get<K: Any + EncodableKey>(&self, key_spec: &dyn KeySpecifier) -> Result<Option<K>> {
// Check if the requested key specifier exists in any of the key stores:
for store in &self.key_store {
let key = store.get(key_id, K::key_type())?;
let key = store.get(key_spec, K::key_type())?;
if key.is_some() {
// Found it! Now try to downcast it to the right type (the
@ -319,19 +405,19 @@ impl KeyMgr {
/// TODO hs: update the API to return a Result<Option<K>> here (i.e. the old key)
pub fn insert<K: EncodableKey>(
&self,
key_id: &dyn KeyIdentity,
key_spec: &dyn KeySpecifier,
key: K,
) -> Result<()> {
for store in &self.key_store {
if store.has_key_bundle(key_id) {
return store.insert(&key, key_id, K::key_type());
if store.has_key_bundle(key_spec) {
return store.insert(&key, key_spec, K::key_type());
}
}
// None of the stores has the key bundle of key_id, so we insert the key into the first key
// None of the stores has the key bundle of key_spec, so we insert the key into the first key
// store.
if let Some(store) = self.key_store.first() {
return store.insert(&key, key_id, K::key_type());
return store.insert(&key, key_spec, K::key_type());
}
// Bug: no key stores were configured
@ -342,12 +428,9 @@ impl KeyMgr {
///
/// If the key exists in multiple key stores, this will only remove it from the first one. An
/// error is returned if none of the key stores contain the specified key.
pub fn remove<K: EncodableKey>(
&self,
key_id: &dyn KeyIdentity,
) -> Result<()> {
pub fn remove(&self, key_spec: &dyn KeySpecifier) -> Result<()> {
for store in &self.key_store {
match store.remove(key_id, K::key_type()) {
match store.remove(key_spec) {
Ok(()) => return Ok(()),
Err(e) if e is NotFound => continue,
Err(e) => return Err(e),
@ -367,24 +450,28 @@ stores all implement the `KeyStore` trait:
```rust
/// A generic key store.
pub trait KeyStore {
/// Retrieve the key identified by `key_id`.
fn get(&self, key_id: &dyn KeyIdentity, key_type: KeyType) -> Result<Option<ErasedKey>>;
/// Retrieve the key identified by `key_spec`.
fn get(&self, key_spec: &dyn KeySpecifier, key_type: KeyType) -> Result<Option<ErasedKey>>;
/// Write `key` to the key store.
fn insert(&self, key: &dyn EncodableKey, key_id: &dyn KeyIdentity, key_type: KeyType) -> Result<()>;
fn insert(&self, key: &dyn EncodableKey, key_spec: &dyn KeySpecifier, key_type: KeyType) -> Result<()>;
/// Remove the specified key.
fn remove(&self, key_id: &dyn KeyIdentity, key_type: KeyType) -> Result<()>;
fn remove(&self, key_spec: &dyn KeySpecifier) -> Result<()>;
}
```
We will initially support 2 key store implementations (one for the C Tor key
store, and another for the Arti store):
store, and one for the Arti store).
### The Arti key store
```rust
impl KeyStore for ArtiNativeKeyStore {
fn get(&self, key_id: &dyn KeyIdentity, key_type: KeyType) -> Result<Option<ErasedKey>> {
let key_path = self.key_path(key_id, key_type);
fn get(&self, key_spec: &dyn KeySpecifier, key_type: KeyType) -> Result<Option<ErasedKey>> {
let key_path = self.key_path(key_spec);
let input = match fs::read(key_path) {
Ok(input) => input,
@ -395,8 +482,8 @@ impl KeyStore for ArtiNativeKeyStore {
key_type.read_ssh_format_erased(&input).map(Some)
}
fn insert(&self, key: &dyn EncodableKey, key_id: &dyn KeyIdentity, key_type: KeyType) -> Result<()> {
let key_path = self.key_path(key_id, key_type);
fn insert(&self, key: &dyn EncodableKey, key_spec: &dyn KeySpecifier, key_type: KeyType) -> Result<()> {
let key_path = self.key_path(key_spec);
let ssh_format = key_type.write_ssh_format(key)?;
fs::write(key_path, ssh_format).map_err(|_| ())?;
@ -404,8 +491,8 @@ impl KeyStore for ArtiNativeKeyStore {
Ok(())
}
fn remove(&self, key_id: &dyn KeyIdentity, key_type: KeyType) -> Result<()> {
let key_path = self.key_path(key_id, key_type);
fn remove(&self, key_spec: &dyn KeySpecifier) -> Result<()> {
let key_path = self.key_path(key_spec);
fs::remove_file(key_path).map_err(|e| ...)?;
@ -414,16 +501,13 @@ impl KeyStore for ArtiNativeKeyStore {
}
impl ArtiNativeKeyStore {
/// The path on disk of the key with the specified identity and type.
fn key_path(&self, key_id: &dyn KeyIdentity, key_type: KeyType) -> PathBuf {
self.keystore_dir
.join(key_id.arti_path().0)
.join(key_type.extension())
}
}
/// The path on disk of the key with the specified specifier and type.
fn key_path(&self, key_spec: &dyn KeySpecifier) -> PathBuf {
let mut arti_path = self.keystore_dir.join(key_spec.arti_path().0);
arti_path.set_extension(key_spec.arti_extension());
impl KeyStore for CTorKeyStore {
...
arti_path
}
}
#[derive(Copy, Clone, ...)]
@ -437,22 +521,6 @@ pub enum KeyType {
}
impl KeyType {
/// The file extension for a key of this type.
///
/// We use nonstandard extensions to prevent keys from being used in unexpected ways (e.g. if
/// the user renames a key from KP_hsc_intro_auth.arti_priv to KP_hsc_intro_auth.arti_priv.old,
/// the arti key store should disregard the backup file).
///
/// The key stores will ignore any files that don't have a recognized extension.
pub fn extension(&self) -> &'static str {
// TODO hs: come up with a better convention for extensions.
if self.is_private() {
".arti_priv"
} else {
".arti_pub"
}
}
/// Whether the key is public or private.
fn is_private(&self) -> bool {
match self {
@ -551,6 +619,64 @@ impl SshKeyType for KeyType {
```
#### Versioning
As Arti evolves, it is likely we will eventually need to make changes to the
structure of its key store (for example, to support new key specifiers, or to
change something about the existing ones). This means we'll need to be able to
distinguish between the different supported key store versions. To achieve this,
the root of the Arti key store will have a `.VERSION` file that contains 2
version numbers (the format of the `.VERSION` file is TBD):
* `version`: the version of the key store
* `min_version`: the minimum `ArtiKeyStore` version required to
read/manipulate the store
The `ArtiKeyStore` won't be constructed if the `.VERSION` file of the configured
store is malformed, or if `ArtiKeyStore::VERSION` is less than its
`min_version`. This should likely be treated as a fatal error (i.e. Arti should
refuse to start if the keystore exists but is inaccessible or malformed).
#### Key passphrases
OpenSSH keys can have passphrases. While the first version of the key manager
won't be able to handle such keys, we will add passphrase support at some point
in the future.
### The C Tor key store
TODO
```rust
impl KeyStore for CTorKeyStore {
...
}
impl CTorKeyStore {
/// The path on disk of the key with the specified specifier and type.
fn key_path(&self, key_spec: &dyn KeySpecifier) -> Option<PathBuf> {
let ext = key_spec.ctor_extension();
let root_dir = match key_spec.user_kind() {
UserKind::Client => self.client_dir,
UserKind::Relay => self.key_dir,
UserKind::Service(hs_id) => {
// CTorKeyStore contains a mapping from hs_id to
// `HiddenServiceDir`. We work out which HiddenServiceDir to
// load keys from based on the hs_id of the KeySpecifier.
self.hs_service_dirs.get(hs_id)?
}
...
};
let mut ctor_path = root_dir.join(key_spec.ctor_path().0);
ctor_path.set_extension(ext);
Some(ctor_path)
}
}
```
## Concurrent access for disk-based key stores
The key stores will allow concurrent modification by different processes. In
@ -566,3 +692,4 @@ exists). As such, on Windows we will need some sort of synchronization mechanism
(unless it exposes some other APIs we can use for atomic renaming).
[#728]: https://gitlab.torproject.org/tpo/core/arti/-/issues/728
[#699]: https://gitlab.torproject.org/tpo/core/arti/-/issues/699