0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

License System Day 17: 鍵管理とセキュリティ

Last updated at Posted at 2025-12-16

🎄 科学と神々株式会社 アドベントカレンダー 2025

License System Day 17: 鍵管理とセキュリティ


📖 今日のテーマ

今日は、暗号鍵の安全な管理を学びます。

ECDSA P-256秘密鍵・公開鍵の生成、保存、ローテーション、そしてセキュリティベストプラクティスを解説します。


🎯 鍵管理の重要性

鍵漏洩のリスク

秘密鍵が漏洩すると...

❌ すべてのライセンスが偽造可能に
❌ ユーザーデータが不正アクセスされる
❌ サービス全体の信頼性が崩壊
❌ 法的責任問題に発展

→ 鍵管理は最重要のセキュリティ課題!

セキュリティ原則

✅ 最小権限の原則 (Principle of Least Privilege)
✅ 深層防御 (Defense in Depth)
✅ 秘密の分離 (Separation of Secrets)
✅ 定期ローテーション (Regular Rotation)
✅ 監査ログ (Audit Logging)

🔐 ECDSA P-256鍵ペア生成

OpenSSLによる生成

#!/bin/bash
# generate_keys.sh

# 鍵ディレクトリ作成
mkdir -p keys
chmod 700 keys  # 所有者のみアクセス可能

# ECDSA P-256秘密鍵生成
openssl ecparam -name prime256v1 -genkey -noout -out keys/private_key.pem

# 秘密鍵のパーミッション設定(所有者のみ読み取り可能)
chmod 400 keys/private_key.pem

# 公開鍵の抽出
openssl ec -in keys/private_key.pem -pubout -out keys/public_key.pem

# 公開鍵は読み取り可能でOK
chmod 644 keys/public_key.pem

echo "✅ ECDSA P-256 key pair generated successfully"
echo "   Private key: keys/private_key.pem (chmod 400)"
echo "   Public key:  keys/public_key.pem (chmod 644)"

鍵の検証

# 秘密鍵の検証
openssl ec -in keys/private_key.pem -text -noout

# 出力例:
# read EC key
# Private-Key: (256 bit)
# priv:
#     00:c9:0e:f7:3f:4c:7e:...
# pub:
#     04:b7:63:5c:0f:...
# ASN1 OID: prime256v1
# NIST CURVE: P-256

# 公開鍵の検証
openssl ec -in keys/public_key.pem -pubin -text -noout

# 出力例:
# read EC key
# Public-Key: (256 bit)
# pub:
#     04:b7:63:5c:0f:...
# ASN1 OID: prime256v1
# NIST CURVE: P-256

🗃️ 鍵の保存戦略

開発環境(ローカル)

# .gitignore に追加(必須!)
keys/
*.pem
*.key
.env
.env.local

# 環境変数ファイル(.env)
PRIVATE_KEY_PATH=./keys/private_key.pem
PUBLIC_KEY_PATH=./keys/public_key.pem
JWT_SECRET=your-super-secret-jwt-key-change-me

Nimでの読み込み:

# server/src/crypto.nim
import std/os

proc loadPrivateKey*(): string =
  let keyPath = getEnv("PRIVATE_KEY_PATH", "../keys/private_key.pem")

  if not fileExists(keyPath):
    raise newException(IOError, "Private key not found: " & keyPath)

  result = readFile(keyPath)

proc loadPublicKey*(): string =
  let keyPath = getEnv("PUBLIC_KEY_PATH", "../keys/public_key.pem")

  if not fileExists(keyPath):
    raise newException(IOError, "Public key not found: " & keyPath)

  result = readFile(keyPath)

本番環境(プロダクション)

1. 環境変数での管理

# サーバー起動時に設定
export PRIVATE_KEY="-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIAoN9z9MfrxvCqRK...
-----END EC PRIVATE KEY-----"

export PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZI...
-----END PUBLIC KEY-----"

export JWT_SECRET="production-super-secret-key-256-bits"

Nimでの読み込み:

proc loadPrivateKeyFromEnv*(): string =
  result = getEnv("PRIVATE_KEY")
  if result == "":
    raise newException(ValueError, "PRIVATE_KEY environment variable not set")

proc loadPublicKeyFromEnv*(): string =
  result = getEnv("PUBLIC_KEY")
  if result == "":
    raise newException(ValueError, "PUBLIC_KEY environment variable not set")

2. AWS Secrets Manager

# Nim + AWS SDK
import std/[asyncdispatch, json]
import aws_sdk/secretsmanager

