paint-brush
Système de gestion des clés : tout ce que vous devez savoirpar@vivalaakam
270 lectures

Système de gestion des clés : tout ce que vous devez savoir

par Andrey Makarov25m2024/07/16
Read on Terminal Reader

Trop long; Pour lire

Le service stockera une clé privée de 256 bits (utilisée dans la plupart des réseaux blockchain) Il signera les transactions et les messages pour les réseaux EVM (la prise en charge d'autres réseaux peut être effectuée ultérieurement) La clé privée d'un utilisateur ne doit pas quitter notre service. Les clés que nous partageons doivent être uniques. Nous devrions avoir un journal d'activité avec chaque clé.
featured image - Système de gestion des clés : tout ce que vous devez savoir
Andrey Makarov HackerNoon profile picture

Il n'y a pas si longtemps, lors d'un travail moldu, nous avons eu une question sur le stockage des clés privées pour divers services. Au cours du processus, nous avons trouvé plusieurs solutions qui n’étaient pas très adaptées pour différentes raisons. J'ai décidé d'y revenir un jour.

Conditions requises pour le service

Pour ma part, j'ai souligné quelques exigences pour que ce service fonctionne :

  • Pour l'instant, le service doit stocker une clé privée de 256 bits (utilisée dans la plupart des réseaux blockchain)
  • Doit signer des transactions et des messages pour les réseaux EVM (la prise en charge d'autres réseaux peut être effectuée ultérieurement).
  • Un utilisateur peut disposer d'un nombre illimité de clés.
  • La clé privée d'un utilisateur ne doit pas quitter notre service.
  • Chaque clé peut être partagée avec un nombre illimité de personnes.
  • Les clés que nous partageons doivent être uniques.
  • Nous devrions avoir un journal d'activité avec chaque clé.
  • si le service qui utilise le sk Si la clé est compromise, nous devrions pouvoir la révoquer.


Déjà en cours de travail, lorsque j'ai réalisé comment cette tâche devait être résolue, j'ai souligné une exigence supplémentaire :

  • Il faut pouvoir limiter la portée de chaque clé. Cependant, comme le travail était déjà en cours, je l'ai laissé pour le prochain article. Nous nous limiterons donc à signer le message contenu dans cet article.

Options pour résoudre le problème

Puisque la clé sera utilisée plusieurs fois dans l'article, j'appellerai la clé privée pk pour éviter toute confusion et la clé partagée sk .


L'option initiale était de chiffrer la clé dans la fonction pbkdf2 , mais il y avait un problème avec la façon de partager la clé pour accéder à la signature car nous n'avons qu'une seule clé dans le processus de cet algorithme.


J'ai trouvé deux options pour résoudre le problème :

  1. Nous disposons d'une clé principale de la clé cryptée stockée dans la base de données et avons déjà partagé la clé générée, qui mène à la clé d'origine. Je ne dirais pas que j'ai aimé cette option car si vous avez accès à la base de données, la clé pk est facile à déchiffrer.


  2. Nous créons une instance distincte de notre clé pk pour chaque clé que nous vérifions. Je ne dirais pas non plus que j’aime beaucoup cette option.


Ainsi, en me promenant et en réfléchissant à la façon de rendre la clé sk pratique, je me suis souvenu qu'en utilisant Shamir Secrets Sharing (SSS), vous pouvez rendre la clé sk unique et partager seulement une partie de la clé. Le reste sera stocké sur le backend en toute sécurité et vous pourrez donner ces pièces à qui vous voulez.


Cela ressemblerait à ceci : nous chiffrons la clé pk avec notre clé générée par SSS, stockons une partie de la clé dans différents stockages et donnons une partie de la clé à l'utilisateur sous le nom sk . Après environ 10 à 15 minutes, j'ai réalisé une chose simple :


Lorsque nous utilisons SSS, nous n'avons pas besoin de chiffrer notre clé pk avec autre chose car SSS peut la gérer un peu, et cette solution est parfaite pour stocker les clés PK, à mon avis. Il est toujours démonté en plusieurs parties en utilisant différentes options de stockage, y compris celle de l'utilisateur. S'il doit être révoqué, nous supprimons les informations d'index de notre clé sk et en assemblons rapidement une nouvelle.


Dans cet article, je ne m'étendrai pas sur les principes du SSS ; J'ai déjà écrit un court article sur ce sujet et de nombreux principes de cet article constitueront la base de notre nouveau service.

Architecture

Le principe de notre prestation sera le suivant :

  1. L'utilisateur choisit de générer une clé.


  2. Nous créons une clé adaptée au service. Ce sera notre clé pk . Il ne quitte jamais le service dans son ensemble.


  3. En utilisant SSS, nous divisons notre clé de sorte que trois parties de la clé divisée soient nécessaires pour récupérer la clé pk . Chaque clé partagée se compose de deux parties : x : la position de notre clé y : la valeur de cette position


  4. Nous jetons la première partie dans Vault (il peut s'agir de n'importe quel service de stockage d'informations sensibles accessible via l'API).


  5. La deuxième partie que nous enregistrons dans la base de données (je vais utiliser PostgreSQL).


  6. La troisième partie est partiellement enregistrée dans la base de données et l'autre partie est donnée à l'utilisateur ( sk ). Pour utiliser SK afin de trouver la valeur dont nous avons besoin, nous enregistrons également keccak256(sk) dans la base de données. A ma connaissance, il n'est pas encore cassé.


  7. Lorsque l'utilisateur a besoin de signer quelque chose, nous collectons la clé privée de différentes parties de l'application et la signons.


Cette approche présente un inconvénient : si l'administrateur de clé sk perd toutes ses clés sk qu'il a générées, nous ne pouvons pas restaurer la clé d'origine. En option, vous pouvez faire une sauvegarde de la clé originale, mais c'est pour une autre fois =).

Base de données

Grâce à mon travail, j'ai cette structure de base de données :

  • les utilisateurs stockent des informations sur l'utilisateur, alias l'administrateur de clé.


  • keys stocke des informations de base sur la clé, telles que la deuxième partie de notre partage, l'index grâce auquel vous pouvez trouver la première partie du partage dans le coffre-fort, et d'autres informations telles que l'adresse de notre clé privée.


  • share contient une partie du partage et stocke également la valeur hachée de ce partage. Ceci est fait pour que nous puissions le trouver dans la base de données.


  • enregistre toute activité avec la clé, telle que la création de clé et toutes les signatures, tombe ici.

La concrétisation

J'ai utilisé le langage de programmation Rust avec le framework Actix-web pour ce service. Je les utilise tout le temps au travail, alors pourquoi pas ?


Comme je l'ai dit, la base de données sera Postgresql pour les raisons suivantes.

Polynôme

 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, } } }

