メインコンテンツへスキップ
BiometricsMar 28, 2026

インドネシアの生体認証システム構築:アーキテクチャとRustパターン

OS
Open Soft Team

Engineering Team

インドネシアの生体SIM認証バックエンド構築

本記事はインドネシアの生体認証SIM義務化シリーズの第3部で、プロダクションレベルの生体認証システム構築に必要な実践的なアーキテクチャ判断とコード実装に焦点を当てています。システム設計、データフロー、暗号化プラクティス、そしてRustとAxumで構築するスケーラブルなバックエンドサービスについて深掘りします。

システムアーキテクチャ概要

プロダクションレベルの生体認証システムは、それぞれ特定の責務を持つ複数の相互接続されたコンポーネントで構成されます:

コアコンポーネント

  1. キャプチャサービス(モバイル/Web SDK) — 顔画像のキャプチャとデバイス上でのライブネス検出を処理するフロントエンドコンポーネント
  2. APIゲートウェイ — 認証、レート制限、リクエストルーティング、TLS終端の一元管理
  3. 生体処理エンジン — 顔画像から生体特徴テンプレートを抽出するサービス
  4. ライブネス検出サービス — パッシブおよびアクティブのライブネス検出モデルを実行
  5. IKD統合サービス — インドネシアのIKDプラットフォームとの1:1照合通信を処理
  6. 暗号化サービス — AES-256キー管理、テンプレートの暗号化/復号
  7. 監査ログサービス — 規制コンプライアンスのためにすべての認証取引を記録
  8. モニタリングとアラート — システム健全性、パフォーマンス指標、異常検出

データフローアーキテクチャ

クライアントSDK → APIゲートウェイ → 生体エンジン → IKDプラットフォーム
                ↓              ↓            ↓
           レート制限       暗号化サービス    監査ログ
                ↓              ↓            ↓
           認証キャッシュ      キー管理    コンプライアンスストレージ

Rustバックエンド実装

プロジェクト構造

生体認証サービスには以下のRustプロジェクト構造を推奨します:

biometric-service/
├── Cargo.toml
├── src/
│   ├── main.rs              # エントリーポイントとサーバーセットアップ
│   ├── config.rs            # 設定管理
│   ├── routes/
│   │   ├── mod.rs
│   │   ├── verify.rs        # 認証エンドポイント
│   │   ├── health.rs        # ヘルスチェック
│   │   └── admin.rs         # 管理エンドポイント
│   ├── services/
│   │   ├── mod.rs
│   │   ├── biometric.rs     # 生体処理
│   │   ├── liveness.rs      # ライブネス検出
│   │   ├── ikd.rs           # IKDプラットフォームクライアント
│   │   ├── crypto.rs        # 暗号化操作
│   │   └── audit.rs         # 監査ログ
│   ├── models/
│   │   ├── mod.rs
│   │   ├── verification.rs  # 認証リクエスト/レスポンス
│   │   └── audit.rs         # 監査レコード
│   ├── middleware/
│   │   ├── mod.rs
│   │   ├── auth.rs          # 認証
│   │   └── rate_limit.rs    # レート制限
│   └── errors.rs            # エラー型
├── migrations/
└── tests/

コア認証エンドポイント

use axum::{extract::State, Json};
use chrono::Utc;
use uuid::Uuid;

/// メイン認証エンドポイント — 完全な生体認証フローを処理
pub async fn verify_biometric(
    State(state): State<AppState>,
    Json(req): Json<VerificationRequest>,
) -> Result<Json<VerificationResponse>, AppError> {
    let transaction_id = Uuid::new_v4();
    let started_at = Utc::now();

    // 1. リクエスト検証
    req.validate()?;

    // 2. ライブネス検出
    let liveness = state.liveness_service
        .detect(&req.capture_data)
        .await
        .map_err(|e| {
            state.audit.log_failure(
                transaction_id, "liveness_failed", &e
            );
            e
        })?;

    if !liveness.is_live {
        return Err(AppError::LivenessCheckFailed);
    }

    // 3. 生体テンプレート抽出
    let template = state.biometric_engine
        .extract(&req.facial_image)
        .await?;

    // 4. 転送用テンプレート暗号化
    let encrypted = state.crypto_service
        .encrypt_template(&template)
        .await?;

    // 5. IKD 1:1照合
    let ikd_result = state.ikd_client
        .verify(&req.nik, &encrypted)
        .await?;

    // 6. 監査ログ記録
    let elapsed = Utc::now() - started_at;
    state.audit.log_verification(AuditRecord {
        transaction_id,
        nik_hash: hash_nik(&req.nik),
        liveness_score: liveness.confidence,
        match_score: ikd_result.score,
        verified: ikd_result.matched,
        duration_ms: elapsed.num_milliseconds(),
        timestamp: started_at,
    }).await?;

    Ok(Json(VerificationResponse {
        transaction_id,
        verified: ikd_result.matched,
        confidence: ikd_result.score,
    }))
}