proc loadPrivateKeyFromAWS*(secretName: string): Future[string] {.async.} =
  let client = newSecretsManagerClient(region = "us-east-1")

  try:
    let response = await client.getSecretValue(secretName)
    let secret = parseJson(response.secretString)
    result = secret["private_key"].getStr()
  except Exception as e:
    raise newException(IOError, "Failed to load private key from AWS: " & e.msg)

3. HashiCorp Vault

import std/[httpclient, json, asyncdispatch]

proc loadPrivateKeyFromVault*(vaultAddr, token, path: string): Future[string] {.async.} =
  let client = newAsyncHttpClient()
  client.headers = newHttpHeaders({
    "X-Vault-Token": token
  })

  try:
    let response = await client.getContent(vaultAddr & "/v1/" & path)
    let data = parseJson(response)
    result = data["data"]["data"]["private_key"].getStr()
  except Exception as e:
    raise newException(IOError, "Failed to load key from Vault: " & e.msg)

4. Google Cloud Secret Manager

import std/[asyncdispatch, json]
import google_cloud/secretmanager

proc loadPrivateKeyFromGCP*(projectId, secretId: string): Future[string] {.async.} =
  let client = newSecretManagerClient()

  try:
    let name = "projects/" & projectId & "/secrets/" & secretId & "/versions/latest"
    let response = await client.accessSecretVersion(name)
    result = response.payload.data.toString()
  except Exception as e:
    raise newException(IOError, "Failed to load key from GCP: " & e.msg)

🔄 鍵のローテーション

なぜローテーションが必要か?

理由:
1. 漏洩リスクの最小化
   → 万が一漏洩しても被害期間を限定

2. コンプライアンス要件
   → SOC2, ISO27001などで90日ローテーション推奨

3. 暗号学的強度の維持
   → 長期使用による解析リスクを軽減

ローテーション戦略

# server/src/key_rotation.nim
import std/[times, os, strutils]

type
  KeyRotationConfig* = object
    rotationIntervalDays*: int
    keyDirectory*: string
    archiveDirectory*: string

proc shouldRotate*(config: KeyRotationConfig): bool =
  ## 鍵ローテーションが必要かチェック

  let privateKeyPath = config.keyDirectory / "private_key.pem"

  if not fileExists(privateKeyPath):
    return true  # 鍵が存在しない場合は生成

  # ファイルの最終更新日時を取得
  let modTime = getLastModificationTime(privateKeyPath)
  let now = now()
  let daysSinceModification = (now - modTime).inDays

  return daysSinceModification >= config.rotationIntervalDays

proc rotateKeys*(config: KeyRotationConfig): bool =
  ## 鍵ローテーション実行

  try:
    let now = now()
    let timestamp = now.format("yyyyMMdd_HHmmss")

    # 古い鍵をアーカイブ
    if fileExists(config.keyDirectory / "private_key.pem"):
      let archiveName = "private_key_" & timestamp & ".pem"
      moveFile(
        config.keyDirectory / "private_key.pem",
        config.archiveDirectory / archiveName
      )

    if fileExists(config.keyDirectory / "public_key.pem"):
      let archiveName = "public_key_" & timestamp & ".pem"
      moveFile(
        config.keyDirectory / "public_key.pem",
        config.archiveDirectory / archiveName
      )

    # 新しい鍵ペアを生成
    let result = execShellCmd("""
      openssl ecparam -name prime256v1 -genkey -noout -out """ & config.keyDirectory & """/private_key.pem &&
      chmod 400 """ & config.keyDirectory & """/private_key.pem &&
      openssl ec -in """ & config.keyDirectory & """/private_key.pem -pubout -out """ & config.keyDirectory & """/public_key.pem &&
      chmod 644 """ & config.keyDirectory & """/public_key.pem
    """)

    if result != 0:
      raise newException(OSError, "Failed to generate new key pair")

    echo "✅ Key rotation completed successfully"
    return true

  except Exception as e:
    echo "❌ Key rotation failed: ", e.msg
    return false

# 使用例
let config = KeyRotationConfig(
  rotationIntervalDays: 90,  # 90日ごとにローテーション
  keyDirectory: "./keys",
  archiveDirectory: "./keys/archive"
)

if shouldRotate(config):
  discard rotateKeys(config)

🛡️ セキュリティベストプラクティス

1. パーミッション設定

# サーバー上での適切な権限設定
chown root:app-group /path/to/keys/
chmod 750 /path/to/keys/

chown root:app-group /path/to/keys/private_key.pem
chmod 440 /path/to/keys/private_key.pem  # root + app-groupのみ読み取り可

chown root:app-group /path/to/keys/public_key.pem
chmod 644 /path/to/keys/public_key.pem  # 全員読み取り可(公開鍵)

2. 暗号化保存

