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使用するサービスでキーが侵害された場合、そのキーを取り消すことができる必要があります。


すでに作業の途中で、このタスクをどのように解決すべきかがわかったとき、私はもう 1 つの要件を強調しました。

  • 各キーのスコープを制限できる必要がありますが、作業はすでに進行中であったため、次の記事に残しました。したがって、この記事ではメッセージに署名することに限定します。

問題を解決するための選択肢

このキーは記事の中で何度も使用されるため、混乱を避けるために秘密キーをpk 、共有キーをskと呼ぶことにします。


当初のオプションは、 pbkdf2関数でキーを暗号化することでしたが、このアルゴリズムのプロセスではキーが 1 つしかないため、署名にアクセスするためにキーを共有する方法に問題がありました。


この問題を解決するには 2 つの選択肢が見つかりました。

  1. 暗号化されたキーのマスター キーがデータベースに保存されており、生成されたキーはすでに共有されているため、元のキーにつながります。データベースにアクセスできれば、 pkキーは簡単に復号化できるため、このオプションは好きではないと思います。


  2. チェックするキーごとに、 pkキーの個別のインスタンスを作成します。このオプションもあまり好きとは言えません。


そこで、sk キーを便利にするにはどうしたらよいかといろいろ考えていたところ、Shamir Secrets Sharing (SSS) を使用すると、 skキーを一意にして、キーの一部だけを共有できることを思い出しました。残りの部分はバックエンドで安全に保管され、これらの部分を好きな人に渡すことができます。


それは次のようになります: pk キーを SSS で生成されたキーで暗号化し、キーの一部を別のストレージに保存し、キーの一部をskとしてユーザーに渡します。約 10 〜 15 分後、私は 1 つの単純なことに気付きました:


SSS を使用する場合、SSS はそれを少し処理できるため、他の方法でpkキーを暗号化する必要はありません。このソリューションは、私の意見では PK キーの保存に最適です。ユーザーのものも含め、さまざまなストレージ オプションを使用して、常にパーツに分解されます。取り消す必要がある場合は、 skキーのインデックス情報を削除し、新しいキーをすばやく組み立てます。


この記事では、SSS の原則については詳しく説明しません。このトピックについてはすでに短い記事を書いており、この記事の多くの原則が私たちの新しいサービスの基礎となるでしょう。

建築

当社のサービスの原則は次のとおりです。

  1. ユーザーはキーを生成することを選択します。


  2. サービスに適したキーを作成します。これがpkキーになります。サービス全体から外に出ることはありません。


  3. SSSを使用してキーを分割し、 pkキーを復元するには分割キーの3つの部分が必要になります。各分割キーは2つの部分で構成されます。x: キーの位置 y: この位置の値


  4. 最初の部分を Vault に投入します (これは、API 経由でアクセスできる機密情報を保存するための任意のサービスになります)。


  5. 2 番目の部分はデータベースに保存します (PostgreSQL を使用します)。


  6. 3 番目の部分は部分的にデータベースに保存し、残りの部分はユーザーに渡します ( sk )。SK を使用して必要な値を見つけるために、 keccak256(sk)もデータベースに保存します。私の知る限り、まだ破られていません。


  7. ユーザーが何かに署名する必要がある場合、アプリケーションのさまざまな部分から秘密鍵を収集して署名します。


このアプローチには欠点が 1 つあります。sk キー管理者sk自分で生成したすべてのskキーを紛失した場合、元のキーを復元できません。オプションとして、元のキーのバックアップを作成することもできますが、それはまた別の機会に説明します =)。

データベース

私の作業の結果、次のようなデータベース構造ができました。

  • users は、ユーザー (つまりキー管理者) に関する情報を保存します。


  • keys には、共有の 2 番目の部分、Vault 内で共有の最初の部分を見つけるためのインデックス、秘密キーのアドレスなどのキーに関する基本情報が保存されます。


  • shares にはShare の一部が含まれており、この Share のハッシュ値も保存されます。これは、データベース内で見つけられるようにするためです。


  • キーの作成やすべての署名など、キーを使用したすべてのアクティビティがここに記録されます。

実現

このサービスでは、Actix-web フレームワークで Rust プログラミング言語を使用しました。仕事でいつも使っているのに、なぜ使わないのでしょうか?


前述したように、データベースは、次の理由により 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 に関するコメントも歓迎します =)


皆様のご多幸をお祈り申し上げます。秘密鍵を安全に保管してください。