AES-256暗号化サービス

UU PDPとKOMDIGI規則の要件に従い、すべての生体テンプレートはAES-256で暗号化する必要があります:

use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
use aes_gcm::aead::Aead;
use rand::RngCore;

pub struct CryptoService {
    cipher: Aes256Gcm,
}

impl CryptoService {
    pub fn new(key: &[u8; 32]) -> Self {
        let cipher = Aes256Gcm::new_from_slice(key)
            .expect("AES-256 key must be 32 bytes");
        Self { cipher }
    }

    pub async fn encrypt_template(
        &self,
        template: &BiometricTemplate,
    ) -> Result<EncryptedTemplate, CryptoError> {
        let mut nonce_bytes = [0u8; 12];
        rand::thread_rng().fill_bytes(&mut nonce_bytes);
        let nonce = Nonce::from_slice(&nonce_bytes);

        let plaintext = bincode::serialize(template)?;
        let ciphertext = self.cipher
            .encrypt(nonce, plaintext.as_ref())
            .map_err(|_| CryptoError::EncryptionFailed)?;

        Ok(EncryptedTemplate {
            ciphertext,
            nonce: nonce_bytes.to_vec(),
            algorithm: "AES-256-GCM".into(),
        })
    }

    pub async fn decrypt_template(
        &self,
        encrypted: &EncryptedTemplate,
    ) -> Result<BiometricTemplate, CryptoError> {
        let nonce = Nonce::from_slice(&encrypted.nonce);
        let plaintext = self.cipher
            .decrypt(nonce, encrypted.ciphertext.as_ref())
            .map_err(|_| CryptoError::DecryptionFailed)?;

        Ok(bincode::deserialize(&plaintext)?)
    }
}

スケーラビリティとパフォーマンス

インドネシア規模の課題

2億7000万人以上の人口と3億4500万枚のアクティブSIMカードを持つインドネシアでは、生体認証システムのスケール要件は膨大です:

  • ピーク負荷推定:最初の6ヶ月で5000万件の新規登録と仮定すると、1日あたり平均約27.8万件の認証
  • ピーク時間帯:インドネシアの業務時間パターンを考慮すると、ピーク時は平均の3-5倍、つまり1日83-139万件
  • 秒間リクエスト数:ピーク時約16 TPS、ただしバースト時のトラフィックに対する余裕が必要

水平スケーリング戦略

// Axumのマルチワーカースレッドセットアップ
#[tokio::main]
async fn main() {
    let config = Config::from_env();

    // データベース接続プール
    let pool = PgPoolOptions::new()
        .max_connections(config.db_max_connections) // 推奨:50-100
        .min_connections(config.db_min_connections) // 推奨:10
        .acquire_timeout(Duration::from_secs(3))
        .connect(&config.database_url)
        .await
        .expect("Failed to create pool");

    // アプリケーション構築
    let app = Router::new()
        .route("/api/v1/verify", post(verify_biometric))
        .route("/health", get(health_check))
        .layer(RateLimitLayer::new(config.rate_limit))
        .layer(TimeoutLayer::new(Duration::from_secs(10)))
        .with_state(AppState::new(pool, config));

    // サーバーバインド
    let listener = TcpListener::bind(&config.bind_addr)
        .await
        .expect("Failed to bind");

    axum::serve(listener, app).await.unwrap();
}

キャッシング戦略

IKDプラットフォームの負荷と応答時間を削減するため、多段キャッシングを実装:

  • L1キャッシュ(プロセス内):最近の認証結果のLRUキャッシュ、TTL 5分
  • L2キャッシュ(Redis):複数のサービスインスタンス間で共有する分散キャッシュ
  • 注意:規制要件により、認証結果はキャッシュ可能だが生体テンプレートはキャッシュ不可

UU PDPコンプライアンス実装

データ保持ポリシー

/// 定期タスク:期限切れ監査レコードのクリーンアップ
pub async fn cleanup_expired_records(
    pool: &PgPool,
) -> Result<u64, sqlx::Error> {
    // UU PDP要件:認証ログを5年間保持
    let cutoff = Utc::now() - chrono::Duration::days(5 * 365);

    let result = sqlx::query(
        "DELETE FROM audit_logs WHERE created_at < $1"
    )
    .bind(cutoff)
    .execute(pool)
    .await?;

    Ok(result.rows_affected())
}