# 秘密鍵を暗号化して保存
import nimcrypto

proc encryptPrivateKey*(plainKey, masterPassword: string): string =
  ## マスターパスワードでAES-256暗号化

  var ctx: CTR[aes256]
  var key: array[32, byte]  # AES-256 key
  var iv: array[16, byte]   # Initialization Vector

  # パスワードからキー導出(PBKDF2)
  let salt = "license-system-salt-change-me"
  discard pbkdf2(sha256, masterPassword, salt, 100000, key)

  # IVを生成
  randomBytes(iv)

  # 暗号化
  ctx.init(key, iv)
  let encrypted = ctx.encrypt(plainKey.toOpenArrayByte(0, plainKey.len - 1))

  # IV + 暗号文をBase64エンコード
  result = encode(iv & encrypted)

proc decryptPrivateKey*(encryptedKey, masterPassword: string): string =
  ## 暗号化された秘密鍵を復号化

  let decoded = decode(encryptedKey)
  let iv = decoded[0..<16]
  let encrypted = decoded[16..^1]

  var ctx: CTR[aes256]
  var key: array[32, byte]

  let salt = "license-system-salt-change-me"
  discard pbkdf2(sha256, masterPassword, salt, 100000, key)

  ctx.init(key, iv)
  let decrypted = ctx.decrypt(encrypted)

  result = decrypted.toString()

3. アクセスログ

# 秘密鍵アクセスを監査ログに記録
import std/logging

var logger = newConsoleLogger()

proc loadPrivateKeyWithAudit*(keyPath: string, userId: string): string =
  logger.log(lvlInfo, "Private key accessed by user: " & userId)

  try:
    result = readFile(keyPath)
    logger.log(lvlInfo, "Private key loaded successfully")
  except Exception as e:
    logger.log(lvlError, "Failed to load private key: " & e.msg)
    raise

4. セキュリティチェックリスト

本番環境デプロイ前チェックリスト:

鍵管理:
  - [ ] 秘密鍵は環境変数またはシークレットマネージャーに保存
  - [ ] .gitignoreに鍵ファイルパスを追加
  - [ ] 秘密鍵のファイルパーミッションを400に設定
  - [ ] 鍵ローテーションスケジュールを設定(90日推奨)
  - [ ] アーカイブ戦略を定義

アクセス制御:
  - [ ] 最小権限の原則を適用
  - [ ] 秘密鍵へのアクセスをロギング
  - [ ] 定期的なアクセスログレビュー

暗号化:
  - [ ] ECDSA P-256を使用(RSA 2048以上も可)
  - [ ] JWTシークレットは256ビット以上
  - [ ] パスワードはbcryptでハッシュ化

監査:
  - [ ] すべての鍵操作をログに記録
  - [ ] 異常アクセスの検知アラート設定
  - [ ] 定期的なセキュリティ監査実施

🔍 漏洩検知と対応

漏洩検知方法

# GitHub Secret Scanningなどの自動検知ツールと連携
# 漏洩が検知された場合の自動対応

proc handleKeyLeak*(keyId: string) {.async.} =
  ## 鍵漏洩時の緊急対応

  echo "⚠️  Key leak detected for key ID: ", keyId

  # 1. 即座に鍵を無効化
  await revokeKey(keyId)

  # 2. 新しい鍵を生成
  let newKeyId = await generateNewKey()

  # 3. 全クライアントに通知
  await notifyAllClients("Key rotation required. Please reactivate your license.")

  # 4. インシデントレポート作成
  await createIncidentReport(keyId, newKeyId)

  # 5. セキュリティチームに通知
  await notifySecurityTeam("Key leak incident", keyId)

  echo "✅ Key leak response completed. New key ID: ", newKeyId

漏洩後の復旧手順

1. 漏洩した鍵を即座に無効化
2. すべての関連JWTトークンを失効
3. 新しい鍵ペアを生成
4. 全ユーザーに再認証を要求
5. インシデントレポート作成
6. 根本原因分析と再発防止策実施

🌟 まとめ

鍵管理とセキュリティの要点:

  1. 鍵生成

    • ECDSA P-256
    • OpenSSL使用
    • 適切なパーミッション
  2. 保存戦略

    • 開発: 環境変数 + .gitignore
    • 本番: Secrets Manager
    • 暗号化保存
  3. ローテーション

    • 90日ごと推奨
    • 自動ローテーション
    • アーカイブ戦略
  4. セキュリティ

    • 最小権限の原則
    • アクセスログ
    • 漏洩検知と対応

前回: Day 16: クライアントサイドの実装(WebAssembly)
次回: Day 18: デバイスフィンガープリント

Happy Learning! 🎉

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?