Skip to content

Commit e147b4d

Browse files
quextenMGibson1dani-garciaHinton
authored
[PM-20361] Signature keys (#207)
## 🎟️ Tracking https://bitwarden.atlassian.net/browse/PM-20361 ## 📔 Objective ### Signing operations This PR adds signing/verifying operations, and keys, including serialization to COSE for both the keys, and for the signatures. We enforce strong domain separation by adding a namespace. This ensures not having to worry about cross-protocol attacks / swapping messages between contexts. Two signing operations are provided; sign and sign detached. The former signs an object and returns a signedobject, containing the payload. To get the payload, the signature *must* be verified (guaranteed by the interface). The latter returns signature and serialized message. This is useful when multiple signatures are needed for one message; in this case a countersignature can be created, so that we can have multiple signatures for the same object, in case a trust relationship needs to be proven in multiple directions. Signing is implemented in 3 layers documented at the top of `sign.rs`. The high level layer accepts a struct that is serializable and namespace, and returns a signature and the serialized representation, or the signedobject. The serialization may not be canonical, so the consumer should store the serialized representation instead of their input struct. The reverse operation - verify / getverifiedpayload - returns the deserialized struct. The middle layer accepts byte array plus namespace. The low layer is the direct implementation of the signature, and accepts byte array (no namespace). This ensures that implementing a new signature scheme only requires implementing this low level operation. The one implemented algorithm type is ed25519. Keys are represented as enums such that it is easy to add other signing key types (ML-DSA for post-quantum crypto, PoC here: #216) later on. The signature is represented as a Cose Sign1 message (https://www.rfc-editor.org/rfc/rfc9052.html#name-signing-with-one-signer). The namespace is separated by setting the protected header. Since this is included in the signed data, and since this is validated on verifying the signature, and since the values are unique, domain separation is enforced. We only ever want to expose the safe function outside of the crate (if we even expose it out of the crate). ### SignedPublicKey Public encryption keys should have their owner identity verified. Thus, a new SignedPublicKey that is a signed object containing the public key is created. Consumers must verify the signature before being able to access the public encryption key. ### Public key changes Public keys are hardcoded at the moment to expect DER encoding and RSA-OAEP encryption. This is not the case long-term, and the signature format above makes this clear by including format types. This PR also includes some prep-work, applying the same per-encryption-time enum variant split in the asymmetric encryption keys. Follow-up work can further clean this up, and clean up the folder structure / file split. ## ⏰ Reminders before review - Contributor guidelines followed - All formatters and local linters executed and passed - Written new unit and / or integration tests where applicable - Protected functional changes with optionality (feature flags) - Used internationalization (i18n) for all UI strings - CI builds passed - Communicated to DevOps any deployment requirements - Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team ## 🦮 Reviewer guidelines <!-- Suggested interactions but feel free to use (or not) as you desire! --> - 👍 (`:+1:`) or similar for great changes - 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info - ❓ (`:question:`) for questions - 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion - 🎨 (`:art:`) for suggestions / improvements - ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention - 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt - ⛏ (`:pick:`) for minor or nitpick changes --------- Co-authored-by: Matt Gibson <[email protected]> Co-authored-by: Daniel García <[email protected]> Co-authored-by: Oscar Hinton <[email protected]>
1 parent b0335ef commit e147b4d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+2443
-190
lines changed

Cargo.lock

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bitwarden-core/src/auth/auth_request.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use base64::{engine::general_purpose::STANDARD, Engine};
22
use bitwarden_crypto::{
33
fingerprint, generate_random_alphanumeric, AsymmetricCryptoKey, AsymmetricPublicCryptoKey,
4-
CryptoError, UnsignedSharedKey,
4+
CryptoError, PublicKeyEncryptionAlgorithm, UnsignedSharedKey,
55
};
66
#[cfg(feature = "internal")]
77
use bitwarden_crypto::{EncString, SymmetricCryptoKey};
@@ -31,11 +31,9 @@ pub struct AuthRequestResponse {
3131
/// to another device. Where the user confirms the validity by confirming the fingerprint. The user
3232
/// key is then encrypted using the public key and returned to the initiating device.
3333
pub(crate) fn new_auth_request(email: &str) -> Result<AuthRequestResponse, CryptoError> {
34-
let mut rng = rand::thread_rng();
34+
let key = AsymmetricCryptoKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
3535

36-
let key = AsymmetricCryptoKey::generate(&mut rng);
37-
38-
let spki = key.to_public_der()?;
36+
let spki = key.to_public_key().to_der()?;
3937

4038
let fingerprint = fingerprint(email, &spki)?;
4139
let b64 = STANDARD.encode(&spki);
@@ -124,7 +122,7 @@ fn test_auth_request() {
124122

125123
let encrypted = UnsignedSharedKey::encapsulate_key_unsigned(
126124
&SymmetricCryptoKey::try_from(secret.clone()).unwrap(),
127-
&private_key,
125+
&private_key.to_public_key(),
128126
)
129127
.unwrap();
130128

@@ -162,7 +160,7 @@ mod tests {
162160
let private_key ="2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=".parse().unwrap();
163161
client
164162
.internal
165-
.initialize_user_crypto_master_key(master_key, user_key, private_key)
163+
.initialize_user_crypto_master_key(master_key, user_key, private_key, None)
166164
.unwrap();
167165

168166
let public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvyLRDUwXB4BfQ507D4meFPmwn5zwy3IqTPJO4plrrhnclWahXa240BzyFW9gHgYu+Jrgms5xBfRTBMcEsqqNm7+JpB6C1B6yvnik0DpJgWQw1rwvy4SUYidpR/AWbQi47n/hvnmzI/sQxGddVfvWu1iTKOlf5blbKYAXnUE5DZBGnrWfacNXwRRdtP06tFB0LwDgw+91CeLSJ9py6dm1qX5JIxoO8StJOQl65goLCdrTWlox+0Jh4xFUfCkb+s3px+OhSCzJbvG/hlrSRcUz5GnwlCEyF3v5lfUtV96MJD+78d8pmH6CfFAp2wxKRAbGdk+JccJYO6y6oIXd3Fm7twIDAQAB";
@@ -229,7 +227,7 @@ mod tests {
229227

230228
existing_device
231229
.internal
232-
.initialize_user_crypto_master_key(master_key, user_key, private_key.clone())
230+
.initialize_user_crypto_master_key(master_key, user_key, private_key.clone(), None)
233231
.unwrap();
234232

235233
// Initialize a new device which will request to be logged in
@@ -247,6 +245,7 @@ mod tests {
247245
kdf_params: kdf,
248246
email: email.to_owned(),
249247
private_key,
248+
signing_key: None,
250249
method: InitUserCryptoMethod::AuthRequest {
251250
request_private_key: auth_req.private_key,
252251
method: AuthRequestMethod::UserKey {

crates/bitwarden-core/src/auth/login/api_key.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,12 @@ pub(crate) async fn login_api_key(
5151
let user_key: EncString = require!(r.key.as_deref()).parse()?;
5252
let private_key: EncString = require!(r.private_key.as_deref()).parse()?;
5353

54-
client
55-
.internal
56-
.initialize_user_crypto_master_key(master_key, user_key, private_key)?;
54+
client.internal.initialize_user_crypto_master_key(
55+
master_key,
56+
user_key,
57+
private_key,
58+
None,
59+
)?;
5760
}
5861

5962
Ok(ApiKeyLoginResponse::process_response(response))

crates/bitwarden-core/src/auth/login/auth_request.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ pub(crate) async fn complete_auth_request(
120120
kdf_params: kdf,
121121
email: auth_req.email,
122122
private_key: require!(r.private_key).parse()?,
123+
signing_key: None,
123124
method: InitUserCryptoMethod::AuthRequest {
124125
request_private_key: auth_req.private_key,
125126
method,

crates/bitwarden-core/src/auth/login/password.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,12 @@ pub(crate) async fn login_password(
5050
let user_key: EncString = require!(r.key.as_deref()).parse()?;
5151
let private_key: EncString = require!(r.private_key.as_deref()).parse()?;
5252

53-
client
54-
.internal
55-
.initialize_user_crypto_master_key(master_key, user_key, private_key)?;
53+
client.internal.initialize_user_crypto_master_key(
54+
master_key,
55+
user_key,
56+
private_key,
57+
None,
58+
)?;
5659
}
5760

5861
Ok(PasswordLoginResponse::process_response(response))

crates/bitwarden-core/src/auth/password/validate.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,12 @@ mod tests {
140140

141141
client
142142
.internal
143-
.initialize_user_crypto_master_key(master_key, user_key.parse().unwrap(), private_key)
143+
.initialize_user_crypto_master_key(
144+
master_key,
145+
user_key.parse().unwrap(),
146+
private_key,
147+
None,
148+
)
144149
.unwrap();
145150

146151
let result =
@@ -183,7 +188,12 @@ mod tests {
183188

184189
client
185190
.internal
186-
.initialize_user_crypto_master_key(master_key, user_key.parse().unwrap(), private_key)
191+
.initialize_user_crypto_master_key(
192+
master_key,
193+
user_key.parse().unwrap(),
194+
private_key,
195+
None,
196+
)
187197
.unwrap();
188198

189199
let result =

crates/bitwarden-core/src/auth/pin.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,12 @@ mod tests {
7575

7676
client
7777
.internal
78-
.initialize_user_crypto_master_key(master_key, user_key.parse().unwrap(), private_key)
78+
.initialize_user_crypto_master_key(
79+
master_key,
80+
user_key.parse().unwrap(),
81+
private_key,
82+
None,
83+
)
7984
.unwrap();
8085

8186
client

crates/bitwarden-core/src/auth/tde.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,13 @@ pub(super) fn make_register_tde_keys(
3737
kdf: Kdf::default(),
3838
},
3939
));
40-
client
41-
.internal
42-
.initialize_user_crypto_decrypted_key(user_key.0, key_pair.private.clone())?;
40+
client.internal.initialize_user_crypto_decrypted_key(
41+
user_key.0,
42+
key_pair.private.clone(),
43+
// Note: Signing keys are not supported on registration yet. This needs to be changed as
44+
// soon as registration is supported.
45+
None,
46+
)?;
4347

4448
Ok(RegisterTdeKeyResponse {
4549
private_key: key_pair.private,

crates/bitwarden-core/src/client/encryption_settings.rs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
#[cfg(feature = "internal")]
2-
use bitwarden_crypto::{AsymmetricCryptoKey, EncString, UnsignedSharedKey};
2+
use bitwarden_crypto::{EncString, UnsignedSharedKey};
33
#[cfg(any(feature = "internal", feature = "secrets"))]
44
use bitwarden_crypto::{KeyStore, SymmetricCryptoKey};
55
use bitwarden_error::bitwarden_error;
66
use thiserror::Error;
77
#[cfg(any(feature = "internal", feature = "secrets"))]
88
use uuid::Uuid;
99

10-
#[cfg(feature = "internal")]
11-
use crate::key_management::AsymmetricKeyId;
1210
#[cfg(any(feature = "internal", feature = "secrets"))]
1311
use crate::key_management::{KeyIds, SymmetricKeyId};
1412
use crate::{error::UserIdAlreadySetError, MissingPrivateKeyError, VaultLockedError};
@@ -48,12 +46,15 @@ impl EncryptionSettings {
4846
pub(crate) fn new_decrypted_key(
4947
user_key: SymmetricCryptoKey,
5048
private_key: EncString,
49+
signing_key: Option<EncString>,
5150
store: &KeyStore<KeyIds>,
5251
) -> Result<(), EncryptionSettingsError> {
53-
use bitwarden_crypto::KeyDecryptable;
52+
use bitwarden_crypto::{
53+
AsymmetricCryptoKey, CoseSerializable, CryptoError, KeyDecryptable, SigningKey,
54+
};
5455
use log::warn;
5556

56-
use crate::key_management::{AsymmetricKeyId, SymmetricKeyId};
57+
use crate::key_management::{AsymmetricKeyId, SigningKeyId, SymmetricKeyId};
5758

5859
let private_key = {
5960
let dec: Vec<u8> = private_key.decrypt_with_key(&user_key)?;
@@ -71,6 +72,12 @@ impl EncryptionSettings {
7172
// .map_err(|_| EncryptionSettingsError::InvalidPrivateKey)?,
7273
// )
7374
};
75+
let signing_key = signing_key
76+
.map(|key| {
77+
let dec: Vec<u8> = key.decrypt_with_key(&user_key)?;
78+
SigningKey::from_cose(dec.as_slice()).map_err(Into::<CryptoError>::into)
79+
})
80+
.transpose()?;
7481

7582
// FIXME: [PM-18098] When this is part of crypto we won't need to use deprecated methods
7683
#[allow(deprecated)]
@@ -80,6 +87,10 @@ impl EncryptionSettings {
8087
if let Some(private_key) = private_key {
8188
ctx.set_asymmetric_key(AsymmetricKeyId::UserPrivateKey, private_key)?;
8289
}
90+
91+
if let Some(signing_key) = signing_key {
92+
ctx.set_signing_key(SigningKeyId::UserSigningKey, signing_key)?;
93+
}
8394
}
8495

8596
Ok(())
@@ -106,6 +117,8 @@ impl EncryptionSettings {
106117
org_enc_keys: Vec<(Uuid, UnsignedSharedKey)>,
107118
store: &KeyStore<KeyIds>,
108119
) -> Result<(), EncryptionSettingsError> {
120+
use crate::key_management::AsymmetricKeyId;
121+
109122
let mut ctx = store.context_mut();
110123

111124
// FIXME: [PM-11690] - Early abort to handle private key being corrupt

crates/bitwarden-core/src/client/internal.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@ use bitwarden_crypto::{EncString, Kdf, MasterKey, PinKey, UnsignedSharedKey};
88
use chrono::Utc;
99
use uuid::Uuid;
1010

11+
use super::encryption_settings::EncryptionSettings;
1112
#[cfg(feature = "secrets")]
1213
use super::login_method::ServiceAccountLoginMethod;
13-
#[cfg(any(feature = "internal", feature = "secrets"))]
14-
use crate::client::encryption_settings::EncryptionSettings;
1514
use crate::{
1615
auth::renew::renew_token, client::login_method::LoginMethod, error::UserIdAlreadySetError,
1716
key_management::KeyIds, DeviceType,
@@ -199,9 +198,10 @@ impl InternalClient {
199198
master_key: MasterKey,
200199
user_key: EncString,
201200
private_key: EncString,
201+
signing_key: Option<EncString>,
202202
) -> Result<(), EncryptionSettingsError> {
203203
let user_key = master_key.decrypt_user_key(user_key)?;
204-
EncryptionSettings::new_decrypted_key(user_key, private_key, &self.key_store)?;
204+
EncryptionSettings::new_decrypted_key(user_key, private_key, signing_key, &self.key_store)?;
205205

206206
Ok(())
207207
}
@@ -211,8 +211,9 @@ impl InternalClient {
211211
&self,
212212
user_key: SymmetricCryptoKey,
213213
private_key: EncString,
214+
signing_key: Option<EncString>,
214215
) -> Result<(), EncryptionSettingsError> {
215-
EncryptionSettings::new_decrypted_key(user_key, private_key, &self.key_store)?;
216+
EncryptionSettings::new_decrypted_key(user_key, private_key, signing_key, &self.key_store)?;
216217

217218
Ok(())
218219
}
@@ -223,9 +224,10 @@ impl InternalClient {
223224
pin_key: PinKey,
224225
pin_protected_user_key: EncString,
225226
private_key: EncString,
227+
signing_key: Option<EncString>,
226228
) -> Result<(), EncryptionSettingsError> {
227229
let decrypted_user_key = pin_key.decrypt_user_key(pin_protected_user_key)?;
228-
self.initialize_user_crypto_decrypted_key(decrypted_user_key, private_key)
230+
self.initialize_user_crypto_decrypted_key(decrypted_user_key, private_key, signing_key)
229231
}
230232

231233
#[cfg(feature = "secrets")]

0 commit comments

Comments
 (0)