/// ユーザーデータ削除リクエスト(忘れられる権利)
pub async fn handle_deletion_request(
    pool: &PgPool,
    nik_hash: &str,
) -> Result<DeletionReport, AppError> {
    let mut tx = pool.begin().await?;

    // 関連する生体テンプレートをすべて削除
    let templates_deleted = sqlx::query(
        "DELETE FROM biometric_templates WHERE nik_hash = $1"
    )
    .bind(nik_hash)
    .execute(&mut *tx)
    .await?
    .rows_affected();

    // 監査ログを匿名化(コンプライアンス要件のため削除ではなく匿名化)
    let logs_anonymized = sqlx::query(
        "UPDATE audit_logs SET nik_hash = 'anonymized' WHERE nik_hash = $1"
    )
    .bind(nik_hash)
    .execute(&mut *tx)
    .await?
    .rows_affected();

    tx.commit().await?;

    Ok(DeletionReport {
        templates_deleted,
        logs_anonymized,
        completed_at: Utc::now(),
    })
}

モニタリングと可観測性

主要メトリクス

生体認証システムには包括的なモニタリングが必要です。PrometheusメトリクスとGrafanaダッシュボードで追跡すべき主要指標:

  • 認証成功率:期間、事業者、地域別
  • ライブネス検出通過率:異常に低い率はシステムの問題を示す可能性
  • IKD応答時間:P50、P95、P99レイテンシー
  • エラー率:エラータイプ別(ネットワーク、タイムアウト、IKDエラー、ライブネス検出失敗)
  • 同時接続数:データベースおよびIKDプラットフォーム
  • キュー深度:非同期処理使用時
use metrics::{counter, histogram};
use std::time::Instant;

pub async fn verify_with_metrics(
    state: &AppState,
    req: &VerificationRequest,
) -> Result<VerificationResponse, AppError> {
    let start = Instant::now();
    counter!("verification_requests_total").increment(1);

    let result = do_verification(state, req).await;

    let duration = start.elapsed().as_secs_f64();
    histogram!("verification_duration_seconds").record(duration);

    match &result {
        Ok(resp) if resp.verified => {
            counter!("verification_success_total").increment(1);
        }
        Ok(_) => {
            counter!("verification_nomatch_total").increment(1);
        }
        Err(e) => {
            counter!("verification_errors_total",
                "error_type" => e.error_type()
            ).increment(1);
        }
    }

    result
}

デプロイ推奨事項

インドネシアのデータセンター

KOMDIGI規則とUU PDPの要件に基づき、生体処理はインドネシア国内のデータセンターで行う必要があります。推奨デプロイ先:

  • プライマリ:ジャカルタ(大部分のユーザーとIKDプラットフォームに近接)
  • DR:スラバヤまたはバリ(地理的冗長性)
  • CDN:全国エッジノード(SDK配布と静的リソース用)

コンテナ化デプロイ

FROM rust:1.88-slim AS builder
WORKDIR /app
COPY . .
RUN cargo build --release

FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/biometric-service /usr/local/bin/
EXPOSE 3001
CMD ["biometric-service"]

ヘルスチェックとレディネスプローブ

pub async fn health_check(
    State(state): State<AppState>,
) -> impl IntoResponse {
    let db_ok = sqlx::query("SELECT 1")
        .execute(&state.pool)
        .await
        .is_ok();

    let ikd_ok = state.ikd_client
        .ping()
        .await
        .is_ok();

    if db_ok && ikd_ok {
        (StatusCode::OK, Json(json!({"status": "healthy"})))
    } else {
        (StatusCode::SERVICE_UNAVAILABLE, Json(json!({
            "status": "unhealthy",
            "db": db_ok,
            "ikd": ikd_ok,
        })))
    }
}

まとめ

インドネシアのKOMDIGI規則に準拠した生体認証システムの構築は、複雑ですが管理可能なエンジニアリング課題です。重要なポイント:

  1. セキュリティファースト:AES-256暗号化、TLS 1.3通信、ゼロトラストアーキテクチャ
  2. コンプライアンス駆動:UU PDPデータ保護、5年間の監査ログ保持、ユーザー削除権
  3. スケーラブル設計:水平スケーリング、多段キャッシング、非同期処理
  4. ローカライズドデプロイ:インドネシア国内のデータセンター、低帯域幅最適化、デバイス多様性対応
  5. 包括的モニタリング:リアルタイムメトリクス、異常検出、コンプライアンスレポート

RustとAxumでこのシステムを構築することで、インドネシアの厳格な規制要件を満たしながら、優れたパフォーマンスとセキュリティ保証を提供できます。