paint-brush
密钥管理系统:您需要了解的一切by@vivalaakam
231

密钥管理系统:您需要了解的一切

Andrey Makarov25m2024/07/16
Read on Terminal Reader

该服务将存储一个 256 位私钥(用于大多数区块链网络)它将为 EVM 网络签署交易和消息(以后可以支持其他网络)用户的私钥不得离开我们的服务。我们共享的密钥必须是唯一的。我们应该对每个密钥都有活动日志。
featured image - 密钥管理系统:您需要了解的一切
Andrey Makarov HackerNoon profile picture

不久前,在一次麻瓜工作中,我们遇到了一个关于为各种服务存储私钥的问题。在此过程中,我们发现了几种由于不同原因而不太合适的解决方案。我决定找个时间再回过头来解决这个问题。

服务要求

就我个人而言,我强调了这项服务正常运行的一些要求:

  • 目前,该服务必须存储 256 位私钥(大多数区块链网络使用)
  • 应该为 EVM 网络签署交易和消息(以后可以支持其他网络)。
  • 一个用户可以拥有无限数量的密钥。
  • 用户的私钥不得离开我们的服务。
  • 每个钥匙可与无限数量的人共享。
  • 我们共享的密钥必须是唯一的。
  • 我们应该记录每一个键的活动。
  • 如果使用sk服务密钥被泄露,我们应该能够撤销它。


在工作过程中,当我意识到应该如何解决这个任务时,我又强调了一个要求:

  • 我们必须能够限制每个密钥的范围。但是,由于这项工作已经在进行中,我将其留到下一篇文章中。因此,我们将限制自己在这篇文章中对消息进行签名。

解决问题的选项

由于密钥将在文章中多次使用,因此为了避免混淆,我将私钥称为pk ,将共享的密钥称为sk


最初的选择是在pbkdf2函数中加密密钥,但是由于在这个算法的过程中我们只有一个密钥,所以存在如何共享密钥来访问签名的问题。


我找到了两种解决问题的方法:

  1. 我们有存储在数据库中的加密密钥的主密钥,并且已经共享了生成的密钥,从而可以获取原始密钥。我不会说我喜欢这个选项,因为如果您可以访问数据库,则pk密钥很容易解密。


  2. 我们为检查的每个密钥创建单独的pk密钥实例。我也不怎么喜欢这个选项。


因此,四处走走并思考如何使 sk 密钥变得方便时,我想到使用 Shamir Secrets Sharing (SSS) 时,您可以使sk密钥独一无二,并且仅共享密钥的一部分。其余部分将安全地存储在后端,您可以将这些部分提供给您想要的任何人。


它看起来像这样:我们用 SSS 生成的密钥加密 pk 密钥,将密钥的一部分存储在不同的存储中,并将密钥的一部分作为sk提供给用户。大约 10-15 分钟后,我意识到了一件简单的事情:


使用 SSS 时,我们不需要用其他任何东西加密我们的pk密钥,因为 SSS 可以处理一些问题,而且我认为这种解决方案非常适合存储 PK 密钥。它总是使用不同的存储选项(包括用户的存储选项)分解成几部分。如果需要撤销,我们会删除sk密钥的索引信息并快速组装一个新的。


在本文中,我不会详细讨论 SSS 的原则;我已经就这个主题写了一篇短文,本文中的许多原则将构成我们新服务的基础。

建筑学

我们的服务原则如下:

  1. 用户选择生成密钥。


  2. 我们为服务创建一个合适的密钥。这将是我们的pk密钥。它永远不会离开整个服务。


  3. 使用 SSS,我们拆分密钥,因此需要拆分密钥的三个部分来恢复pk密钥。每个拆分密钥由两部分组成:x:密钥的位置 y:此位置的值


  4. 我们将第一部分放入 Vault(它可以是任何可通过 API 访问的存储敏感信息的服务)。


  5. 第二部分我们保存到数据库(我将使用 PostgreSQL)。


  6. 第三部分我们部分保存到数据库,另一部分我们提供给用户( sk )。为了使用 SK 找到我们需要的值,我们还将keccak256(sk)保存到数据库中。据我所知,它还没有被破解。


  7. 当用户需要签署某些内容时,我们会从应用程序的不同部分收集私钥并对其进行签署。


