summaryrefslogtreecommitdiff
path: root/src/kms/mod.rs
blob: 37793e8c64f3ce4afecddd2272978dbc9ab68ff6 (plain)
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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
// Copyright 2017-2019 int08h LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//!
//! Protect the server's long-term key with envelope encryption and a key management system.
//!
//! Note: KMS support must be enabled at compile time, see the Roughenough's [documentation
//! on optional features](https://github.com/int08h/roughenough/blob/master/doc/OPTIONAL-FEATURES.md#key-management-system-kms-support)
//! for instructions.
//!
//! ## Motivation
//!
//! The seed for the server's [long-term key](../key/struct.LongTermKey.html) is subject to
//! contradictory requirements:
//!
//!   1. The seed must be kept secret, but
//!   2. The seed must be available at server start-up to create the
//!      [delegated on-line key](../key/struct.OnlineKey.html)
//!
//! ## Plaintext seed
//!
//! The default option is to store the seed in plaintext as part of the server's configuration.
//! This usually means the seed is present in the clear: on disk, in a repository, or otherwise
//! durably persisted where it can be compromised (accidentally or maliciously).
//!
//! ## Encrypting the seed
//!
//! Envelope encryption protects the seed by encrypting it with a locally generated 256-bit
//! Data Encryption Key (DEK). The DEK itself is then encrypted using a cloud key management
//! system (KMS). The resulting opaque encrypted "blob" (encrypted seed + encrypted DEK) is
//! stored in the Roughenough configuration.
//!
//! At server start-up the KMS is used to decrypt the DEK, which is then used to (in memory)
//! decrypt the seed. The seed is used to generate the
//! [delegated on-line key](../key/struct.OnlineKey.html) after which the seed and DEK are erased
//! from memory.
//!
//! See
//!   * [`EnvelopeEncryption`](struct.EnvelopeEncryption.html) for Roughenough's implementation.
//!   * [Google](https://cloud.google.com/kms/docs/envelope-encryption) or
//!     [Amazon](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#enveloping)
//!     for more in-depth explanations of envelope encryption.
//!

mod envelope;

use base64;
use ring;
use std;

use crate::config::ServerConfig;
use crate::error;
use crate::key::KmsProtection;

pub use self::envelope::EnvelopeEncryption;

/// Errors generated by KMS operations
#[derive(Debug, PartialEq, Eq, PartialOrd, Hash, Clone)]
pub enum KmsError {
    OperationFailed(String),
    InvalidConfiguration(String),
    InvalidData(String),
    InvalidKey(String),
}

impl From<std::io::Error> for KmsError {
    fn from(error: std::io::Error) -> Self {
        KmsError::OperationFailed(format!("{:?}", error))
    }
}

impl From<ring::error::Unspecified> for KmsError {
    fn from(_: ring::error::Unspecified) -> Self {
        KmsError::OperationFailed("unspecified ring cryptographic failure".to_string())
    }
}

impl From<base64::DecodeError> for KmsError {
    fn from(error: base64::DecodeError) -> Self {
        KmsError::OperationFailed(format!("base64: {}", error))
    }
}

// Size of the AEAD nonce in bytes.
const NONCE_SIZE_BYTES: usize = 12;

// Size of the AEAD authentication tag in bytes.
const TAG_SIZE_BYTES: usize = 16;

// Size of the 256-bit Data Encryption Key (DEK) in bytes.
const DEK_SIZE_BYTES: usize = 32;

// Trivial domain separation to guard against KMS key reuse
const AD: &str = "roughenough";

/// An unencrypted (plaintext) 256-bit Data Encryption Key (DEK).
pub type PlaintextDEK = Vec<u8>;

/// A Data Encryption Key (DEK) that has been encrypted (wrapped) by a Key Management System (KMS).
///
/// This is an opaque, implementation-specific value. AEAD tag size, nonce size,
/// provider metadata, and so on will vary between [`KmsProvider`](trait.KmsProvider.html)
/// implementations.
pub type EncryptedDEK = Vec<u8>;