Je vais faire un petit aveu ici : je ne suis pas mathématicien. Et même si j’essayais de trouver autant d’informations que possible à ce sujet, il s’agit en fait d’un code adapté de mon article précédent.


Vous pouvez en savoir plus sur cette fonctionnalité ici https://en.wikipedia.org/wiki/Lagrange_polynomial


Cette structure (ou classe, selon ce qui est le plus pratique) effectue la partie la plus importante du processus que nous avons décrit aujourd'hui : briser la clé pk en morceaux et la réassembler à nouveau.

Créer un utilisateur

 #[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)); } } }

Ici, tout est le plus simple possible ; nous créons un utilisateur qui dispose d'un passe-partout pour travailler avec ses clés. Ceci est fait pour empêcher toute autre partie de faire quoi que ce soit avec nos clés. Idéalement, cette clé ne devrait en aucun cas être distribuée.

Générer une clé

 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, }) }

Vérifiez auprès de l'utilisateur qu'un tel utilisateur existe, créez une clé pk , divisez-la en parties et enregistrez-les à différents endroits.

Accorder l'accès

 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, }) }

Le mécanisme de fonctionnement de cette fonction est le suivant :


Nous vérifions que le demandeur d'accès dispose de tous les droits sur le Partage.


Nous avons besoin de la clé secrète ici pour une raison très simple, sans elle nous ne pouvons pas récupérer la clé pk originale. Créez un partage supplémentaire et donnez-le à l'utilisateur.

Accès révoqué

 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() }

Ici, il suffit de connaître l'identifiant du partage auquel nous révoquons l'accès. À l’avenir, si je crée une interface Web, elle sera plus facile à utiliser. Nous n'avons pas besoin de notre clé sk ici car nous ne restaurons pas la clé privée ici.

Message de signature

 #[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()), }) }

Reçu le message, si tout allait bien, récupéré la clé privée et signé le message avec.


Fondamentalement, les principales méthodes de notre application sont décrites ; J'ai décidé d'avoir pitié et de ne pas mettre tout le code ici. Il y a GitHub pour ça, et tout le code y sera disponible =)

Conclusion

Bien qu’il s’agisse encore d’une ébauche de l’application, il est important de noter qu’il ne s’agit pas simplement d’un concept. c'est un projet réalisable et prometteur. Il existe également des tests d'intégration dans le référentiel pour comprendre son fonctionnement. Dans les parties suivantes, je prévois d'ajouter la signature des transactions et de permettre de limiter la portée de leur utilisation. Je pourrai ensuite créer une interface Web et rendre ce projet convivial pour le commun des mortels.


Ce développement a un grand potentiel d'utilisation, et j'espère pouvoir en révéler au moins une partie ; Je m'excuse pour ces pages de code et ces commentaires clairsemés. Je préfère écrire du code plutôt que d'expliquer ce que j'ai écrit ci-dessus. Je vais essayer de m'améliorer, mais c'est plus fort maintenant que moi.


Aussi, n'hésitez pas à commenter le code et les relations publiques si vous le souhaitez =)


Meilleurs vœux à tous et gardez vos clés privées en sécurité.