这种方法有一个缺点,如果sk密钥管理员丢失了他生成的所有sk密钥,我们就无法恢复原始密钥。您可以选择备份原始密钥,但那是另一回事了 =)。

数据库

由于我的工作,我有这样的数据库结构:

  • 用户存储有关用户(又名密钥管理员)的信息。


  • keys存储有关密钥的基本信息,例如我们的共享的第二部分、在 Vault 中可以找到共享的第一部分的索引,以及其他信息,例如我们的私钥的地址。


  • shares包含 Share 的一部分,还存储了此 Share 的哈希值。这样做是为了我们可以在数据库中找到它。


  • 记录与密钥相关的任何活动,例如密钥创建和所有签名,都在这里。

实现

我使用 Rust 编程语言和 Actix-web 框架来提供这项服务。我在工作中一直使用它们,所以为什么不呢?


正如我所说,由于以下原因,数据库将采用 Postgresql。

多项式

lazy_static! { static ref PRIME: BigUint = BigUint::from_str_radix( "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 16 ) .expect("N parse error"); } #[derive(Clone, Debug)] pub struct Share { pub x: BigUint, pub y: BigUint, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ShareStore { pub x: String, pub y: String, } impl From<Share> for ShareStore { fn from(share: Share) -> Self { ShareStore { x: hex::encode(share.x.to_bytes_be()), y: hex::encode(share.y.to_bytes_be()), } } } impl From<&Share> for ShareStore { fn from(share: &Share) -> Self { ShareStore { x: hex::encode(share.x.to_bytes_be()), y: hex::encode(share.y.to_bytes_be()), } } } pub struct Polynomial { prime: BigUint, } impl Polynomial { pub(crate) fn new() -> Self { Polynomial { prime: PRIME.clone(), } } // Calculates the modular multiplicative inverse of `a` modulo `m` using Fermat's Little Theorem. fn mod_inverse(&self, a: &BigUint, m: &BigUint) -> BigUint { a.modpow(&(m - 2u32), m) } // Generates a random polynomial of a given degree with the secret as the constant term. fn random_polynomial(&self, degree: usize, secret: &BigUint) -> Vec<BigUint> { let mut coefficients = vec![secret.clone()]; for _ in 0..degree { let index = BigUint::from_bytes_be(generate_random().as_slice()); coefficients.push(index); } coefficients } // Evaluates a polynomial at a given point `x`, using Horner's method for efficient computation under a prime modulus. fn evaluate_polynomial(&self, coefficients: &[BigUint], x: &BigUint) -> BigUint { let mut result = BigUint::zero(); let mut power = BigUint::one(); for coeff in coefficients { result = (&result + (coeff * &power) % &self.prime) % &self.prime; power = (&power * x) % &self.prime; } result } // Generates `num_shares` shares from a secret, using a polynomial of degree `threshold - 1`. pub fn generate_shares( &self, secret: &BigUint, num_shares: usize, threshold: usize, ) -> Vec<Share> { let coefficients = self.random_polynomial(threshold - 1, secret); let mut shares = vec![]; for _x in 1..=num_shares { let x = BigUint::from_bytes_be(generate_random().as_slice()); let y = self.evaluate_polynomial(&coefficients, &x); shares.push(Share { x, y }); } shares } // Reconstructs the secret from a subset of shares using Lagrange interpolation in a finite field. pub fn reconstruct_secret(&self, shares: &Vec<Share>) -> BigUint { let mut secret = BigUint::zero(); for share_i in shares { let mut numerator = BigUint::one(); let mut denominator = BigUint::one(); for share_j in shares { if share_i.x != share_j.x { numerator = (&numerator * &share_j.x) % &self.prime; let diff = if share_j.x > share_i.x { &share_j.x - &share_i.x } else { &self.prime - (&share_i.x - &share_j.x) }; denominator = (&denominator * &diff) % &self.prime; } } let lagrange = (&share_i.y * &numerator * self.mod_inverse(&denominator, &self.prime)) % &self.prime; secret = (&secret + &lagrange) % &self.prime; } secret } // Adds a new share to the existing set of shares using Lagrange interpolation in a finite field. pub fn add_share(&self, shares: &Vec<Share>) -> Share { let new_index = BigUint::from_bytes_be(generate_random().as_slice()); let mut result = BigUint::zero(); for share_i in shares { let mut lambda = BigUint::one(); for share_j in shares { if share_i.x != share_j.x { let numerator = if new_index.clone() >= share_j.x { (new_index.clone() - &share_j.x) % &self.prime } else { (&self.prime - (&share_j.x - new_index.clone()) % &self.prime) % &self.prime }; let denominator = if share_i.x >= share_j.x { (&share_i.x - &share_j.x) % &self.prime } else { (&self.prime - (&share_j.x - &share_i.x) % &self.prime) % &self.prime }; lambda = (&lambda * &numerator * self.mod_inverse(&denominator, &self.prime)) % &self.prime; } } result = (&result + &share_i.y * &lambda) % &self.prime; } Share { x: new_index, y: result, } } }

我在这里要坦白一点:我不是数学家。虽然我试图找到尽可能多的相关信息,但事实上,这是我之前文章中改编的代码。


您可以在此处阅读有关此功能的更多信息https://en.wikipedia.org/wiki/Lagrange_polynomial


这个结构(或类,以更方便的为准)执行我们今天描述的流程中最重要的部分 - 将pk密钥分解成碎片并重新组装。

创建用户

#[derive(Serialize, Deserialize)] pub struct CreateUserResponse { pub secret: String, } pub async fn users_create_handler(app_data: web::Data<AppData>) -> HttpResponse { let code = generate_code(); match create_user( CreateOrUpdateUser { secret: code.clone(), }, app_data.get_db_connection(), ) .await { Ok(_) => HttpResponse::Ok().json(CreateUserResponse { secret: code }), Err(e) => { return HttpResponse::InternalServerError().body(format!("Error creating user: {}", e)); } } }

在这里,一切都尽可能简单;我们创建一个拥有主密钥的用户来使用他的密钥。这样做是为了防止任何其他方使用我们的密钥做任何事情。理想情况下,此密钥不应以任何方式分发。

生成密钥

pub async fn keys_generate_handler(req: HttpRequest, app_data: web::Data<AppData>) -> HttpResponse { // Check if the request has a master key header let Some(Ok(master_key)) = req.headers().get(MASTER_KEY).map(|header| header.to_str()) else { return HttpResponse::Unauthorized().finish(); }; // Check if user with master key exist let user = match get_user_by_secret(&master_key, app_data.get_db_connection()).await { Ok(user) => user, Err(UserErrors::NotFound(_)) => { return HttpResponse::Unauthorized().finish(); } Err(e) => { return HttpResponse::InternalServerError().body(format!("Error getting user: {}", e)); } }; // generate random `pk` private key let private_key = generate_random(); let Ok(signer) = PrivateKeySigner::from_slice(private_key.as_slice()) else { return HttpResponse::InternalServerError().finish(); }; let secret = BigUint::from_bytes_be(private_key.as_slice()); let poly = Polynomial::new(); // divide `pk` key into 3 shares let shares = poly .generate_shares(&secret, 3, 3) .iter() .map(Into::into) .collect::<Vec<ShareStore>>(); // store first part at Vault let path = generate_code(); if let Err(err) = kv2::set( app_data.get_vault_client().as_ref(), "secret", &path, &shares[0], ) .await { return HttpResponse::InternalServerError().body(format!("Error setting secret: {}", err)); } // Store second part at database and path to first share let key = CreateOrUpdateKey { user_id: user.id, local_key: shares[1].y.clone(), local_index: shares[1].y.clone(), cloud_key: path, address: signer.address(), }; let key = match create_key(key, app_data.get_db_connection()).await { Ok(key) => key, Err(err) => { return HttpResponse::InternalServerError() .body(format!("Error creating key: {}", err)); } }; // Store third part at database as share let share = match create_share( CreateOrUpdateShare { secret: shares[2].y.clone(), key_id: key.id, user_index: shares[2].x.clone(), owner: SharesOwner::Admin, }, app_data.get_db_connection(), ) .await { Ok(share) => share, Err(err) => { return HttpResponse::InternalServerError() .body(format!("Error creating share: {}", err)); } }; let Ok(user_key) = hex::decode(&shares[2].y) else { return HttpResponse::InternalServerError().finish(); }; // Store log let _ = create_log( CreateLog { key_id: key.id, action: "generate_key".to_string(), data: serde_json::json!({ "user_id": user.id }), message: None, }, app_data.get_db_connection(), ) .await; // Return the key and share identifier HttpResponse::Ok().json(KeysGenerateResponse { key: STANDARD.encode(user_key), id: share.id, }) }

检查用户是否存在这样的用户,创建一个pk密钥,将其分成几部分,并将它们保存在不同的地方。

授予访问权限

pub async fn keys_grant_handler(req: HttpRequest, app_data: web::Data<AppData>) -> HttpResponse { // Check if the request has a master key header let Some(Ok(master_key)) = req.headers().get(MASTER_KEY).map(|header| header.to_str()) else { return HttpResponse::Unauthorized().finish(); }; // Check if a user with the master key exists let user = match get_user_by_secret(&master_key, app_data.get_db_connection()).await { Ok(user) => user, Err(UserErrors::NotFound(_)) => { return HttpResponse::Unauthorized().finish(); } Err(e) => { return HttpResponse::InternalServerError().body(format!("Error getting user: {}", e)); } }; // Check if the request has a secret key header let Some(Ok(secret_key)) = req.headers().get(SECRET_KEY).map(|header| header.to_str()) else { return HttpResponse::Unauthorized().finish(); }; let Ok(share) = STANDARD.decode(secret_key) else { return HttpResponse::Unauthorized().finish(); }; // Check if the share exists let share_value = hex::encode(share); let share = match get_share_by_secret(&share_value, app_data.get_db_connection()).await { Ok(share) => share, Err(ShareErrors::NotFound(_)) => return HttpResponse::NotFound().finish(), Err(_) => { return HttpResponse::Unauthorized().finish(); } }; if !matches!(share.status, SharesStatus::Granted) { return HttpResponse::Unauthorized().finish(); } // Get original key with necessary information let key = match get_key_by_id(&share.key_id, app_data.get_db_connection()).await { Ok(key) => key, Err(KeyErrors::NotFound(_)) => return HttpResponse::NotFound().finish(), Err(_) => { return HttpResponse::Unauthorized().finish(); } }; // Check if the key belongs to the user if key.user_id != user.id { return HttpResponse::Unauthorized().finish(); } // Get the first part of the key from Vault let Ok(cloud_secret) = kv2::read::<ShareStore>( app_data.get_vault_client().as_ref(), "secret", &key.cloud_key, ) .await else { return HttpResponse::InternalServerError().finish(); }; // Combine the shares let shares = vec![ Share { x: BigUint::from_str_radix(&cloud_secret.x, 16).expect("Error parsing local index"), y: BigUint::from_str_radix(&cloud_secret.y, 16).expect("Error parsing local key"), }, Share { x: BigUint::from_str_radix(&key.local_index, 16).expect("Error parsing local index"), y: BigUint::from_str_radix(&key.local_key, 16).expect("Error parsing local key"), }, Share { x: BigUint::from_str_radix(&share.user_index, 16).expect("Error parsing user index"), y: BigUint::from_str_radix(&share_value, 16).expect("Error parsing user key"), }, ]; let sss = Polynomial::new(); // Create a new share let new_share = ShareStore::from(sss.add_share(&shares)); // Store new share into database let share = match create_share( CreateOrUpdateShare { secret: new_share.y.to_string(), key_id: key.id, user_index: new_share.x.to_string(), owner: SharesOwner::Guest, }, app_data.get_db_connection(), ) .await { Ok(share) => share, Err(err) => { return HttpResponse::InternalServerError() .body(format!("Error creating share: {}", err)); } }; let Ok(user_key) = hex::decode(&new_share.y).map(|k| STANDARD.encode(k)) else { return HttpResponse::InternalServerError().finish(); }; // Store log let _ = create_log( CreateLog { key_id: key.id, action: "grant".to_string(), data: serde_json::json!({ "user_id": user.id, "share_id": share.id, }), message: None, }, app_data.get_db_connection(), ) .await; // Return the key and share the identifier HttpResponse::Ok().json(KeysGenerateResponse { key: user_key, id: share.id, }) }

该函数的运行机制如下:


我们验证访问请求者是否拥有该共享的所有权利。


我们在这里需要密钥的原因很简单,没有它我们就无法恢复原始pk密钥。创建一个额外的共享,并将其提供给用户。

撤销访问权限

pub async fn keys_revoke_handler( req: HttpRequest, app_data: web::Data<AppData>, body: web::Json<KeysRevokeRequest>, ) -> HttpResponse { let Some(Ok(master_key)) = req.headers().get(MASTER_KEY).map(|header| header.to_str()) else { return HttpResponse::Unauthorized().finish(); }; let user = match get_user_by_secret(&master_key, app_data.get_db_connection()).await { Ok(user) => user, Err(UserErrors::NotFound(_)) => { return HttpResponse::Unauthorized().finish(); } Err(e) => { return HttpResponse::InternalServerError().body(format!("Error getting user: {}", e)); } }; let share = match get_share_by_id(&body.id, app_data.get_db_connection()).await { Ok(share) => share, Err(ShareErrors::NotFound(_)) => return HttpResponse::NotFound().finish(), Err(_) => { return HttpResponse::Unauthorized().finish(); } }; let key = match get_key_by_id(&share.key_id, app_data.get_db_connection()).await { Ok(key) => key, Err(KeyErrors::NotFound(_)) => return HttpResponse::NotFound().finish(), Err(_) => { return HttpResponse::Unauthorized().finish(); } }; if key.user_id != user.id { return HttpResponse::Unauthorized().finish(); } if revoke_share_by_id(&share.id, app_data.get_db_connection()) .await .is_err() { return HttpResponse::InternalServerError().finish(); } let _ = create_log( CreateLog { key_id: key.id, action: "revoke".to_string(), data: serde_json::json!({ "user_id": user.id, "share_id": share.id, }), message: None, }, app_data.get_db_connection(), ) .await; HttpResponse::Ok().finish() }

在这里,我们只需要知道要撤销访问权限的共享的标识符。将来,如果我制作了一个 Web 界面,这将更容易使用。我们不需要我们的sk密钥,因为我们不会在这里恢复私钥。

签署讯息

#[derive(Deserialize, Serialize, Debug)] pub struct SignMessageRequest { pub message: String, } #[derive(Deserialize, Serialize, Debug)] pub struct SignMessageResponse { pub signature: String, } pub async fn sign_message_handler( app_data: web::Data<AppData>, req: HttpRequest, body: web::Json<SignMessageRequest>, ) -> HttpResponse { // Get the `sk` key from the request headers let Some(Ok(secret_key)) = req.headers().get(SECRET_KEY).map(|header| header.to_str()) else { return HttpResponse::Unauthorized().finish(); }; // restore shares let (shares, key_id, share_id) = match restore_shares(secret_key, &app_data).await { Ok(shares) => shares, Err(e) => { return HttpResponse::BadRequest().json(json!({"error": e.to_string()})); } }; let sss = Polynomial::new(); // restore `pk` key let private_key = sss.reconstruct_secret(&shares); //sign message let Ok(signer) = PrivateKeySigner::from_slice(private_key.to_bytes_be().as_slice()) else { return HttpResponse::InternalServerError().finish(); }; let Ok(signature) = signer.sign_message(body.message.as_bytes()).await else { return HttpResponse::InternalServerError().finish(); }; // create log let _ = create_log( CreateLog { key_id, action: "sign_message".to_string(), data: json!({ "share_id": share_id, }), message: Some(body.message.clone()), }, app_data.get_db_connection(), ) .await; // return signature HttpResponse::Ok().json(SignMessageResponse { signature: hex::encode(signature.as_bytes()), }) }

收到消息,如果一切正常,则恢复私钥,并用它签署消息。


基本上,我们应用程序的主要方法已经描述完毕;出于同情,我决定不把整个代码放在这里。GitHub 上有这个,所有代码都可以在那里找到 =)

结论

虽然这仍然是应用程序的草案,但需要注意的是,这不仅仅是一个概念。这是一个可行的草案,显示出希望。存储库中还有集成测试,以了解其工作原理。在接下来的部分中,我计划添加交易签名,并使其使用范围受到限制。然后我可能会制作一个 Web 界面,让这个项目对普通人来说很友好。


这项开发具有相当大的使用潜力,我希望至少能透露其中的一部分;我为这些代码页和稀疏的评论道歉。我更喜欢编写代码,而不是解释我上面写的内容。我会努力变得更好,但现在这比我更强了。


此外,如果需要,欢迎对代码和 PR 进行评论 =)


祝大家一切顺利,并保管好你们的私钥。