From 43bda11ed8d825c8b1b12e1b79b52b853d2b1560 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Fri, 20 Oct 2023 09:52:29 +0300 Subject: [PATCH] feat(certificates): assign unique IDs (UUIDv7) to private keys --- .cargo/config.toml | 3 + ...44ec2ca01252c862839e1270da51511a3fdb6.json | 12 - ...0e2593eff25be4d6e0aeb889c184edf99028.json} | 4 +- ...4b221ffb61c5e7ddeaf80e066a58e07215611.json | 12 + ...372986ea22847dcaacf2e9f5fb98e6960a2ba.json | 12 + ...9efaf4bd6bea5229a2fd4cb9557540b5282c.json} | 20 +- ...cc96d1e25e23b44b5c32b8e324db34de43ba9.json | 12 - ...7f40b3a23a9f33ee0792bc388fc0300e70c1.json} | 20 +- Cargo.lock | 11 +- Cargo.toml | 3 +- ...231019195044_certificates_private_keys.sql | 3 +- src/utils/api_ext.rs | 4 +- src/utils/certificates/api_ext.rs | 239 ++++++++++++++---- src/utils/certificates/database_ext.rs | 221 +++++++++++++--- .../database_ext/raw_private_key.rs | 21 ++ .../certificates/private_keys/private_key.rs | 8 + .../certificates/utils_certificates_action.rs | 179 ++++++++----- .../utils_certificates_action_result.rs | 12 +- .../api/utils/certificates_private_keys.http | 6 +- 19 files changed, 598 insertions(+), 204 deletions(-) create mode 100644 .cargo/config.toml delete mode 100644 .sqlx/query-1bd19d483286948d51edd7bdb4244ec2ca01252c862839e1270da51511a3fdb6.json rename .sqlx/{query-180414b2dc559e21cdfaa145d1f823ec230201a7beb8093f6ad4917209577a0d.json => query-3121f171402006b60b2e2661f4fe0e2593eff25be4d6e0aeb889c184edf99028.json} (60%) create mode 100644 .sqlx/query-37dbb4adec8bc3fbaf74b83e0364b221ffb61c5e7ddeaf80e066a58e07215611.json create mode 100644 .sqlx/query-5110c1863c5f4d3900bb865218a372986ea22847dcaacf2e9f5fb98e6960a2ba.json rename .sqlx/{query-021f5bcdbb3f216981a11a6b7e871d415e5eb9e1c9ecd3c6d2e9c740c4436896.json => query-5dd8b6dc5fc59b39e167ebfb69859efaf4bd6bea5229a2fd4cb9557540b5282c.json} (66%) delete mode 100644 .sqlx/query-e71ad284df67d3402fad13846bccc96d1e25e23b44b5c32b8e324db34de43ba9.json rename .sqlx/{query-53abe9d06e48eaa2bc32753f621d0a3bdd9209b4a7212739f3fff99af0fc57ae.json => query-ed803a11674db8d6aa3b69720f9d7f40b3a23a9f33ee0792bc388fc0300e70c1.json} (65%) diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..07d015f --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,3 @@ + +[target.'cfg(all())'] +rustflags = ["--cfg", "uuid_unstable"] diff --git a/.sqlx/query-1bd19d483286948d51edd7bdb4244ec2ca01252c862839e1270da51511a3fdb6.json b/.sqlx/query-1bd19d483286948d51edd7bdb4244ec2ca01252c862839e1270da51511a3fdb6.json deleted file mode 100644 index c11b1f9..0000000 --- a/.sqlx/query-1bd19d483286948d51edd7bdb4244ec2ca01252c862839e1270da51511a3fdb6.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\nUPDATE user_data_certificates_private_keys\nSET pkcs8 = ?3, encrypted = ?4\nWHERE user_id = ?1 AND name = ?2\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 4 - }, - "nullable": [] - }, - "hash": "1bd19d483286948d51edd7bdb4244ec2ca01252c862839e1270da51511a3fdb6" -} diff --git a/.sqlx/query-180414b2dc559e21cdfaa145d1f823ec230201a7beb8093f6ad4917209577a0d.json b/.sqlx/query-3121f171402006b60b2e2661f4fe0e2593eff25be4d6e0aeb889c184edf99028.json similarity index 60% rename from .sqlx/query-180414b2dc559e21cdfaa145d1f823ec230201a7beb8093f6ad4917209577a0d.json rename to .sqlx/query-3121f171402006b60b2e2661f4fe0e2593eff25be4d6e0aeb889c184edf99028.json index 8910f19..2ba86c6 100644 --- a/.sqlx/query-180414b2dc559e21cdfaa145d1f823ec230201a7beb8093f6ad4917209577a0d.json +++ b/.sqlx/query-3121f171402006b60b2e2661f4fe0e2593eff25be4d6e0aeb889c184edf99028.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\nDELETE FROM user_data_certificates_private_keys\nWHERE name = ?1 AND user_id = ?2\n ", + "query": "\nDELETE FROM user_data_certificates_private_keys\nWHERE user_id = ?1 AND id = ?2\n ", "describe": { "columns": [], "parameters": { @@ -8,5 +8,5 @@ }, "nullable": [] }, - "hash": "180414b2dc559e21cdfaa145d1f823ec230201a7beb8093f6ad4917209577a0d" + "hash": "3121f171402006b60b2e2661f4fe0e2593eff25be4d6e0aeb889c184edf99028" } diff --git a/.sqlx/query-37dbb4adec8bc3fbaf74b83e0364b221ffb61c5e7ddeaf80e066a58e07215611.json b/.sqlx/query-37dbb4adec8bc3fbaf74b83e0364b221ffb61c5e7ddeaf80e066a58e07215611.json new file mode 100644 index 0000000..ee78d52 --- /dev/null +++ b/.sqlx/query-37dbb4adec8bc3fbaf74b83e0364b221ffb61c5e7ddeaf80e066a58e07215611.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\nUPDATE user_data_certificates_private_keys\nSET name = ?3, pkcs8 = ?4, encrypted = ?5\nWHERE user_id = ?1 AND id = ?2\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 5 + }, + "nullable": [] + }, + "hash": "37dbb4adec8bc3fbaf74b83e0364b221ffb61c5e7ddeaf80e066a58e07215611" +} diff --git a/.sqlx/query-5110c1863c5f4d3900bb865218a372986ea22847dcaacf2e9f5fb98e6960a2ba.json b/.sqlx/query-5110c1863c5f4d3900bb865218a372986ea22847dcaacf2e9f5fb98e6960a2ba.json new file mode 100644 index 0000000..9a2e5da --- /dev/null +++ b/.sqlx/query-5110c1863c5f4d3900bb865218a372986ea22847dcaacf2e9f5fb98e6960a2ba.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\nINSERT INTO user_data_certificates_private_keys (user_id, id, name, alg, pkcs8, encrypted, created_at)\nVALUES ( ?1, ?2, ?3, ?4, ?5, ?6, ?7 )\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 7 + }, + "nullable": [] + }, + "hash": "5110c1863c5f4d3900bb865218a372986ea22847dcaacf2e9f5fb98e6960a2ba" +} diff --git a/.sqlx/query-021f5bcdbb3f216981a11a6b7e871d415e5eb9e1c9ecd3c6d2e9c740c4436896.json b/.sqlx/query-5dd8b6dc5fc59b39e167ebfb69859efaf4bd6bea5229a2fd4cb9557540b5282c.json similarity index 66% rename from .sqlx/query-021f5bcdbb3f216981a11a6b7e871d415e5eb9e1c9ecd3c6d2e9c740c4436896.json rename to .sqlx/query-5dd8b6dc5fc59b39e167ebfb69859efaf4bd6bea5229a2fd4cb9557540b5282c.json index 41d0f8b..fb709f7 100644 --- a/.sqlx/query-021f5bcdbb3f216981a11a6b7e871d415e5eb9e1c9ecd3c6d2e9c740c4436896.json +++ b/.sqlx/query-5dd8b6dc5fc59b39e167ebfb69859efaf4bd6bea5229a2fd4cb9557540b5282c.json @@ -1,31 +1,36 @@ { "db_name": "SQLite", - "query": "\nSELECT name, alg, pkcs8, encrypted, created_at\nFROM user_data_certificates_private_keys\nWHERE name = ?1 AND user_id = ?2\n ", + "query": "\nSELECT id, name, alg, pkcs8, encrypted, created_at\nFROM user_data_certificates_private_keys\nWHERE user_id = ?1 AND id = ?2\n ", "describe": { "columns": [ { - "name": "name", + "name": "id", "ordinal": 0, + "type_info": "Blob" + }, + { + "name": "name", + "ordinal": 1, "type_info": "Text" }, { "name": "alg", - "ordinal": 1, + "ordinal": 2, "type_info": "Blob" }, { "name": "pkcs8", - "ordinal": 2, + "ordinal": 3, "type_info": "Blob" }, { "name": "encrypted", - "ordinal": 3, + "ordinal": 4, "type_info": "Int64" }, { "name": "created_at", - "ordinal": 4, + "ordinal": 5, "type_info": "Int64" } ], @@ -37,8 +42,9 @@ false, false, false, + false, false ] }, - "hash": "021f5bcdbb3f216981a11a6b7e871d415e5eb9e1c9ecd3c6d2e9c740c4436896" + "hash": "5dd8b6dc5fc59b39e167ebfb69859efaf4bd6bea5229a2fd4cb9557540b5282c" } diff --git a/.sqlx/query-e71ad284df67d3402fad13846bccc96d1e25e23b44b5c32b8e324db34de43ba9.json b/.sqlx/query-e71ad284df67d3402fad13846bccc96d1e25e23b44b5c32b8e324db34de43ba9.json deleted file mode 100644 index e8fd356..0000000 --- a/.sqlx/query-e71ad284df67d3402fad13846bccc96d1e25e23b44b5c32b8e324db34de43ba9.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\nINSERT INTO user_data_certificates_private_keys (user_id, name, alg, pkcs8, encrypted, created_at)\nVALUES ( ?1, ?2, ?3, ?4, ?5, ?6 )\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 6 - }, - "nullable": [] - }, - "hash": "e71ad284df67d3402fad13846bccc96d1e25e23b44b5c32b8e324db34de43ba9" -} diff --git a/.sqlx/query-53abe9d06e48eaa2bc32753f621d0a3bdd9209b4a7212739f3fff99af0fc57ae.json b/.sqlx/query-ed803a11674db8d6aa3b69720f9d7f40b3a23a9f33ee0792bc388fc0300e70c1.json similarity index 65% rename from .sqlx/query-53abe9d06e48eaa2bc32753f621d0a3bdd9209b4a7212739f3fff99af0fc57ae.json rename to .sqlx/query-ed803a11674db8d6aa3b69720f9d7f40b3a23a9f33ee0792bc388fc0300e70c1.json index f46734f..4b6e924 100644 --- a/.sqlx/query-53abe9d06e48eaa2bc32753f621d0a3bdd9209b4a7212739f3fff99af0fc57ae.json +++ b/.sqlx/query-ed803a11674db8d6aa3b69720f9d7f40b3a23a9f33ee0792bc388fc0300e70c1.json @@ -1,31 +1,36 @@ { "db_name": "SQLite", - "query": "\nSELECT name, alg, x'' as \"pkcs8!\", encrypted, created_at\nFROM user_data_certificates_private_keys\nWHERE user_id = ?1\nORDER BY created_at\n ", + "query": "\nSELECT id, name, alg, x'' as \"pkcs8!\", encrypted, created_at\nFROM user_data_certificates_private_keys\nWHERE user_id = ?1\nORDER BY created_at\n ", "describe": { "columns": [ { - "name": "name", + "name": "id", "ordinal": 0, + "type_info": "Blob" + }, + { + "name": "name", + "ordinal": 1, "type_info": "Text" }, { "name": "alg", - "ordinal": 1, + "ordinal": 2, "type_info": "Blob" }, { "name": "pkcs8!", - "ordinal": 2, + "ordinal": 3, "type_info": "Blob" }, { "name": "encrypted", - "ordinal": 3, + "ordinal": 4, "type_info": "Int64" }, { "name": "created_at", - "ordinal": 4, + "ordinal": 5, "type_info": "Int64" } ], @@ -37,8 +42,9 @@ false, false, false, + false, false ] }, - "hash": "53abe9d06e48eaa2bc32753f621d0a3bdd9209b4a7212739f3fff99af0fc57ae" + "hash": "ed803a11674db8d6aa3b69720f9d7f40b3a23a9f33ee0792bc388fc0300e70c1" } diff --git a/Cargo.lock b/Cargo.lock index 0ebde37..2908574 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -695,6 +695,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + [[package]] name = "atomic-waker" version = "1.1.2" @@ -4992,10 +4998,11 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" dependencies = [ + "atomic", "getrandom", "serde", ] diff --git a/Cargo.toml b/Cargo.toml index d43fe8a..110892e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,7 +57,7 @@ tokio-cron-scheduler = "0.9.4" trust-dns-resolver = "0.23.1" url = "2.4.1" urlencoding = "2.1.3" -uuid = "1.4.1" +uuid = "1.5.0" webauthn-rs = "0.4.8" zip = "0.6.6" @@ -102,6 +102,7 @@ default = [ "tokio/rt-multi-thread", "tokio/macros", "url/serde", + "uuid/v7", "webauthn-rs/danger-allow-state-serialisation" ] diff --git a/migrations/20231019195044_certificates_private_keys.sql b/migrations/20231019195044_certificates_private_keys.sql index 97fc6d6..df450e4 100644 --- a/migrations/20231019195044_certificates_private_keys.sql +++ b/migrations/20231019195044_certificates_private_keys.sql @@ -15,11 +15,12 @@ WHERE -- Create table to store private keys. CREATE TABLE IF NOT EXISTS user_data_certificates_private_keys ( + id BLOB PRIMARY KEY, name TEXT NOT NULL COLLATE NOCASE, alg BLOB NOT NULL, pkcs8 BLOB NOT NULL, encrypted INTEGER NOT NULL, created_at INTEGER NOT NULL, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - PRIMARY KEY (name, user_id) + UNIQUE (name, user_id) ) STRICT; diff --git a/src/utils/api_ext.rs b/src/utils/api_ext.rs index f2b04f5..ce66bbe 100644 --- a/src/utils/api_ext.rs +++ b/src/utils/api_ext.rs @@ -90,8 +90,8 @@ mod tests { }, Util { id: 11, - handle: "certificates__templates", - name: "Templates", + handle: "certificates__certificate_templates", + name: "Certificate templates", keywords: Some( "digital certificates x509 X.509 ssl tls openssl public private key encryption self-signed pki templates", ), diff --git a/src/utils/certificates/api_ext.rs b/src/utils/certificates/api_ext.rs index ba5589c..99a2299 100644 --- a/src/utils/certificates/api_ext.rs +++ b/src/utils/certificates/api_ext.rs @@ -29,6 +29,7 @@ use std::{ time::Instant, }; use time::OffsetDateTime; +use uuid::Uuid; use zip::{write::FileOptions, CompressionMethod, ZipWriter}; /// API extension to work with certificates utilities. @@ -42,16 +43,16 @@ impl<'a, DR: DnsResolver, ET: EmailTransport> CertificatesApi<'a, DR, ET> { Self { api } } - /// Retrieves the private key with the specified name. + /// Retrieves the private key with the specified ID. pub async fn get_private_key( &self, user_id: UserId, - name: &str, + id: Uuid, ) -> anyhow::Result> { self.api .db .certificates() - .get_private_key(user_id, name) + .get_private_key(user_id, id) .await } @@ -64,6 +65,7 @@ impl<'a, DR: DnsResolver, ET: EmailTransport> CertificatesApi<'a, DR, ET> { passphrase: Option<&str>, ) -> anyhow::Result { let private_key = PrivateKey { + id: Uuid::now_v7(), name: name.into(), alg, pkcs8: Self::export_private_key_to_pkcs8(Self::generate_private_key(alg)?, passphrase)?, @@ -83,37 +85,55 @@ impl<'a, DR: DnsResolver, ET: EmailTransport> CertificatesApi<'a, DR, ET> { Ok(private_key) } - /// Updates private key passphrase. - pub async fn change_private_key_passphrase( + /// Updates private key (only name and passphrases are updatable). + pub async fn update_private_key( &self, user_id: UserId, - name: &str, + id: Uuid, + name: Option<&str>, passphrase: Option<&str>, new_passphrase: Option<&str>, ) -> anyhow::Result<()> { - let Some(private_key) = self.get_private_key(user_id, name).await? else { + let Some(private_key) = self.get_private_key(user_id, id).await? else { bail!(SecutilsError::client(format!( - "Private key ('{name}') is not found." + "Private key ('{id}') is not found." ))); }; - // Try to decrypt private key using the provided passphrase. - let pkcs8_private_key = Self::import_private_key_from_pkcs8(&private_key.pkcs8, passphrase) - .map_err(|err| { - SecutilsError::client_with_root_cause(anyhow!(err).context(format!( - "Unable to decrypt private key ('{name}') with the provided passphrase." - ))) - })?; + // If name update is needed, extract it from parameters. + let name = if let Some(name) = name { + name.to_string() + } else { + private_key.name + }; + + // If passphrase update is needed, try to decrypt private key using the provided passphrase. + let (pkcs8, encrypted) = if passphrase != new_passphrase { + let pkcs8_private_key = + Self::import_private_key_from_pkcs8(&private_key.pkcs8, passphrase).map_err( + |err| { + SecutilsError::client_with_root_cause(anyhow!(err).context(format!( + "Unable to decrypt private key ('{id}') with the provided passphrase." + ))) + }, + )?; + ( + Self::export_private_key_to_pkcs8(pkcs8_private_key, new_passphrase)?, + new_passphrase.is_some(), + ) + } else { + (private_key.pkcs8, private_key.encrypted) + }; - // Convert private key to PKCS8 using the new passphrase, and update it in the database. self.api .db .certificates() .update_private_key( user_id, &PrivateKey { - pkcs8: Self::export_private_key_to_pkcs8(pkcs8_private_key, new_passphrase)?, - encrypted: new_passphrase.is_some(), + name, + pkcs8, + encrypted, ..private_key }, ) @@ -121,11 +141,11 @@ impl<'a, DR: DnsResolver, ET: EmailTransport> CertificatesApi<'a, DR, ET> { } /// Removes private key with the specified name. - pub async fn remove_private_key(&self, user_id: UserId, name: &str) -> anyhow::Result<()> { + pub async fn remove_private_key(&self, user_id: UserId, id: Uuid) -> anyhow::Result<()> { self.api .db .certificates() - .remove_private_key(user_id, name) + .remove_private_key(user_id, id) .await } @@ -133,14 +153,14 @@ impl<'a, DR: DnsResolver, ET: EmailTransport> CertificatesApi<'a, DR, ET> { pub async fn export_private_key( &self, user_id: UserId, - name: &str, + id: Uuid, format: ExportFormat, passphrase: Option<&str>, export_passphrase: Option<&str>, ) -> anyhow::Result> { - let Some(private_key) = self.get_private_key(user_id, name).await? else { + let Some(private_key) = self.get_private_key(user_id, id).await? else { bail!(SecutilsError::client(format!( - "Private key ('{name}') is not found." + "Private key ('{id}') is not found." ))); }; @@ -148,7 +168,7 @@ impl<'a, DR: DnsResolver, ET: EmailTransport> CertificatesApi<'a, DR, ET> { let pkcs8_private_key = Self::import_private_key_from_pkcs8(&private_key.pkcs8, passphrase) .map_err(|err| { SecutilsError::client_with_root_cause(anyhow!(err).context(format!( - "Unable to decrypt private key ('{name}') with the provided passphrase." + "Unable to decrypt private key ('{id}') with the provided passphrase." ))) })?; @@ -168,7 +188,7 @@ impl<'a, DR: DnsResolver, ET: EmailTransport> CertificatesApi<'a, DR, ET> { export_result.map_err(|err| { SecutilsError::client_with_root_cause(anyhow!(err).context(format!( - "Unable to export private key ('{name}') to the specified format ('{format:?}')." + "Unable to export private key ('{id}') to the specified format ('{format:?}')." ))) .into() }) @@ -577,12 +597,12 @@ mod tests { // Set passphrase. certificates - .change_private_key_passphrase(mock_user.id, "pk", None, Some("pass")) + .update_private_key(mock_user.id, private_key.id, None, None, Some("pass")) .await?; // Decrypting without passphrase should fail. let private_key = certificates - .get_private_key(mock_user.id, "pk") + .get_private_key(mock_user.id, private_key.id) .await? .unwrap(); assert!( @@ -603,12 +623,18 @@ mod tests { // Change passphrase. certificates - .change_private_key_passphrase(mock_user.id, "pk", Some("pass"), Some("pass-1")) + .update_private_key( + mock_user.id, + private_key.id, + None, + Some("pass"), + Some("pass-1"), + ) .await?; // Decrypting without passphrase should fail. let private_key = certificates - .get_private_key(mock_user.id, "pk") + .get_private_key(mock_user.id, private_key.id) .await? .unwrap(); assert!( @@ -621,7 +647,7 @@ mod tests { // Decrypting with old passphrase should fail. let private_key = certificates - .get_private_key(mock_user.id, "pk") + .get_private_key(mock_user.id, private_key.id) .await? .unwrap(); assert!( @@ -642,12 +668,12 @@ mod tests { // Remove passphrase. certificates - .change_private_key_passphrase(mock_user.id, "pk", Some("pass-1"), None) + .update_private_key(mock_user.id, private_key.id, None, Some("pass-1"), None) .await?; // Decrypting without passphrase should succeed. let private_key = certificates - .get_private_key(mock_user.id, "pk") + .get_private_key(mock_user.id, private_key.id) .await? .unwrap(); assert!( @@ -660,7 +686,7 @@ mod tests { // Decrypting with old passphrase should fail. let private_key = certificates - .get_private_key(mock_user.id, "pk") + .get_private_key(mock_user.id, private_key.id) .await? .unwrap(); assert!( @@ -682,6 +708,107 @@ mod tests { Ok(()) } + #[actix_rt::test] + async fn can_change_private_key_name() -> anyhow::Result<()> { + let api = mock_api().await?; + + let mock_user = mock_user()?; + api.db.insert_user(&mock_user).await?; + + let certificates = CertificatesApi::new(&api); + let private_key = certificates + .create_private_key( + mock_user.id, + "pk", + PrivateKeyAlgorithm::Ed25519, + Some("pass"), + ) + .await?; + + // Update name. + certificates + .update_private_key(mock_user.id, private_key.id, Some("pk-new"), None, None) + .await?; + + // Name should change, and pkcs8 shouldn't change. + let updated_private_key = certificates + .get_private_key(mock_user.id, private_key.id) + .await? + .unwrap(); + assert_eq!(updated_private_key.name, "pk-new"); + assert_eq!(private_key.pkcs8, updated_private_key.pkcs8); + assert_eq!(private_key.encrypted, updated_private_key.encrypted); + + // Decrypting with the old passphrase should succeed. + assert!( + CertificatesApi::::import_private_key_from_pkcs8( + &private_key.pkcs8, + Some("pass"), + ) + .is_ok() + ); + + // Change both name and passphrase. + certificates + .update_private_key( + mock_user.id, + private_key.id, + Some("pk-new-new"), + Some("pass"), + Some("pass-1"), + ) + .await?; + + // Name should change and decrypting with old passphrase should fail. + let updated_private_key = certificates + .get_private_key(mock_user.id, private_key.id) + .await? + .unwrap(); + assert_eq!(updated_private_key.name, "pk-new-new"); + assert!( + CertificatesApi::::import_private_key_from_pkcs8( + &updated_private_key.pkcs8, + Some("pass"), + ) + .is_err() + ); + // Decrypting with new passphrase should succeed. + assert!( + CertificatesApi::::import_private_key_from_pkcs8( + &updated_private_key.pkcs8, + Some("pass-1"), + ) + .is_ok() + ); + + // Remove passphrase and return old name back. + certificates + .update_private_key( + mock_user.id, + private_key.id, + Some("pk"), + Some("pass-1"), + None, + ) + .await?; + + // Name should change and decrypting without passphrase should succeed. + let updated_private_key = certificates + .get_private_key(mock_user.id, private_key.id) + .await? + .unwrap(); + assert_eq!(updated_private_key.name, "pk"); + assert!( + CertificatesApi::::import_private_key_from_pkcs8( + &updated_private_key.pkcs8, + None, + ) + .is_ok() + ); + + Ok(()) + } + #[actix_rt::test] async fn can_export_private_key() -> anyhow::Result<()> { let api = mock_api().await?; @@ -691,13 +818,19 @@ mod tests { // Create private key without passphrase. let certificates = CertificatesApi::new(&api); - certificates + let private_key = certificates .create_private_key(mock_user.id, "pk", PrivateKeyAlgorithm::Ed25519, None) .await?; // Export private key without passphrase and make sure it can be without passphrase. let pkcs8 = certificates - .export_private_key(mock_user.id, "pk", ExportFormat::Pkcs8, None, None) + .export_private_key( + mock_user.id, + private_key.id, + ExportFormat::Pkcs8, + None, + None, + ) .await?; assert!( CertificatesApi::::import_private_key_from_pkcs8( @@ -707,7 +840,13 @@ mod tests { ); // Export private key with passphrase and make sure it can be imported with passphrase. let pkcs8 = certificates - .export_private_key(mock_user.id, "pk", ExportFormat::Pkcs8, None, Some("pass")) + .export_private_key( + mock_user.id, + private_key.id, + ExportFormat::Pkcs8, + None, + Some("pass"), + ) .await?; assert!( CertificatesApi::::import_private_key_from_pkcs8( @@ -719,12 +858,18 @@ mod tests { // Set passphrase and repeat. certificates - .change_private_key_passphrase(mock_user.id, "pk", None, Some("pass")) + .update_private_key(mock_user.id, private_key.id, None, None, Some("pass")) .await?; // Export private key without passphrase and make sure it can be without passphrase. let pkcs8 = certificates - .export_private_key(mock_user.id, "pk", ExportFormat::Pkcs8, Some("pass"), None) + .export_private_key( + mock_user.id, + private_key.id, + ExportFormat::Pkcs8, + Some("pass"), + None, + ) .await?; assert!( CertificatesApi::::import_private_key_from_pkcs8( @@ -736,7 +881,7 @@ mod tests { let pkcs8 = certificates .export_private_key( mock_user.id, - "pk", + private_key.id, ExportFormat::Pkcs8, Some("pass"), Some("pass"), @@ -767,15 +912,17 @@ mod tests { assert_eq!( private_key, certificates - .get_private_key(mock_user.id, "pk") + .get_private_key(mock_user.id, private_key.id) .await? .unwrap() ); - certificates.remove_private_key(mock_user.id, "pk").await?; + certificates + .remove_private_key(mock_user.id, private_key.id) + .await?; assert!(certificates - .get_private_key(mock_user.id, "pk") + .get_private_key(mock_user.id, private_key.id) .await? .is_none()); @@ -816,12 +963,14 @@ mod tests { })?; assert_eq!( certificates.get_private_keys(mock_user.id).await?, - vec![private_key_one, private_key_two] + vec![private_key_one.clone(), private_key_two.clone()] ); - certificates.remove_private_key(mock_user.id, "pk").await?; certificates - .remove_private_key(mock_user.id, "pk-2") + .remove_private_key(mock_user.id, private_key_one.id) + .await?; + certificates + .remove_private_key(mock_user.id, private_key_two.id) .await?; assert!(certificates diff --git a/src/utils/certificates/database_ext.rs b/src/utils/certificates/database_ext.rs index 204c4cd..96f5ab7 100644 --- a/src/utils/certificates/database_ext.rs +++ b/src/utils/certificates/database_ext.rs @@ -4,6 +4,7 @@ use self::raw_private_key::RawPrivateKey; use crate::{database::Database, error::Error as SecutilsError, users::UserId, utils::PrivateKey}; use anyhow::{anyhow, bail}; use sqlx::{error::ErrorKind as SqlxErrorKind, query, query_as, Pool, Sqlite}; +use uuid::Uuid; /// A database extension for the certificate utility-related operations. pub struct CertificatesDatabaseExt<'pool> { @@ -19,17 +20,18 @@ impl<'pool> CertificatesDatabaseExt<'pool> { pub async fn get_private_key( &self, user_id: UserId, - name: &str, + id: Uuid, ) -> anyhow::Result> { + let id = id.as_ref(); query_as!( RawPrivateKey, r#" -SELECT name, alg, pkcs8, encrypted, created_at +SELECT id, name, alg, pkcs8, encrypted, created_at FROM user_data_certificates_private_keys -WHERE name = ?1 AND user_id = ?2 +WHERE user_id = ?1 AND id = ?2 "#, - name, - *user_id + *user_id, + id ) .fetch_optional(self.pool) .await? @@ -46,10 +48,11 @@ WHERE name = ?1 AND user_id = ?2 let raw_private_key = RawPrivateKey::try_from(private_key)?; let result = query!( r#" -INSERT INTO user_data_certificates_private_keys (user_id, name, alg, pkcs8, encrypted, created_at) -VALUES ( ?1, ?2, ?3, ?4, ?5, ?6 ) +INSERT INTO user_data_certificates_private_keys (user_id, id, name, alg, pkcs8, encrypted, created_at) +VALUES ( ?1, ?2, ?3, ?4, ?5, ?6, ?7 ) "#, *user_id, + raw_private_key.id, raw_private_key.name, raw_private_key.alg, raw_private_key.pkcs8, @@ -80,7 +83,7 @@ VALUES ( ?1, ?2, ?3, ?4, ?5, ?6 ) Ok(()) } - /// Upserts private key (only `pkcs8` content can be updated due to password change). + /// Updates private key (only `name` and `pkcs8` content can be updated due to password change). pub async fn update_private_key( &self, user_id: UserId, @@ -90,36 +93,59 @@ VALUES ( ?1, ?2, ?3, ?4, ?5, ?6 ) let result = query!( r#" UPDATE user_data_certificates_private_keys -SET pkcs8 = ?3, encrypted = ?4 -WHERE user_id = ?1 AND name = ?2 +SET name = ?3, pkcs8 = ?4, encrypted = ?5 +WHERE user_id = ?1 AND id = ?2 "#, *user_id, + raw_private_key.id, raw_private_key.name, raw_private_key.pkcs8, raw_private_key.encrypted ) .execute(self.pool) - .await?; + .await; - if result.rows_affected() == 0 { - bail!(SecutilsError::client(format!( - "A private key ('{}') doesn't exist.", - private_key.name - ))); + match result { + Ok(result) => { + if result.rows_affected() == 0 { + bail!(SecutilsError::client(format!( + "A private key ('{}') doesn't exist.", + private_key.name + ))); + } + } + Err(err) => { + let is_conflict_error = err + .as_database_error() + .map(|db_error| matches!(db_error.kind(), SqlxErrorKind::UniqueViolation)) + .unwrap_or_default(); + bail!(if is_conflict_error { + SecutilsError::client_with_root_cause(anyhow!(err).context(format!( + "Private key ('{}') already exists.", + private_key.name + ))) + } else { + SecutilsError::from(anyhow!(err).context(format!( + "Couldn't update private key ('{}') due to unknown reason.", + private_key.name + ))) + }); + } } Ok(()) } /// Removes private key for the specified user with the specified name. - pub async fn remove_private_key(&self, user_id: UserId, name: &str) -> anyhow::Result<()> { + pub async fn remove_private_key(&self, user_id: UserId, id: Uuid) -> anyhow::Result<()> { + let id = id.as_ref(); query!( r#" DELETE FROM user_data_certificates_private_keys -WHERE name = ?1 AND user_id = ?2 +WHERE user_id = ?1 AND id = ?2 "#, - name, - *user_id + *user_id, + id ) .execute(self.pool) .await?; @@ -134,7 +160,7 @@ WHERE name = ?1 AND user_id = ?2 let raw_private_keys = query_as!( RawPrivateKey, r#" -SELECT name, alg, x'' as "pkcs8!", encrypted, created_at +SELECT id, name, alg, x'' as "pkcs8!", encrypted, created_at FROM user_data_certificates_private_keys WHERE user_id = ?1 ORDER BY created_at @@ -170,6 +196,7 @@ mod tests { use actix_web::ResponseError; use insta::assert_debug_snapshot; use time::OffsetDateTime; + use uuid::uuid; #[actix_rt::test] async fn can_add_and_retrieve_private_keys() -> anyhow::Result<()> { @@ -179,6 +206,7 @@ mod tests { let mut private_keys = vec![ PrivateKey { + id: uuid!("00000000-0000-0000-0000-000000000001"), name: "pk-name".to_string(), alg: PrivateKeyAlgorithm::Rsa { key_size: PrivateKeySize::Size2048, @@ -188,6 +216,7 @@ mod tests { created_at: OffsetDateTime::from_unix_timestamp(946720800)?, }, PrivateKey { + id: uuid!("00000000-0000-0000-0000-000000000002"), name: "pk-name-2".to_string(), alg: PrivateKeyAlgorithm::Dsa { key_size: PrivateKeySize::Size2048, @@ -206,21 +235,21 @@ mod tests { let private_key = db .certificates() - .get_private_key(user.id, "pk-name") + .get_private_key(user.id, private_keys[0].id) .await? .unwrap(); assert_eq!(private_key, private_keys.remove(0)); let private_key = db .certificates() - .get_private_key(user.id, "pk-name-2") + .get_private_key(user.id, private_keys[0].id) .await? .unwrap(); assert_eq!(private_key, private_keys.remove(0)); assert!(db .certificates() - .get_private_key(user.id, "pk-name-3") + .get_private_key(user.id, uuid!("00000000-0000-0000-0000-000000000003")) .await? .is_none()); @@ -228,12 +257,13 @@ mod tests { } #[actix_rt::test] - async fn correctly_handles_duplicated_private_keys() -> anyhow::Result<()> { + async fn correctly_handles_duplicated_private_keys_on_insert() -> anyhow::Result<()> { let user = mock_user()?; let db = mock_db().await?; db.insert_user(&user).await?; let private_key = PrivateKey { + id: uuid!("00000000-0000-0000-0000-000000000001"), name: "pk-name".to_string(), alg: PrivateKeyAlgorithm::Rsa { key_size: PrivateKeySize::Size2048, @@ -262,7 +292,7 @@ mod tests { context: "Private key (\'pk-name\') already exists.", source: Database( SqliteError { - code: 1555, + code: 2067, message: "UNIQUE constraint failed: user_data_certificates_private_keys.name, user_data_certificates_private_keys.user_id", }, ), @@ -283,6 +313,7 @@ mod tests { .insert_private_key( user.id, &PrivateKey { + id: uuid!("00000000-0000-0000-0000-000000000001"), name: "pk-name".to_string(), alg: PrivateKeyAlgorithm::Rsa { key_size: PrivateKeySize::Size2048, @@ -298,7 +329,8 @@ mod tests { .update_private_key( user.id, &PrivateKey { - name: "pk-name".to_string(), + id: uuid!("00000000-0000-0000-0000-000000000001"), + name: "pk-name-new".to_string(), alg: PrivateKeyAlgorithm::Rsa { key_size: PrivateKeySize::Size1024, }, @@ -311,13 +343,14 @@ mod tests { let private_key = db .certificates() - .get_private_key(user.id, "pk-name") + .get_private_key(user.id, uuid!("00000000-0000-0000-0000-000000000001")) .await? .unwrap(); assert_eq!( private_key, PrivateKey { - name: "pk-name".to_string(), + id: uuid!("00000000-0000-0000-0000-000000000001"), + name: "pk-name-new".to_string(), alg: PrivateKeyAlgorithm::Rsa { key_size: PrivateKeySize::Size2048, }, @@ -330,6 +363,112 @@ mod tests { Ok(()) } + #[actix_rt::test] + async fn correctly_handles_duplicated_private_keys_on_update() -> anyhow::Result<()> { + let user = mock_user()?; + let db = mock_db().await?; + db.insert_user(&user).await?; + + let private_key_a = PrivateKey { + id: uuid!("00000000-0000-0000-0000-000000000001"), + name: "pk-name-a".to_string(), + alg: PrivateKeyAlgorithm::Rsa { + key_size: PrivateKeySize::Size2048, + }, + pkcs8: vec![1, 2, 3], + encrypted: true, + created_at: OffsetDateTime::from_unix_timestamp(946720800)?, + }; + db.certificates() + .insert_private_key(user.id, &private_key_a) + .await?; + + let private_key_b = PrivateKey { + id: uuid!("00000000-0000-0000-0000-000000000002"), + name: "pk-name-b".to_string(), + alg: PrivateKeyAlgorithm::Rsa { + key_size: PrivateKeySize::Size2048, + }, + pkcs8: vec![3, 4, 5], + encrypted: true, + created_at: OffsetDateTime::from_unix_timestamp(946720800)?, + }; + db.certificates() + .insert_private_key(user.id, &private_key_b) + .await?; + + let update_error = db + .certificates() + .update_private_key( + user.id, + &PrivateKey { + id: uuid!("00000000-0000-0000-0000-000000000002"), + name: "pk-name-a".to_string(), + alg: PrivateKeyAlgorithm::Rsa { + key_size: PrivateKeySize::Size2048, + }, + pkcs8: vec![3, 4, 5], + encrypted: true, + created_at: OffsetDateTime::from_unix_timestamp(946720800)?, + }, + ) + .await + .unwrap_err() + .downcast::() + .unwrap(); + assert_eq!(update_error.status_code(), 400); + assert_debug_snapshot!( + update_error, + @r###" + Error { + context: "Private key (\'pk-name-a\') already exists.", + source: Database( + SqliteError { + code: 2067, + message: "UNIQUE constraint failed: user_data_certificates_private_keys.name, user_data_certificates_private_keys.user_id", + }, + ), + } + "### + ); + + Ok(()) + } + + #[actix_rt::test] + async fn correctly_handles_non_existent_private_keys_on_update() -> anyhow::Result<()> { + let user = mock_user()?; + let db = mock_db().await?; + db.insert_user(&user).await?; + + let update_error = db + .certificates() + .update_private_key( + user.id, + &PrivateKey { + id: uuid!("00000000-0000-0000-0000-000000000002"), + name: "pk-name-a".to_string(), + alg: PrivateKeyAlgorithm::Rsa { + key_size: PrivateKeySize::Size2048, + }, + pkcs8: vec![3, 4, 5], + encrypted: true, + created_at: OffsetDateTime::from_unix_timestamp(946720800)?, + }, + ) + .await + .unwrap_err() + .downcast::() + .unwrap(); + assert_eq!(update_error.status_code(), 400); + assert_debug_snapshot!( + update_error, + @r###""A private key ('pk-name-a') doesn't exist.""### + ); + + Ok(()) + } + #[actix_rt::test] async fn can_remove_private_keys() -> anyhow::Result<()> { let user = mock_user()?; @@ -338,6 +477,7 @@ mod tests { let mut private_keys = vec![ PrivateKey { + id: uuid!("00000000-0000-0000-0000-000000000001"), name: "pk-name".to_string(), alg: PrivateKeyAlgorithm::Rsa { key_size: PrivateKeySize::Size2048, @@ -347,6 +487,7 @@ mod tests { created_at: OffsetDateTime::from_unix_timestamp(946720800)?, }, PrivateKey { + id: uuid!("00000000-0000-0000-0000-000000000002"), name: "pk-name-2".to_string(), alg: PrivateKeyAlgorithm::Dsa { key_size: PrivateKeySize::Size2048, @@ -365,48 +506,48 @@ mod tests { let private_key = db .certificates() - .get_private_key(user.id, "pk-name") + .get_private_key(user.id, uuid!("00000000-0000-0000-0000-000000000001")) .await? .unwrap(); assert_eq!(private_key, private_keys.remove(0)); - let private_key = db + let private_key_2 = db .certificates() - .get_private_key(user.id, "pk-name-2") + .get_private_key(user.id, uuid!("00000000-0000-0000-0000-000000000002")) .await? .unwrap(); - assert_eq!(private_key, private_keys[0].clone()); + assert_eq!(private_key_2, private_keys[0].clone()); db.certificates() - .remove_private_key(user.id, "pk-name") + .remove_private_key(user.id, uuid!("00000000-0000-0000-0000-000000000001")) .await?; let private_key = db .certificates() - .get_private_key(user.id, "pk-name") + .get_private_key(user.id, uuid!("00000000-0000-0000-0000-000000000001")) .await?; assert!(private_key.is_none()); let private_key = db .certificates() - .get_private_key(user.id, "pk-name-2") + .get_private_key(user.id, uuid!("00000000-0000-0000-0000-000000000002")) .await? .unwrap(); assert_eq!(private_key, private_keys.remove(0)); db.certificates() - .remove_private_key(user.id, "pk-name-2") + .remove_private_key(user.id, uuid!("00000000-0000-0000-0000-000000000002")) .await?; let private_key = db .certificates() - .get_private_key(user.id, "pk-name") + .get_private_key(user.id, uuid!("00000000-0000-0000-0000-000000000001")) .await?; assert!(private_key.is_none()); let private_key = db .certificates() - .get_private_key(user.id, "pk-name-2") + .get_private_key(user.id, uuid!("00000000-0000-0000-0000-000000000002")) .await?; assert!(private_key.is_none()); @@ -421,6 +562,7 @@ mod tests { let private_keys = vec![ PrivateKey { + id: uuid!("00000000-0000-0000-0000-000000000001"), name: "pk-name".to_string(), alg: PrivateKeyAlgorithm::Rsa { key_size: PrivateKeySize::Size2048, @@ -430,6 +572,7 @@ mod tests { created_at: OffsetDateTime::from_unix_timestamp(946720800)?, }, PrivateKey { + id: uuid!("00000000-0000-0000-0000-000000000002"), name: "pk-name-2".to_string(), alg: PrivateKeyAlgorithm::Dsa { key_size: PrivateKeySize::Size2048, diff --git a/src/utils/certificates/database_ext/raw_private_key.rs b/src/utils/certificates/database_ext/raw_private_key.rs index c212762..194d6dc 100644 --- a/src/utils/certificates/database_ext/raw_private_key.rs +++ b/src/utils/certificates/database_ext/raw_private_key.rs @@ -1,6 +1,7 @@ use crate::utils::{PrivateKey, PrivateKeyAlgorithm, PrivateKeyEllipticCurve, PrivateKeySize}; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; +use uuid::Uuid; /// Main `KeyAlgorithm` enum has Serde attributes that are needed fro JSON serialization, but aren't /// compatible with the `postcard`. @@ -36,6 +37,7 @@ impl From for RawPrivateKeyAlgorithm { #[derive(Debug, Eq, PartialEq, Clone)] pub(super) struct RawPrivateKey { + pub id: Vec, pub name: String, pub alg: Vec, pub pkcs8: Vec, @@ -48,6 +50,7 @@ impl TryFrom for PrivateKey { fn try_from(raw: RawPrivateKey) -> Result { Ok(PrivateKey { + id: Uuid::from_slice(raw.id.as_slice())?, name: raw.name, alg: postcard::from_bytes::(&raw.alg)?.into(), pkcs8: raw.pkcs8, @@ -62,6 +65,7 @@ impl TryFrom<&PrivateKey> for RawPrivateKey { fn try_from(item: &PrivateKey) -> Result { Ok(RawPrivateKey { + id: item.id.as_ref().to_vec(), name: item.name.clone(), alg: postcard::to_stdvec(&RawPrivateKeyAlgorithm::from(item.alg))?, pkcs8: item.pkcs8.clone(), @@ -76,6 +80,7 @@ mod tests { use super::{RawPrivateKey, RawPrivateKeyAlgorithm}; use crate::utils::{PrivateKey, PrivateKeyAlgorithm, PrivateKeyEllipticCurve, PrivateKeySize}; use time::OffsetDateTime; + use uuid::uuid; #[test] fn can_convert_to_key_algorithm() -> anyhow::Result<()> { @@ -155,6 +160,9 @@ mod tests { fn can_convert_into_private_key() -> anyhow::Result<()> { assert_eq!( PrivateKey::try_from(RawPrivateKey { + id: uuid!("00000000-0000-0000-0000-000000000001") + .as_bytes() + .to_vec(), name: "pk-name".to_string(), alg: vec![0, 1], pkcs8: vec![1, 2, 3], @@ -163,6 +171,7 @@ mod tests { created_at: 946720800, })?, PrivateKey { + id: uuid!("00000000-0000-0000-0000-000000000001"), name: "pk-name".to_string(), alg: PrivateKeyAlgorithm::Rsa { key_size: PrivateKeySize::Size2048 @@ -175,6 +184,9 @@ mod tests { assert_eq!( PrivateKey::try_from(RawPrivateKey { + id: uuid!("00000000-0000-0000-0000-000000000001") + .as_bytes() + .to_vec(), name: "pk-name".to_string(), alg: vec![0, 1], pkcs8: vec![1, 2, 3], @@ -183,6 +195,7 @@ mod tests { created_at: 946720800, })?, PrivateKey { + id: uuid!("00000000-0000-0000-0000-000000000001"), name: "pk-name".to_string(), alg: PrivateKeyAlgorithm::Rsa { key_size: PrivateKeySize::Size2048 @@ -200,6 +213,7 @@ mod tests { fn can_convert_into_raw_private_key() -> anyhow::Result<()> { assert_eq!( RawPrivateKey::try_from(&PrivateKey { + id: uuid!("00000000-0000-0000-0000-000000000001"), name: "pk-name".to_string(), alg: PrivateKeyAlgorithm::Rsa { key_size: PrivateKeySize::Size2048 @@ -209,6 +223,9 @@ mod tests { created_at: OffsetDateTime::from_unix_timestamp(946720800)?, })?, RawPrivateKey { + id: uuid!("00000000-0000-0000-0000-000000000001") + .as_bytes() + .to_vec(), name: "pk-name".to_string(), alg: vec![0, 1], pkcs8: vec![1, 2, 3], @@ -220,6 +237,7 @@ mod tests { assert_eq!( RawPrivateKey::try_from(&PrivateKey { + id: uuid!("00000000-0000-0000-0000-000000000001"), name: "pk-name".to_string(), alg: PrivateKeyAlgorithm::Rsa { key_size: PrivateKeySize::Size2048 @@ -229,6 +247,9 @@ mod tests { created_at: OffsetDateTime::from_unix_timestamp(946720800)?, })?, RawPrivateKey { + id: uuid!("00000000-0000-0000-0000-000000000001") + .as_bytes() + .to_vec(), name: "pk-name".to_string(), alg: vec![0, 1], pkcs8: vec![1, 2, 3], diff --git a/src/utils/certificates/private_keys/private_key.rs b/src/utils/certificates/private_keys/private_key.rs index f28e125..0b86af8 100644 --- a/src/utils/certificates/private_keys/private_key.rs +++ b/src/utils/certificates/private_keys/private_key.rs @@ -1,11 +1,14 @@ use crate::utils::PrivateKeyAlgorithm; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; +use uuid::Uuid; /// Describes stored private key. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct PrivateKey { + /// Unique private key id (UUIDv7). + pub id: Uuid, /// Arbitrary name of the private key. pub name: String, /// Algorithm of the private key (RSA, DSA, etc.). @@ -24,11 +27,13 @@ mod tests { use crate::utils::{PrivateKey, PrivateKeyAlgorithm, PrivateKeySize}; use insta::assert_json_snapshot; use time::OffsetDateTime; + use uuid::uuid; #[test] fn serialization() -> anyhow::Result<()> { assert_json_snapshot!( PrivateKey { + id: uuid!("00000000-0000-0000-0000-000000000001"), name: "pk-name".to_string(), alg: PrivateKeyAlgorithm::Rsa { key_size: PrivateKeySize::Size2048 }, pkcs8: vec![1, 2, 3], @@ -38,6 +43,7 @@ mod tests { }, @r###" { + "id": "00000000-0000-0000-0000-000000000001", "name": "pk-name", "alg": { "keyType": "rsa", @@ -63,6 +69,7 @@ mod tests { serde_json::from_str::( r#" { + "id": "00000000-0000-0000-0000-000000000001", "name": "pk-name", "alg": { "keyType": "rsa", @@ -79,6 +86,7 @@ mod tests { "# )?, PrivateKey { + id: uuid!("00000000-0000-0000-0000-000000000001"), name: "pk-name".to_string(), alg: PrivateKeyAlgorithm::Rsa { key_size: PrivateKeySize::Size2048 diff --git a/src/utils/certificates/utils_certificates_action.rs b/src/utils/certificates/utils_certificates_action.rs index 8507c63..bb944d0 100644 --- a/src/utils/certificates/utils_certificates_action.rs +++ b/src/utils/certificates/utils_certificates_action.rs @@ -10,6 +10,7 @@ use crate::{ }; use anyhow::bail; use serde::Deserialize; +use uuid::Uuid; #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] @@ -29,18 +30,19 @@ pub enum UtilsCertificatesAction { passphrase: Option, }, #[serde(rename_all = "camelCase")] - ChangePrivateKeyPassphrase { - key_name: String, + UpdatePrivateKey { + key_id: Uuid, + name: Option, passphrase: Option, new_passphrase: Option, }, #[serde(rename_all = "camelCase")] RemovePrivateKey { - key_name: String, + key_id: Uuid, }, #[serde(rename_all = "camelCase")] ExportPrivateKey { - key_name: String, + key_id: Uuid, format: ExportFormat, passphrase: Option, export_passphrase: Option, @@ -83,26 +85,30 @@ impl UtilsCertificatesAction { UtilsCertificatesAction::CreatePrivateKey { key_name: name, .. } => { assert_private_key_name(name)?; } - UtilsCertificatesAction::ChangePrivateKeyPassphrase { - key_name, + UtilsCertificatesAction::UpdatePrivateKey { + name, passphrase, new_passphrase, + key_id, } => { - assert_private_key_name(key_name)?; + let includes_new_passphrase = passphrase.is_some() || new_passphrase.is_some(); + if let Some(name) = name { + assert_private_key_name(name)?; + } else if !includes_new_passphrase { + bail!(SecutilsError::client(format!( + "Either new name or passphrase should be provided ({key_id})." + ))); + } - if passphrase == new_passphrase { + if includes_new_passphrase && passphrase == new_passphrase { bail!(SecutilsError::client(format!( - "New private key passphrase should be different from the current passphrase ({key_name})." + "New private key passphrase should be different from the current passphrase ({key_id})." ))); } } - UtilsCertificatesAction::RemovePrivateKey { key_name } => { - assert_private_key_name(key_name)?; - } - UtilsCertificatesAction::ExportPrivateKey { key_name, .. } => { - assert_private_key_name(key_name)?; - } - UtilsCertificatesAction::GetPrivateKeys => {} + UtilsCertificatesAction::GetPrivateKeys + | UtilsCertificatesAction::RemovePrivateKey { .. } + | UtilsCertificatesAction::ExportPrivateKey { .. } => {} } Ok(()) @@ -145,23 +151,25 @@ impl UtilsCertificatesAction { .create_private_key(user.id, &key_name, alg, passphrase.as_deref()) .await?, )), - UtilsCertificatesAction::ChangePrivateKeyPassphrase { - key_name, + UtilsCertificatesAction::UpdatePrivateKey { + key_id, + name, passphrase, new_passphrase, } => { certificates - .change_private_key_passphrase( + .update_private_key( user.id, - &key_name, + key_id, + name.as_deref(), passphrase.as_deref(), new_passphrase.as_deref(), ) .await?; - Ok(UtilsCertificatesActionResult::ChangePrivateKeyPassphrase) + Ok(UtilsCertificatesActionResult::UpdatePrivateKey) } UtilsCertificatesAction::ExportPrivateKey { - key_name, + key_id, passphrase, export_passphrase, format, @@ -169,15 +177,15 @@ impl UtilsCertificatesAction { certificates .export_private_key( user.id, - &key_name, + key_id, format, passphrase.as_deref(), export_passphrase.as_deref(), ) .await?, )), - UtilsCertificatesAction::RemovePrivateKey { key_name } => { - certificates.remove_private_key(user.id, &key_name).await?; + UtilsCertificatesAction::RemovePrivateKey { key_id } => { + certificates.remove_private_key(user.id, key_id).await?; Ok(UtilsCertificatesActionResult::RemovePrivateKey) } } @@ -190,6 +198,7 @@ mod tests { ExportFormat, PrivateKeyAlgorithm, PrivateKeySize, UtilsCertificatesAction, }; use insta::assert_debug_snapshot; + use uuid::uuid; #[test] fn deserialization() -> anyhow::Result<()> { @@ -275,13 +284,14 @@ mod tests { serde_json::from_str::( r#" { - "type": "changePrivateKeyPassphrase", - "value": { "keyName": "pk", "passphrase": "phrase", "newPassphrase": "phrase_new" } + "type": "updatePrivateKey", + "value": { "keyId": "00000000-0000-0000-0000-000000000001", "passphrase": "phrase", "newPassphrase": "phrase_new" } } "# )?, - UtilsCertificatesAction::ChangePrivateKeyPassphrase { - key_name: "pk".to_string(), + UtilsCertificatesAction::UpdatePrivateKey { + key_id: uuid!("00000000-0000-0000-0000-000000000001"), + name: None, passphrase: Some("phrase".to_string()), new_passphrase: Some("phrase_new".to_string()), } @@ -291,13 +301,48 @@ mod tests { serde_json::from_str::( r#" { - "type": "changePrivateKeyPassphrase", - "value": { "keyName": "pk" } + "type": "updatePrivateKey", + "value": { "keyId": "00000000-0000-0000-0000-000000000001", "name": "pk", "passphrase": "phrase", "newPassphrase": "phrase_new" } } "# )?, - UtilsCertificatesAction::ChangePrivateKeyPassphrase { - key_name: "pk".to_string(), + UtilsCertificatesAction::UpdatePrivateKey { + key_id: uuid!("00000000-0000-0000-0000-000000000001"), + name: Some("pk".to_string()), + passphrase: Some("phrase".to_string()), + new_passphrase: Some("phrase_new".to_string()), + } + ); + + assert_eq!( + serde_json::from_str::( + r#" +{ + "type": "updatePrivateKey", + "value": { "keyId": "00000000-0000-0000-0000-000000000001", "name": "pk" } +} + "# + )?, + UtilsCertificatesAction::UpdatePrivateKey { + key_id: uuid!("00000000-0000-0000-0000-000000000001"), + name: Some("pk".to_string()), + passphrase: None, + new_passphrase: None, + } + ); + + assert_eq!( + serde_json::from_str::( + r#" +{ + "type": "updatePrivateKey", + "value": { "keyId": "00000000-0000-0000-0000-000000000001" } +} + "# + )?, + UtilsCertificatesAction::UpdatePrivateKey { + key_id: uuid!("00000000-0000-0000-0000-000000000001"), + name: None, passphrase: None, new_passphrase: None, } @@ -308,12 +353,12 @@ mod tests { r#" { "type": "removePrivateKey", - "value": { "keyName": "pk" } + "value": { "keyId": "00000000-0000-0000-0000-000000000001" } } "# )?, UtilsCertificatesAction::RemovePrivateKey { - key_name: "pk".to_string(), + key_id: uuid!("00000000-0000-0000-0000-000000000001") } ); @@ -322,12 +367,12 @@ mod tests { r#" { "type": "exportPrivateKey", - "value": { "keyName": "pk", "format": "pem", "passphrase": "phrase", "exportPassphrase": "phrase_new" } + "value": { "keyId": "00000000-0000-0000-0000-000000000001", "format": "pem", "passphrase": "phrase", "exportPassphrase": "phrase_new" } } "# )?, UtilsCertificatesAction::ExportPrivateKey { - key_name: "pk".to_string(), + key_id: uuid!("00000000-0000-0000-0000-000000000001"), format: ExportFormat::Pem, passphrase: Some("phrase".to_string()), export_passphrase: Some("phrase_new".to_string()), @@ -339,12 +384,12 @@ mod tests { r#" { "type": "exportPrivateKey", - "value": { "keyName": "pk", "format": "pem" } + "value": { "keyId": "00000000-0000-0000-0000-000000000001", "format": "pem" } } "# )?, UtilsCertificatesAction::ExportPrivateKey { - key_name: "pk".to_string(), + key_id: uuid!("00000000-0000-0000-0000-000000000001"), format: ExportFormat::Pem, passphrase: None, export_passphrase: None, @@ -393,19 +438,11 @@ mod tests { }, passphrase: Some("phrase".to_string()), }, - UtilsCertificatesAction::ChangePrivateKeyPassphrase { - key_name: key_name.clone(), - passphrase: Some("pass".to_string()), - new_passphrase: Some("pass_new".to_string()), - }, - UtilsCertificatesAction::ExportPrivateKey { - key_name: key_name.clone(), - format: ExportFormat::Pem, + UtilsCertificatesAction::UpdatePrivateKey { + key_id: uuid!("00000000-0000-0000-0000-000000000001"), + name: Some(key_name), passphrase: None, - export_passphrase: None, - }, - UtilsCertificatesAction::RemovePrivateKey { - key_name: key_name.clone(), + new_passphrase: None, }, ] }; @@ -428,28 +465,36 @@ mod tests { ); } - for (passphrase, new_passphrase) in [ - (None, None), - (Some("pass".to_string()), Some("pass".to_string())), - ] { - let change_password_action = UtilsCertificatesAction::ChangePrivateKeyPassphrase { - key_name: "pk".to_string(), - passphrase, - new_passphrase, - }; - assert_eq!( - change_password_action.validate().map_err(|err| err.to_string()), - Err("New private key passphrase should be different from the current passphrase (pk).".to_string()) - ); - } + let change_password_action = UtilsCertificatesAction::UpdatePrivateKey { + key_id: uuid!("00000000-0000-0000-0000-000000000001"), + name: Some("pk".to_string()), + passphrase: Some("pass".to_string()), + new_passphrase: Some("pass".to_string()), + }; + assert_eq!( + change_password_action.validate().map_err(|err| err.to_string()), + Err("New private key passphrase should be different from the current passphrase (00000000-0000-0000-0000-000000000001).".to_string()) + ); + + let change_password_action = UtilsCertificatesAction::UpdatePrivateKey { + key_id: uuid!("00000000-0000-0000-0000-000000000001"), + name: None, + passphrase: None, + new_passphrase: None, + }; + assert_eq!( + change_password_action.validate().map_err(|err| err.to_string()), + Err("Either new name or passphrase should be provided (00000000-0000-0000-0000-000000000001).".to_string()) + ); for (passphrase, new_passphrase) in [ (None, Some("pass".to_string())), (Some("pass".to_string()), Some("pass_new".to_string())), (Some("pass".to_string()), None), ] { - let change_password_action = UtilsCertificatesAction::ChangePrivateKeyPassphrase { - key_name: "pk".to_string(), + let change_password_action = UtilsCertificatesAction::UpdatePrivateKey { + key_id: uuid!("00000000-0000-0000-0000-000000000001"), + name: None, passphrase, new_passphrase, }; diff --git a/src/utils/certificates/utils_certificates_action_result.rs b/src/utils/certificates/utils_certificates_action_result.rs index aadf82c..7ba7301 100644 --- a/src/utils/certificates/utils_certificates_action_result.rs +++ b/src/utils/certificates/utils_certificates_action_result.rs @@ -9,7 +9,7 @@ pub enum UtilsCertificatesActionResult { GenerateSelfSignedCertificate(Vec), GetPrivateKeys(Vec), CreatePrivateKey(PrivateKey), - ChangePrivateKeyPassphrase, + UpdatePrivateKey, RemovePrivateKey, ExportPrivateKey(Vec), } @@ -21,7 +21,7 @@ mod tests { }; use insta::assert_json_snapshot; use time::OffsetDateTime; - + use uuid::uuid; #[test] fn serialization() -> anyhow::Result<()> { assert_json_snapshot!(UtilsCertificatesActionResult::GenerateSelfSignedCertificate (vec![1,2,3]), @r###" @@ -36,6 +36,7 @@ mod tests { "###); assert_json_snapshot!(UtilsCertificatesActionResult::GetPrivateKeys(vec![PrivateKey { + id: uuid!("00000000-0000-0000-0000-000000000001"), name: "pk-name".to_string(), alg: PrivateKeyAlgorithm::Rsa { key_size: PrivateKeySize::Size2048 @@ -48,6 +49,7 @@ mod tests { "type": "getPrivateKeys", "value": [ { + "id": "00000000-0000-0000-0000-000000000001", "name": "pk-name", "alg": { "keyType": "rsa", @@ -62,6 +64,7 @@ mod tests { "###); assert_json_snapshot!(UtilsCertificatesActionResult::CreatePrivateKey(PrivateKey { + id: uuid!("00000000-0000-0000-0000-000000000001"), name: "pk-name".to_string(), alg: PrivateKeyAlgorithm::Rsa { key_size: PrivateKeySize::Size2048 @@ -73,6 +76,7 @@ mod tests { { "type": "createPrivateKey", "value": { + "id": "00000000-0000-0000-0000-000000000001", "name": "pk-name", "alg": { "keyType": "rsa", @@ -89,9 +93,9 @@ mod tests { } "###); - assert_json_snapshot!(UtilsCertificatesActionResult::ChangePrivateKeyPassphrase, @r###" + assert_json_snapshot!(UtilsCertificatesActionResult::UpdatePrivateKey, @r###" { - "type": "changePrivateKeyPassphrase" + "type": "updatePrivateKey" } "###); diff --git a/tools/api/utils/certificates_private_keys.http b/tools/api/utils/certificates_private_keys.http index 0e334bb..ae4bd2e 100644 --- a/tools/api/utils/certificates_private_keys.http +++ b/tools/api/utils/certificates_private_keys.http @@ -9,7 +9,7 @@ Content-Type: application/json "type": "certificates", "value": { "type": "createPrivateKey", - "value": { "name": "pk", "alg": { "keyType": "rsa", "keySize": "1024" } } + "value": { "keyName": "pk", "alg": { "keyType": "rsa", "keySize": "1024" } } } } } @@ -25,7 +25,7 @@ Content-Type: application/json "type": "certificates", "value": { "type": "createPrivateKey", - "value": { "name": "pk-ed25519", "alg": { "keyType": "ed25519" }, "passphrase": "123456" } + "value": { "keyName": "pk-ed25519", "alg": { "keyType": "ed25519" }, "passphrase": "123456" } } } } @@ -43,7 +43,7 @@ Content-Type: application/json "type": "certificates", "value": { "type": "exportPrivateKey", - "value": { "keyName": "pk", "format": "pem" } + "value": { "keyId": "018b4a1b-92d3-739d-a4dd-683d9eb47ce9", "format": "pem", "passphrase": "123456" } } } }