///
/// A key management system that wraps/unwraps a data encryption key (DEK).
///
pub trait KmsProvider {
    /// Make a blocking request to encrypt (wrap) the provided plaintext data encryption key.
    fn encrypt_dek(&self, plaintext_dek: &PlaintextDEK) -> Result<EncryptedDEK, KmsError>;

    /// Make a blocking request to decrypt (unwrap) a previously encrypted data encryption key.
    fn decrypt_dek(&self, encrypted_dek: &EncryptedDEK) -> Result<PlaintextDEK, KmsError>;
}

#[cfg(feature = "awskms")]
mod awskms;

#[cfg(feature = "awskms")]
pub use crate::kms::awskms::inner::AwsKms;

/// Load the seed value for the long-term key.
///
/// Loading behavior depends on the value of `config.kms_protection()`:
///
///  * If `config.kms_protection() == Plaintext` then the value returned from `config.seed()`
///    is used as-is and assumed to be a 32-byte hexadecimal value.
///
///  * Otherwise `config.seed()` is assumed to be an encrypted opaque blob generated from
///    a prior `EnvelopeEncryption::encrypt_seed` call. The value of `config.kms_protection()`
///    is parsed as a KMS key id and `EnvelopeEncryption::decrypt_seed` is called to obtain
///    the plaintext seed value.
///
#[cfg(feature = "awskms")]
pub fn load_seed(config: &dyn ServerConfig) -> Result<Vec<u8>, error::Error> {
    match config.kms_protection() {
        KmsProtection::Plaintext => Ok(config.seed()),
        KmsProtection::AwsKmsEnvelope(key_id) => {
            info!("Unwrapping seed via AWS KMS key '{}'", key_id);
            let kms = AwsKms::from_arn(key_id)?;
            let seed = EnvelopeEncryption::decrypt_seed(&kms, &config.seed())?;
            Ok(seed)
        }
        _ => Err(error::Error::InvalidConfiguration(
            "Google KMS not supported".to_string(),
        )),
    }
}

#[cfg(feature = "gcpkms")]
mod gcpkms;

#[cfg(feature = "gcpkms")]
pub use crate::kms::gcpkms::inner::GcpKms;

/// Load the seed value for the long-term key.
///
/// Loading behavior depends on the value of `config.kms_protection()`:
///
///  * If `config.kms_protection() == Plaintext` then the value returned from `config.seed()`
///    is used as-is and assumed to be a 32-byte hexadecimal value.
///
///  * Otherwise `config.seed()` is assumed to be an encrypted opaque blob generated from
///    a prior `EnvelopeEncryption::encrypt_seed` call. The value of `config.kms_protection()`
///    is parsed as a KMS key id and `EnvelopeEncryption::decrypt_seed` is called to obtain
///    the plaintext seed value.
///
#[cfg(feature = "gcpkms")]
pub fn load_seed(config: &dyn ServerConfig) -> Result<Vec<u8>, error::Error> {
    match config.kms_protection() {
        KmsProtection::Plaintext => Ok(config.seed()),
        KmsProtection::GoogleKmsEnvelope(resource_id) => {
            info!("Unwrapping seed via Google KMS key '{}'", resource_id);
            let kms = GcpKms::from_resource_id(resource_id)?;
            let seed = EnvelopeEncryption::decrypt_seed(&kms, &config.seed())?;
            Ok(seed)
        }
        _ => Err(error::Error::InvalidConfiguration(
            "AWS KMS not supported".to_string(),
        )),
    }
}

/// Load the seed value for the long-term key.
///
/// ## This build has KMS disabled
///
/// *The KMS feature is disabled in this build of Roughenough*.
///
/// The only supported `kms_protection` value in this build is `plaintext`. Any
/// other value will cause a runtime error.
///
///  * `config.seed()` is used as-is and assumed to be a 32-byte hexadecimal value
///
#[cfg(not(any(feature = "awskms", feature = "gcpkms")))]
pub fn load_seed(config: &dyn ServerConfig) -> Result<Vec<u8>, error::Error> {
    match config.kms_protection() {
        KmsProtection::Plaintext => Ok(config.seed()),
        v => Err(error::Error::InvalidConfiguration(format!(
            "kms_protection '{}' requires KMS, but server was not compiled with KMS support",
            v
        ))),
    }
}