From 24ba4e5909116e5a352795c63e6da2a04eca97d8 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:23:44 +0900 Subject: [PATCH 01/48] =?UTF-8?q?README=20=EB=AC=B8=EC=84=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20=ED=95=9C=EA=B5=AD=EC=96=B4=20(README=5FKR.md)?= =?UTF-8?q?=20-=20English=20(README.md)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++ README_KR.md | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 README.md create mode 100644 README_KR.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..6cd7e12 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# pairier + +
+ +[한국어](README_KR.md) | English + +
+ +A Zero-Trust hybrid Key Management System (KMS) built with Go and Rust. The Go host provides the network and management interfaces, and the Rust security core owns the cryptographic engine and key material. + +## Layout + +- `core/` Rust workspace (security core) + - `kms-wire` dependency-free `no_std` wire protocol v1 + - `kms-sys` process hardening and secure memory, the only crate where `unsafe` is allowed + - `kms-engine` cryptographic engine built on aws-lc-rs + - `kms-core` security core daemon binary +- `host/` Go module (host) + - `internal/wire` Go implementation of the wire protocol + - `internal/core` core process launcher and client + - `cmd/kmsd`, `cmd/kmsctl` daemon and management tool (API layer not yet implemented) +- `conformance/wire/` protocol conformance vectors that both Rust and Go must pass + +## Features + +The core (Rust) runs as a separate process and never shares an address space with the host (Go). The host creates a `socketpair` inside its own process, keeps one end for itself, and hands the other end to the child as `fd` 3 when it executes `kms-core`. No socket path appears on the filesystem and nothing listens, so there is no way for a third party to connect; only the host and the core share the channel. Because the host launches the core, both run under the same UID. For production, the core can instead be started first under a separate UID, with the host connecting through a `--socket` path; in that mode the core verifies the connecting peer with `--peer-uid`. Either way, TCP is never used, only local IPC on the same machine. + +A stateless core is another key property. The core keeps only a 32-byte root key in memory. Individual keys are stored on the host as blobs sealed under the root key, and the host cannot open them. The blob header (algorithm, usage, key id, label) is fully authenticated. + +The following features are also included. + +- Validated cryptographic module: uses aws-lc-rs (AWS-LC); the `fips` feature selects the FIPS 140-3 validated module. The Go side uses the standard library only. +- Algorithms: AES-256-GCM, ECDSA P-256/P-384, Ed25519, ML-DSA-65/87, ML-KEM-768/1024 +- Optional PQC: enabled only when both the compile-time `pqc` feature and the runtime `--pqc` flag are set, and advertised to the host through capability bits in the `HELLO` response. +- Key usage enforcement: the core rejects any operation outside the usage assigned at generation time (ENCRYPT, DECRYPT, SIGN, VERIFY, and so on). +- Core hardening: on startup the core disables core dumps, denies `ptrace`, sets `no_new_privs` and `umask 077`, scrubs environment variables, closes unneeded `fd`s, verifies the peer UID, and checks the root key file's permissions, owner, and links. On Linux, `mlockall` prevents memory from being swapped. +- Strict protocol: fixed 16-byte header, length-prefixed fields, 1 MiB body limit, and rejection of trailing bytes. `HELLO` must come first, and any framing violation terminates the session. +- Air-gapped build: Rust dependencies are vendored in `core/vendor` and built with `--frozen`. Go builds with `GOPROXY=off` and `-mod=vendor` and has no external dependencies. + +## Build + +Requirements are Rust `1.95.0` (pinned by `rust-toolchain.toml`) and Go `1.27.2`. A FIPS build additionally needs `cmake` and `perl`, and is statically linked only on Linux x86_64/aarch64. + +```bash +$ make build # core (release, with pqc) + host +$ make build-core FIPS=1 +$ make test # Rust tests with all features + Go tests (using the real core binary) +$ make lint +``` + +## Running the core + +```bash +$ kms-core --generate-root-key /path/root.key +$ kms-core --socket /run/kms/core.sock --root-key-file /path/root.key --pqc +``` + +When the host launches the core directly, it uses the inherited socket via `--fd 3`. On platforms without `mlockall`, such as macOS, `--allow-unlocked-memory` is required. diff --git a/README_KR.md b/README_KR.md new file mode 100644 index 0000000..d800953 --- /dev/null +++ b/README_KR.md @@ -0,0 +1,58 @@ +# pairier + +
+ +한국어 | [English](README.md) + +
+ +제로 트러스트(Zero-Trust) 기반 Go 및 Rust의 하이브리드 키 관리 체계(Key Management System, KMS)입니다. Go 호스트가 네트워크와 관리 인터페이스를 맡고, Rust 보안 코어가 암호 엔진과 키 재료(material)를 맡습니다. + +## 구조 + +- `core/` Rust 워크스페이스 (보안 코어) + - `kms-wire` 의존성 없는 `no_std` 와이어 프로토콜 v1 + - `kms-sys` 프로세스 하드닝과 보안 메모리, 유일하게 `unsafe`가 허용된 crate + - `kms-engine` aws-lc-rs 기반 암호 엔진 + - `kms-core` 보안 코어 데몬(daemon) 바이너리 +- `host/` Go 모듈 (호스트) + - `internal/wire` 와이어 프로토콜 Go 구현 + - `internal/core` 코어 프로세스 기동과 클라이언트 + - `cmd/kmsd`, `cmd/kmsctl` 데몬과 관리 도구 (API 계층은 구현 예정) +- `conformance/wire/` Rust와 Go가 함께 통과해야 하는 프로토콜 적합성 벡터 + +## 기능 + +코어(Rust)는 별도 프로세스로 동작하며, 호스트(Go)와 같은 주소 공간을 공유하지 않습니다. 호스트가 자기 프로세스 안에서 `socketpair`를 만들어 한쪽 끝은 자신이 갖고, 다른 쪽 끝을 자식 프로세스의 `fd` 3번으로 넘기며 `kms-core`를 실행합니다. 이 방식에서는 파일시스템(fs)에 소켓 경로가 생기지 않고 listen도 하지 않으므로 제3자가 접속할 방법이 없고, 호스트와 코어만 해당 통로를 공유합니다. 다만 호스트가 코어를 띄우기 때문에 같은 UID로 동작합니다. 운영 환경에서는 코어를 별도 UID로 먼저 띄우고 호스트가 `--socket` 경로로 접속하는 방식도 지원하며, 이때 코어는 `--peer-uid`로 접속자를 검증합니다. 어느 방식이든 TCP는 쓰지 않고 같은 머신 안의 로컬 IPC만 사용합니다. + +상태 없는(stateless) 코어도 핵심 기능입니다. 코어는 32바이트 루트 키(root key)만 메모리에 두고, 개별 키는 루트 키로 봉인된(sealed) blob으로 호스트에 저장되며, 호스트는 해당 blob을 열 수 없습니다. blob 헤더(알고리즘, 용도, 키 id, 라벨)는 모두 인증됩니다. + +이 외에도 다음과 같은 중요한 기능이 포함됩니다. + +- 검인증 암호 모듈: aws-lc-rs(AWS-LC)를 사용하며, `fips` feature로 FIPS 140-3 검증 모듈을 선택합니다. Go 쪽은 표준 라이브러리만 사용합니다. +- 알고리즘: AES-256-GCM, ECDSA P-256/P-384, Ed25519, ML-DSA-65/87, ML-KEM-768/1024 +- 선택적 PQC: 컴파일 타임 `pqc` feature와 런타임 `--pqc` 플래그를 모두 설정해야 활성화되며, `HELLO` 응답의 capability 비트를 통해 호스트에 알립니다. +- 키 용도 강제: 생성 시 지정한 용도(ENCRYPT, DECRYPT, SIGN, VERIFY 등) 외의 연산은 코어 측에서 거부합니다. +- 코어 하드닝: 시작 시 코어 덤프 차단, `ptrace` 거부, `no_new_privs`, `umask 077`, 환경 변수 제거, 불필요 `fd` 정리, 피어(peer) UID 검증, 루트 키 파일 권한·소유자·링크 검증을 수행합니다. Linux에서는 `mlockall`로 메모리 스와핑(swap)을 막습니다. +- 엄격한 프로토콜: 고정 16바이트 헤더와 길이 접두 필드, 1MiB 본문 상한, 후행 바이트를 거부합니다. `HELLO` 선행을 강제하며 프레이밍(framing) 위반 시 세션을 끊어냅니다. +- 폐쇄 빌드: Rust 의존성은 `core/vendor`에 벤더링되고 `--frozen`으로 빌드합니다. Go는 `GOPROXY=off`, `-mod=vendor`로 빌드하며 외부 의존성이 없습니다. + +## 빌드 + +요구 사항은 Rust `1.95.0`(`rust-toolchain.toml`로 고정)과 Go `1.27.2`입니다. FIPS 빌드는 `cmake`, `perl`이 추가로 필요하며 Linux x86_64/aarch64에서만 정적 링크됩니다. + +```bash +$ make build # 코어(release, pqc 포함) + 호스트 +$ make build-core FIPS=1 +$ make test # Rust 전체 feature 테스트 + Go 테스트(실제 코어 바이너리 사용) +$ make lint +``` + +## 코어 실행 + +```bash +$ kms-core --generate-root-key /path/root.key +$ kms-core --socket /run/kms/core.sock --root-key-file /path/root.key --pqc +``` + +호스트가 직접 기동할 때는 `--fd 3`으로 상속 소켓을 사용합니다. macOS처럼 `mlockall`이 없는 환경에서는 `--allow-unlocked-memory`가 필요합니다. From 85578c1898413ef3e4e037eae511a29982aa3d90 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:23:59 +0900 Subject: [PATCH 02/48] =?UTF-8?q?=EB=A6=B4=EB=A6=AC=EC=A6=88=201.0.0=20bum?= =?UTF-8?q?p?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/Cargo.lock | 8 ++++---- core/Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/Cargo.lock b/core/Cargo.lock index 7422ad0..0ac4e7d 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -169,7 +169,7 @@ dependencies = [ [[package]] name = "kms-core" -version = "0.1.0" +version = "1.0.0" dependencies = [ "kms-engine", "kms-sys", @@ -180,7 +180,7 @@ dependencies = [ [[package]] name = "kms-engine" -version = "0.1.0" +version = "1.0.0" dependencies = [ "aws-lc-rs", "kms-sys", @@ -190,7 +190,7 @@ dependencies = [ [[package]] name = "kms-sys" -version = "0.1.0" +version = "1.0.0" dependencies = [ "libc", "zeroize", @@ -198,7 +198,7 @@ dependencies = [ [[package]] name = "kms-wire" -version = "0.1.0" +version = "1.0.0" [[package]] name = "libc" diff --git a/core/Cargo.toml b/core/Cargo.toml index 4e1d9a2..d15b578 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -3,7 +3,7 @@ resolver = "3" members = ["crates/*"] [workspace.package] -version = "0.1.0" +version = "1.0.0" edition = "2024" rust-version = "1.95" publish = false From dabbc5128ebd0e56eaa1397730efe3d8c428abfe Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:50:28 +0900 Subject: [PATCH 03/48] =?UTF-8?q?KEM=20=EC=95=8C=EA=B3=A0=EB=A6=AC?= =?UTF-8?q?=EC=A6=98=20=ED=99=95=EC=9E=A5=20-=20DHKEM(P-256,=20HKDF-SHA256?= =?UTF-8?q?)=EC=9D=84=20RFC=209180=20=EB=B0=A9=EC=8B=9D=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=ED=95=98=EA=B3=A0=20RFC=20=EB=B6=80?= =?UTF-8?q?=EB=A1=9D=20=EB=B2=A1=ED=84=B0=EB=A1=9C=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?-=20P-256=20+=20ML-KEM-768=20=ED=95=98=EC=9D=B4=EB=B8=8C?= =?UTF-8?q?=EB=A6=AC=EB=93=9C=20KEM=20=EC=B6=94=EA=B0=80=20(pqc=20?= =?UTF-8?q?=ED=94=BC=EC=B2=98)=20-=20KEM=20=EC=BA=A1=EC=8A=90=ED=99=94?= =?UTF-8?q?=EC=99=80=20=EB=B3=B5=ED=98=B8=ED=99=94=EB=A5=BC=20pqc=20?= =?UTF-8?q?=ED=94=BC=EC=B2=98=EC=99=80=20=EB=AC=B4=EA=B4=80=ED=95=98?= =?UTF-8?q?=EA=B2=8C=20=EB=85=B8=EC=B6=9C,=20usage=20EXPORT=20=EB=B9=84?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80=20-=20Go=20=EC=99=80=EC=9D=B4?= =?UTF-8?q?=EC=96=B4=20=EC=83=81=EC=88=98=20=EB=8F=99=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/crates/kms-engine/src/alg.rs | 34 ++- core/crates/kms-engine/src/kdf.rs | 14 +- core/crates/kms-engine/src/kem.rs | 364 ++++++++++++++++++++++++++-- core/crates/kms-engine/src/lib.rs | 104 +++++--- core/crates/kms-engine/src/sign.rs | 18 +- core/crates/kms-engine/src/usage.rs | 12 +- core/crates/kms-wire/src/lib.rs | 5 +- host/internal/wire/msg.go | 21 +- 8 files changed, 501 insertions(+), 71 deletions(-) diff --git a/core/crates/kms-engine/src/alg.rs b/core/crates/kms-engine/src/alg.rs index d1fe8c5..6de6723 100644 --- a/core/crates/kms-engine/src/alg.rs +++ b/core/crates/kms-engine/src/alg.rs @@ -8,10 +8,12 @@ pub enum Alg { EcdsaP256Sha256, EcdsaP384Sha384, Ed25519, + DhkemP256, MlDsa65, MlDsa87, MlKem768, MlKem1024, + HybridP256MlKem768, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -31,7 +33,9 @@ impl Alg { | Self::Ed25519 | Self::MlDsa65 | Self::MlDsa87 => AlgKind::Signature, - Self::MlKem768 | Self::MlKem1024 => AlgKind::Kem, + Self::DhkemP256 | Self::MlKem768 | Self::MlKem1024 | Self::HybridP256MlKem768 => { + AlgKind::Kem + } } } @@ -39,7 +43,11 @@ impl Alg { pub fn is_pqc(self) -> bool { matches!( self, - Self::MlDsa65 | Self::MlDsa87 | Self::MlKem768 | Self::MlKem1024 + Self::MlDsa65 + | Self::MlDsa87 + | Self::MlKem768 + | Self::MlKem1024 + | Self::HybridP256MlKem768 ) } @@ -50,10 +58,12 @@ impl Alg { Self::EcdsaP256Sha256 => wire::ECDSA_P256_SHA256, Self::EcdsaP384Sha384 => wire::ECDSA_P384_SHA384, Self::Ed25519 => wire::ED25519, + Self::DhkemP256 => wire::DHKEM_P256, Self::MlDsa65 => wire::ML_DSA_65, Self::MlDsa87 => wire::ML_DSA_87, Self::MlKem768 => wire::ML_KEM_768, Self::MlKem1024 => wire::ML_KEM_1024, + Self::HybridP256MlKem768 => wire::HYBRID_P256_ML_KEM_768, } } @@ -61,13 +71,14 @@ impl Alg { pub fn public_key_len(self) -> Option { match self { Self::Aes256Gcm => None, - Self::EcdsaP256Sha256 => Some(65), + Self::EcdsaP256Sha256 | Self::DhkemP256 => Some(65), Self::EcdsaP384Sha384 => Some(97), Self::Ed25519 => Some(32), Self::MlDsa65 => Some(1952), Self::MlDsa87 => Some(2592), Self::MlKem768 => Some(1184), Self::MlKem1024 => Some(1568), + Self::HybridP256MlKem768 => Some(1249), } } @@ -78,16 +89,27 @@ impl Alg { Self::EcdsaP384Sha384 => Some(96), Self::MlDsa65 => Some(3309), Self::MlDsa87 => Some(4627), - Self::Aes256Gcm | Self::MlKem768 | Self::MlKem1024 => None, + Self::Aes256Gcm + | Self::DhkemP256 + | Self::MlKem768 + | Self::MlKem1024 + | Self::HybridP256MlKem768 => None, } } #[must_use] pub fn kem_ciphertext_len(self) -> Option { match self { + Self::DhkemP256 => Some(65), Self::MlKem768 => Some(1088), Self::MlKem1024 => Some(1568), - _ => None, + Self::HybridP256MlKem768 => Some(1153), + Self::Aes256Gcm + | Self::EcdsaP256Sha256 + | Self::EcdsaP384Sha384 + | Self::Ed25519 + | Self::MlDsa65 + | Self::MlDsa87 => None, } } } @@ -101,10 +123,12 @@ impl TryFrom for Alg { wire::ECDSA_P256_SHA256 => Ok(Self::EcdsaP256Sha256), wire::ECDSA_P384_SHA384 => Ok(Self::EcdsaP384Sha384), wire::ED25519 => Ok(Self::Ed25519), + wire::DHKEM_P256 => Ok(Self::DhkemP256), wire::ML_DSA_65 => Ok(Self::MlDsa65), wire::ML_DSA_87 => Ok(Self::MlDsa87), wire::ML_KEM_768 => Ok(Self::MlKem768), wire::ML_KEM_1024 => Ok(Self::MlKem1024), + wire::HYBRID_P256_ML_KEM_768 => Ok(Self::HybridP256MlKem768), _ => Err(Error::UnsupportedAlgorithm), } } diff --git a/core/crates/kms-engine/src/kdf.rs b/core/crates/kms-engine/src/kdf.rs index be0aefc..4b1b15c 100644 --- a/core/crates/kms-engine/src/kdf.rs +++ b/core/crates/kms-engine/src/kdf.rs @@ -21,14 +21,18 @@ pub(crate) struct DerivedAead { pub(crate) nonce: Nonce, } +pub(crate) fn expand(salt: &[u8], ikm: &[u8], info: &[&[u8]], out: &mut [u8]) -> Result<(), Error> { + let prk = Salt::new(HKDF_SHA256, salt).extract(ikm); + prk.expand(info, OkmLen(out.len())) + .map_err(|_| Error::Internal)? + .fill(out) + .map_err(|_| Error::Internal) +} + pub(crate) fn derive_aead(ikm: &[u8], salt: &[u8], info: &[&[u8]]) -> Result { const OKM_LEN: usize = KEY_LEN + NONCE_LEN; - let prk = Salt::new(HKDF_SHA256, salt).extract(ikm); let mut okm = Zeroizing::new([0u8; OKM_LEN]); - prk.expand(info, OkmLen(OKM_LEN)) - .map_err(|_| Error::Internal)? - .fill(okm.as_mut_slice()) - .map_err(|_| Error::Internal)?; + expand(salt, ikm, info, okm.as_mut_slice())?; let (key_bytes, nonce_bytes) = okm.split_at(KEY_LEN); let key = UnboundKey::new(&AES_256_GCM, key_bytes).map_err(|_| Error::Internal)?; let nonce = Nonce::try_assume_unique_for_key(nonce_bytes).map_err(|_| Error::Internal)?; diff --git a/core/crates/kms-engine/src/kem.rs b/core/crates/kms-engine/src/kem.rs index 6ffa0a6..a1a939f 100644 --- a/core/crates/kms-engine/src/kem.rs +++ b/core/crates/kms-engine/src/kem.rs @@ -1,3 +1,6 @@ +use aws_lc_rs::agreement::{self, ECDH_P256, PrivateKey, UnparsedPublicKey}; +use aws_lc_rs::encoding::{AsBigEndian, EcPrivateKeyBin}; +#[cfg(feature = "pqc")] use aws_lc_rs::kem::{ Algorithm, Ciphertext, DecapsulationKey, EncapsulationKey, ML_KEM_768, ML_KEM_1024, }; @@ -5,17 +8,172 @@ use zeroize::Zeroizing; use crate::alg::Alg; use crate::error::Error; +use crate::kdf; -fn kem_alg(alg: Alg) -> Result<&'static Algorithm, Error> { +pub(crate) const SHARED_SECRET_LEN: usize = 32; +const P256_SECRET_LEN: usize = 32; +const P256_PUBLIC_LEN: usize = 65; +const DHKEM_P256_SUITE_ID: [u8; 5] = [b'K', b'E', b'M', 0x00, 0x10]; +const HPKE_VERSION: &[u8] = b"HPKE-v1"; +#[cfg(feature = "pqc")] +const HYBRID_SALT: &[u8] = b"pairier/kms/hybrid-kem/v1"; +#[cfg(feature = "pqc")] +const ML_KEM_768_PUBLIC_LEN: usize = 1184; +#[cfg(feature = "pqc")] +const ML_KEM_768_SECRET_LEN: usize = 2400; +#[cfg(feature = "pqc")] +const ML_KEM_768_CIPHERTEXT_LEN: usize = 1088; +#[cfg(feature = "pqc")] +const ML_KEM_768_EK_OFFSET: usize = 1152; +#[cfg(feature = "pqc")] +const ML_KEM_768_EK_END: usize = ML_KEM_768_EK_OFFSET + ML_KEM_768_PUBLIC_LEN; + +pub(crate) type Secret = Zeroizing>; +pub(crate) type Generated = (Secret, Vec); +pub(crate) type Encapsulated = (Vec, Secret); + +pub(crate) fn generate(alg: Alg) -> Result { + match alg { + Alg::DhkemP256 => dhkem_generate(), + #[cfg(feature = "pqc")] + Alg::MlKem768 => mlkem_generate(&ML_KEM_768), + #[cfg(feature = "pqc")] + Alg::MlKem1024 => mlkem_generate(&ML_KEM_1024), + #[cfg(feature = "pqc")] + Alg::HybridP256MlKem768 => hybrid_generate(), + #[cfg(not(feature = "pqc"))] + Alg::MlKem768 | Alg::MlKem1024 | Alg::HybridP256MlKem768 => Err(Error::CapabilityDisabled), + Alg::Aes256Gcm + | Alg::EcdsaP256Sha256 + | Alg::EcdsaP384Sha384 + | Alg::Ed25519 + | Alg::MlDsa65 + | Alg::MlDsa87 => Err(Error::InvalidArgument), + } +} + +pub(crate) fn encapsulate(alg: Alg, public_key: &[u8]) -> Result { + if Some(public_key.len()) != alg.public_key_len() { + return Err(Error::InvalidArgument); + } match alg { - Alg::MlKem768 => Ok(&ML_KEM_768), - Alg::MlKem1024 => Ok(&ML_KEM_1024), - _ => Err(Error::InvalidArgument), + Alg::DhkemP256 => dhkem_encapsulate(public_key), + #[cfg(feature = "pqc")] + Alg::MlKem768 => mlkem_encapsulate(&ML_KEM_768, public_key), + #[cfg(feature = "pqc")] + Alg::MlKem1024 => mlkem_encapsulate(&ML_KEM_1024, public_key), + #[cfg(feature = "pqc")] + Alg::HybridP256MlKem768 => hybrid_encapsulate(public_key), + #[cfg(not(feature = "pqc"))] + Alg::MlKem768 | Alg::MlKem1024 | Alg::HybridP256MlKem768 => Err(Error::CapabilityDisabled), + Alg::Aes256Gcm + | Alg::EcdsaP256Sha256 + | Alg::EcdsaP384Sha384 + | Alg::Ed25519 + | Alg::MlDsa65 + | Alg::MlDsa87 => Err(Error::InvalidArgument), } } -pub(crate) fn generate(alg: Alg) -> Result<(Zeroizing>, Vec), Error> { - let a = kem_alg(alg)?; +pub(crate) fn decapsulate(alg: Alg, secret: &[u8], ciphertext: &[u8]) -> Result { + if Some(ciphertext.len()) != alg.kem_ciphertext_len() { + return Err(Error::CryptoFailure); + } + match alg { + Alg::DhkemP256 => dhkem_decapsulate(secret, ciphertext), + #[cfg(feature = "pqc")] + Alg::MlKem768 => mlkem_decapsulate(&ML_KEM_768, secret, ciphertext), + #[cfg(feature = "pqc")] + Alg::MlKem1024 => mlkem_decapsulate(&ML_KEM_1024, secret, ciphertext), + #[cfg(feature = "pqc")] + Alg::HybridP256MlKem768 => hybrid_decapsulate(secret, ciphertext), + #[cfg(not(feature = "pqc"))] + Alg::MlKem768 | Alg::MlKem1024 | Alg::HybridP256MlKem768 => Err(Error::CapabilityDisabled), + Alg::Aes256Gcm + | Alg::EcdsaP256Sha256 + | Alg::EcdsaP384Sha384 + | Alg::Ed25519 + | Alg::MlDsa65 + | Alg::MlDsa87 => Err(Error::InvalidArgument), + } +} + +fn p256_public(sk: &PrivateKey) -> Result, Error> { + let pk = sk.compute_public_key().map_err(|_| Error::Internal)?; + let bytes = pk.as_ref(); + if bytes.len() != P256_PUBLIC_LEN { + return Err(Error::Internal); + } + Ok(bytes.to_vec()) +} + +fn p256_dh(sk: &PrivateKey, peer: &[u8], err: Error) -> Result { + if peer.len() != P256_PUBLIC_LEN { + return Err(err); + } + agreement::agree(sk, UnparsedPublicKey::new(&ECDH_P256, peer), err, |dh| { + Ok(Zeroizing::new(dh.to_vec())) + }) +} + +fn dhkem_shared_secret(dh: &[u8], enc: &[u8], pk_r: &[u8]) -> Result { + let mut labeled_ikm = Zeroizing::new(Vec::new()); + labeled_ikm.extend_from_slice(HPKE_VERSION); + labeled_ikm.extend_from_slice(&DHKEM_P256_SUITE_ID); + labeled_ikm.extend_from_slice(b"eae_prk"); + labeled_ikm.extend_from_slice(dh); + let len = u16::try_from(SHARED_SECRET_LEN) + .map_err(|_| Error::Internal)? + .to_be_bytes(); + let mut ss = Zeroizing::new(vec![0u8; SHARED_SECRET_LEN]); + kdf::expand( + &[], + &labeled_ikm, + &[ + &len, + HPKE_VERSION, + &DHKEM_P256_SUITE_ID, + b"shared_secret", + enc, + pk_r, + ], + ss.as_mut_slice(), + )?; + Ok(ss) +} + +fn dhkem_generate() -> Result { + let sk = PrivateKey::generate(&ECDH_P256).map_err(|_| Error::Internal)?; + let scalar = + AsBigEndian::>::as_be_bytes(&sk).map_err(|_| Error::Internal)?; + if scalar.as_ref().len() != P256_SECRET_LEN { + return Err(Error::Internal); + } + let pk = p256_public(&sk)?; + Ok((Zeroizing::new(scalar.as_ref().to_vec()), pk)) +} + +fn dhkem_encap_with(sk_e: &PrivateKey, pk_r: &[u8]) -> Result { + let dh = p256_dh(sk_e, pk_r, Error::InvalidArgument)?; + let enc = p256_public(sk_e)?; + let ss = dhkem_shared_secret(&dh, &enc, pk_r)?; + Ok((enc, ss)) +} + +fn dhkem_encapsulate(pk_r: &[u8]) -> Result { + let sk_e = PrivateKey::generate(&ECDH_P256).map_err(|_| Error::Internal)?; + dhkem_encap_with(&sk_e, pk_r) +} + +fn dhkem_decapsulate(secret: &[u8], enc: &[u8]) -> Result { + let sk = PrivateKey::from_private_key(&ECDH_P256, secret).map_err(|_| Error::InvalidKeyBlob)?; + let dh = p256_dh(&sk, enc, Error::CryptoFailure)?; + let pk_r = p256_public(&sk)?; + dhkem_shared_secret(&dh, enc, &pk_r) +} + +#[cfg(feature = "pqc")] +fn mlkem_generate(a: &'static Algorithm) -> Result { let dk = DecapsulationKey::generate(a).map_err(|_| Error::Internal)?; let secret = dk.key_bytes().map_err(|_| Error::Internal)?; let ek = dk @@ -29,31 +187,197 @@ pub(crate) fn generate(alg: Alg) -> Result<(Zeroizing>, Vec), Error> )) } -pub(crate) fn encapsulate( - alg: Alg, - ek_bytes: &[u8], -) -> Result<(Vec, Zeroizing>), Error> { - let a = kem_alg(alg)?; - if Some(ek_bytes.len()) != alg.public_key_len() { - return Err(Error::InvalidArgument); +#[cfg(feature = "pqc")] +fn mlkem768_public(dk_bytes: &[u8]) -> Result<&[u8], Error> { + if dk_bytes.len() != ML_KEM_768_SECRET_LEN { + return Err(Error::InvalidKeyBlob); } + dk_bytes + .get(ML_KEM_768_EK_OFFSET..ML_KEM_768_EK_END) + .ok_or(Error::InvalidKeyBlob) +} + +#[cfg(feature = "pqc")] +fn mlkem_encapsulate(a: &'static Algorithm, ek_bytes: &[u8]) -> Result { let ek = EncapsulationKey::new(a, ek_bytes).map_err(|_| Error::InvalidArgument)?; let (ct, ss) = ek.encapsulate().map_err(|_| Error::CryptoFailure)?; Ok((ct.as_ref().to_vec(), Zeroizing::new(ss.as_ref().to_vec()))) } -pub(crate) fn decapsulate( - alg: Alg, +#[cfg(feature = "pqc")] +fn mlkem_decapsulate( + a: &'static Algorithm, dk_bytes: &[u8], ciphertext: &[u8], -) -> Result>, Error> { - let a = kem_alg(alg)?; - if Some(ciphertext.len()) != alg.kem_ciphertext_len() { - return Err(Error::CryptoFailure); - } +) -> Result { let dk = DecapsulationKey::new(a, dk_bytes).map_err(|_| Error::InvalidKeyBlob)?; let ss = dk .decapsulate(Ciphertext::from(ciphertext)) .map_err(|_| Error::CryptoFailure)?; Ok(Zeroizing::new(ss.as_ref().to_vec())) } + +#[cfg(feature = "pqc")] +fn hybrid_combine( + ss_dh: &[u8], + ss_ml: &[u8], + enc_dh: &[u8], + ct_ml: &[u8], + dh_pk: &[u8], + ml_pk: &[u8], +) -> Result { + let mut ikm = Zeroizing::new(Vec::new()); + ikm.extend_from_slice(ss_dh); + ikm.extend_from_slice(ss_ml); + let mut ss = Zeroizing::new(vec![0u8; SHARED_SECRET_LEN]); + kdf::expand( + HYBRID_SALT, + &ikm, + &[enc_dh, ct_ml, dh_pk, ml_pk], + ss.as_mut_slice(), + )?; + Ok(ss) +} + +#[cfg(feature = "pqc")] +fn hybrid_generate() -> Result { + let (dh_secret, dh_public) = dhkem_generate()?; + let (ml_secret, ml_public) = mlkem_generate(&ML_KEM_768)?; + let mut secret = Zeroizing::new(Vec::new()); + secret.extend_from_slice(&dh_secret); + secret.extend_from_slice(&ml_secret); + let mut public = dh_public; + public.extend_from_slice(&ml_public); + Ok((secret, public)) +} + +#[cfg(feature = "pqc")] +fn hybrid_encapsulate(public_key: &[u8]) -> Result { + let (dh_pk, ml_pk) = public_key + .split_at_checked(P256_PUBLIC_LEN) + .ok_or(Error::InvalidArgument)?; + if ml_pk.len() != ML_KEM_768_PUBLIC_LEN { + return Err(Error::InvalidArgument); + } + let (enc_dh, ss_dh) = dhkem_encapsulate(dh_pk)?; + let (ct_ml, ss_ml) = mlkem_encapsulate(&ML_KEM_768, ml_pk)?; + let ss = hybrid_combine(&ss_dh, &ss_ml, &enc_dh, &ct_ml, dh_pk, ml_pk)?; + let mut ct = enc_dh; + ct.extend_from_slice(&ct_ml); + Ok((ct, ss)) +} + +#[cfg(feature = "pqc")] +fn hybrid_decapsulate(secret: &[u8], ciphertext: &[u8]) -> Result { + let (dh_sk, ml_sk) = secret + .split_at_checked(P256_SECRET_LEN) + .ok_or(Error::InvalidKeyBlob)?; + if ml_sk.len() != ML_KEM_768_SECRET_LEN { + return Err(Error::InvalidKeyBlob); + } + let (enc_dh, ct_ml) = ciphertext + .split_at_checked(P256_PUBLIC_LEN) + .ok_or(Error::CryptoFailure)?; + if ct_ml.len() != ML_KEM_768_CIPHERTEXT_LEN { + return Err(Error::CryptoFailure); + } + let ss_dh = dhkem_decapsulate(dh_sk, enc_dh)?; + let ss_ml = mlkem_decapsulate(&ML_KEM_768, ml_sk, ct_ml)?; + let sk = PrivateKey::from_private_key(&ECDH_P256, dh_sk).map_err(|_| Error::InvalidKeyBlob)?; + let dh_public = p256_public(&sk)?; + let ml_public = mlkem768_public(ml_sk)?; + hybrid_combine(&ss_dh, &ss_ml, enc_dh, ct_ml, &dh_public, ml_public) +} + +#[cfg(test)] +#[allow( + clippy::unwrap_used, + clippy::indexing_slicing, + clippy::arithmetic_side_effects +)] +mod tests { + use super::{ + ECDH_P256, Error, P256_PUBLIC_LEN, PrivateKey, dhkem_decapsulate, dhkem_encap_with, + dhkem_encapsulate, dhkem_generate, p256_public, + }; + + fn unhex(s: &str) -> Vec { + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()) + .collect() + } + + #[test] + fn dhkem_p256_matches_rfc9180_a3_1() { + let sk_e = unhex("4995788ef4b9d6132b249ce59a77281493eb39af373d236a1fe415cb0c2d7beb"); + let sk_r = unhex("f3ce7fdae57e1a310d87f1ebbde6f328be0a99cdbcadf4d6589cf29de4b8ffd2"); + let expected = unhex("c0d26aeab536609a572b07695d933b589dcf363ff9d93c93adea537aeabb8cb8"); + let recipient = PrivateKey::from_private_key(&ECDH_P256, &sk_r).unwrap(); + let pk_r = p256_public(&recipient).unwrap(); + let ephemeral = PrivateKey::from_private_key(&ECDH_P256, &sk_e).unwrap(); + let (enc, ss) = dhkem_encap_with(&ephemeral, &pk_r).unwrap(); + assert_eq!(ss.as_slice(), expected.as_slice()); + assert_eq!(enc, p256_public(&ephemeral).unwrap()); + let ss2 = dhkem_decapsulate(&sk_r, &enc).unwrap(); + assert_eq!(ss2.as_slice(), expected.as_slice()); + } + + #[cfg(feature = "pqc")] + #[test] + fn mlkem768_public_key_is_embedded_in_secret() { + let (dk, ek) = super::mlkem_generate(&super::ML_KEM_768).unwrap(); + assert_eq!(super::mlkem768_public(&dk).unwrap(), ek.as_slice()); + assert_eq!( + super::mlkem768_public(&dk[1..]).unwrap_err(), + Error::InvalidKeyBlob + ); + let (sk, pk) = super::hybrid_generate().unwrap(); + assert_eq!(sk.len(), 32 + 2400); + assert_eq!(pk.len(), 65 + 1184); + let (ct, ss) = super::hybrid_encapsulate(&pk).unwrap(); + assert_eq!(ct.len(), 65 + 1088); + assert_eq!( + super::hybrid_decapsulate(&sk, &ct).unwrap().as_slice(), + ss.as_slice() + ); + let mut swapped = ct.clone(); + swapped[70] ^= 1; + assert_ne!( + super::hybrid_decapsulate(&sk, &swapped).unwrap().as_slice(), + ss.as_slice() + ); + } + + #[test] + fn dhkem_rejects_invalid_points_and_keys() { + let (sk, pk) = dhkem_generate().unwrap(); + assert_eq!(pk.len(), P256_PUBLIC_LEN); + assert_eq!(pk[0], 0x04); + let mut off_curve = vec![0u8; P256_PUBLIC_LEN]; + off_curve[0] = 0x04; + assert_eq!( + dhkem_encapsulate(&off_curve).unwrap_err(), + Error::InvalidArgument + ); + assert_eq!( + dhkem_decapsulate(&sk, &off_curve).unwrap_err(), + Error::CryptoFailure + ); + let mut wrong_prefix = pk.clone(); + wrong_prefix[0] = 0x05; + assert_eq!( + dhkem_encapsulate(&wrong_prefix).unwrap_err(), + Error::InvalidArgument + ); + let (enc, _) = dhkem_encapsulate(&pk).unwrap(); + assert_eq!( + dhkem_decapsulate(&sk[1..], &enc).unwrap_err(), + Error::InvalidKeyBlob + ); + assert_eq!( + dhkem_decapsulate(&[0u8; 32], &enc).unwrap_err(), + Error::InvalidKeyBlob + ); + } +} diff --git a/core/crates/kms-engine/src/lib.rs b/core/crates/kms-engine/src/lib.rs index ff89086..4b023d8 100644 --- a/core/crates/kms-engine/src/lib.rs +++ b/core/crates/kms-engine/src/lib.rs @@ -5,7 +5,6 @@ mod alg; mod blob; mod error; mod kdf; -#[cfg(feature = "pqc")] mod kem; mod root; mod sign; @@ -99,10 +98,7 @@ impl Engine { (k, Vec::new()) } AlgKind::Signature => sign::generate(alg)?, - #[cfg(feature = "pqc")] AlgKind::Kem => kem::generate(alg)?, - #[cfg(not(feature = "pqc"))] - AlgKind::Kem => return Err(Error::CapabilityDisabled), }; let header = blob::Header { @@ -198,7 +194,6 @@ impl Engine { Ok(out) } - #[cfg(feature = "pqc")] pub fn encapsulate( &self, alg: Alg, @@ -211,7 +206,6 @@ impl Engine { kem::encapsulate(alg, encapsulation_key) } - #[cfg(feature = "pqc")] pub fn decapsulate(&self, blob: &[u8], ciphertext: &[u8]) -> Result>, Error> { let k = self.open_for(blob, AlgKind::Kem, kms_wire::usage::DECAPSULATE)?; kem::decapsulate(k.alg, &k.secret, ciphertext) @@ -219,7 +213,13 @@ impl Engine { } #[cfg(test)] -#[allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)] +#[allow( + clippy::unwrap_used, + clippy::expect_used, + clippy::indexing_slicing, + clippy::arithmetic_side_effects, + clippy::panic +)] mod tests { use super::{Alg, Engine, Error, RootKey, Usage}; use kms_wire::usage; @@ -369,29 +369,79 @@ mod tests { signature_roundtrip(&e, Alg::MlDsa87); } - #[cfg(feature = "pqc")] + fn kem_roundtrip(e: &Engine, alg: Alg) { + let u = Usage::from_bits(usage::DECAPSULATE).unwrap(); + let k = e.generate_key(alg, u, "kem").unwrap(); + assert_eq!(k.public_key.len(), alg.public_key_len().unwrap()); + let (ct, ss1) = e.encapsulate(alg, &k.public_key).unwrap(); + assert_eq!(ct.len(), alg.kem_ciphertext_len().unwrap()); + let ss2 = e + .decapsulate(&k.blob, &ct) + .unwrap_or_else(|err| panic!("{alg:?}: decapsulate {err}")); + assert_eq!(ss1.as_slice(), ss2.as_slice()); + assert_eq!(ss1.len(), 32); + let (ct2, ss3) = e.encapsulate(alg, &k.public_key).unwrap(); + assert_ne!(ct, ct2); + assert_ne!(ss1.as_slice(), ss3.as_slice()); + let mut bad = ct.clone(); + bad[ct.len() - 1] ^= 1; + match e.decapsulate(&k.blob, &bad) { + Ok(ss) => assert_ne!(ss1.as_slice(), ss.as_slice()), + Err(err) => assert_eq!(err, Error::CryptoFailure), + } + assert_eq!(e.decapsulate(&k.blob, &ct[1..]), Err(Error::CryptoFailure)); + assert_eq!( + e.encapsulate(alg, &k.public_key[1..]), + Err(Error::InvalidArgument) + ); + assert_eq!(e.sign(&k.blob, b"m"), Err(Error::InvalidArgument)); + assert_eq!( + e.aead_encrypt(&k.blob, b"x", b""), + Err(Error::InvalidArgument) + ); + let other = e.generate_key(alg, u, "other").unwrap(); + let ss4 = e.decapsulate(&other.blob, &ct); + assert!(ss4.map_or(true, |s| s.as_slice() != ss1.as_slice())); + } + #[test] - fn pqc_kem_roundtrip() { + fn classical_kem() { let e = engine(); - for alg in [Alg::MlKem768, Alg::MlKem1024] { - let u = Usage::from_bits(usage::DECAPSULATE).unwrap(); - let k = e.generate_key(alg, u, "kem").unwrap(); - assert_eq!(k.public_key.len(), alg.public_key_len().unwrap()); - let (ct, ss1) = e.encapsulate(alg, &k.public_key).unwrap(); - let ss2 = e.decapsulate(&k.blob, &ct).unwrap(); - assert_eq!(ss1.as_slice(), ss2.as_slice()); - assert_eq!(ss1.len(), 32); - let mut bad = ct.clone(); - bad[0] ^= 1; - let ss3 = e.decapsulate(&k.blob, &bad).unwrap(); - assert_ne!(ss1.as_slice(), ss3.as_slice()); - assert_eq!(e.decapsulate(&k.blob, &ct[1..]), Err(Error::CryptoFailure)); - assert_eq!( - e.encapsulate(alg, &k.public_key[1..]), - Err(Error::InvalidArgument) - ); - assert_eq!(e.sign(&k.blob, b"m"), Err(Error::InvalidArgument)); + kem_roundtrip(&e, Alg::DhkemP256); + let enc = Usage::from_bits(usage::ENCRYPT).unwrap(); + assert_eq!( + e.generate_key(Alg::DhkemP256, enc, "").unwrap_err(), + Error::InvalidArgument + ); + assert_eq!( + e.encapsulate(Alg::Aes256Gcm, &[0u8; 65]), + Err(Error::InvalidArgument) + ); + } + + #[test] + fn pqc_kems_follow_capability() { + let e = engine(); + for alg in [Alg::MlKem768, Alg::MlKem1024, Alg::HybridP256MlKem768] { + if cfg!(feature = "pqc") { + kem_roundtrip(&e, alg); + } else { + let u = Usage::from_bits(usage::DECAPSULATE).unwrap(); + assert_eq!( + e.generate_key(alg, u, "").unwrap_err(), + Error::CapabilityDisabled + ); + assert_eq!( + e.encapsulate(alg, &[0u8; 65]), + Err(Error::CapabilityDisabled) + ); + } } + let off = Engine::new(RootKey::generate().unwrap(), false); + assert_eq!( + off.encapsulate(Alg::HybridP256MlKem768, &[0u8; 1249]), + Err(Error::CapabilityDisabled) + ); } #[test] diff --git a/core/crates/kms-engine/src/sign.rs b/core/crates/kms-engine/src/sign.rs index e85aa16..b12f856 100644 --- a/core/crates/kms-engine/src/sign.rs +++ b/core/crates/kms-engine/src/sign.rs @@ -54,7 +54,11 @@ pub(crate) fn generate(alg: Alg) -> Result { } #[cfg(not(feature = "pqc"))] Alg::MlDsa65 | Alg::MlDsa87 => Err(Error::CapabilityDisabled), - Alg::Aes256Gcm | Alg::MlKem768 | Alg::MlKem1024 => Err(Error::InvalidArgument), + Alg::Aes256Gcm + | Alg::DhkemP256 + | Alg::MlKem768 + | Alg::MlKem1024 + | Alg::HybridP256MlKem768 => Err(Error::InvalidArgument), } } @@ -94,7 +98,11 @@ pub(crate) fn sign(alg: Alg, pkcs8: &[u8], message: &[u8]) -> Result, Er } #[cfg(not(feature = "pqc"))] Alg::MlDsa65 | Alg::MlDsa87 => Err(Error::CapabilityDisabled), - Alg::Aes256Gcm | Alg::MlKem768 | Alg::MlKem1024 => Err(Error::InvalidArgument), + Alg::Aes256Gcm + | Alg::DhkemP256 + | Alg::MlKem768 + | Alg::MlKem1024 + | Alg::HybridP256MlKem768 => Err(Error::InvalidArgument), } } @@ -117,7 +125,11 @@ pub(crate) fn verify( Alg::MlDsa87 => &ML_DSA_87, #[cfg(not(feature = "pqc"))] Alg::MlDsa65 | Alg::MlDsa87 => return Err(Error::CapabilityDisabled), - Alg::Aes256Gcm | Alg::MlKem768 | Alg::MlKem1024 => return Err(Error::InvalidArgument), + Alg::Aes256Gcm + | Alg::DhkemP256 + | Alg::MlKem768 + | Alg::MlKem1024 + | Alg::HybridP256MlKem768 => return Err(Error::InvalidArgument), }; Ok(UnparsedPublicKey::new(va, public_key) .verify(message, signature) diff --git a/core/crates/kms-engine/src/usage.rs b/core/crates/kms-engine/src/usage.rs index 335ed90..ebb340d 100644 --- a/core/crates/kms-engine/src/usage.rs +++ b/core/crates/kms-engine/src/usage.rs @@ -24,11 +24,21 @@ impl Usage { flags != 0 && self.0 & flags == flags } + #[must_use] + pub fn is_subset_of(self, other: Self) -> bool { + self.0 & !other.0 == 0 + } + #[must_use] pub fn allowed_for(alg: Alg) -> u32 { match alg.kind() { AlgKind::Symmetric => { - wire::ENCRYPT | wire::DECRYPT | wire::WRAP | wire::UNWRAP | wire::DERIVE + wire::ENCRYPT + | wire::DECRYPT + | wire::WRAP + | wire::UNWRAP + | wire::DERIVE + | wire::EXPORT } AlgKind::Signature => wire::SIGN | wire::VERIFY, AlgKind::Kem => wire::DECAPSULATE | wire::DERIVE, diff --git a/core/crates/kms-wire/src/lib.rs b/core/crates/kms-wire/src/lib.rs index f3d2896..423ddad 100644 --- a/core/crates/kms-wire/src/lib.rs +++ b/core/crates/kms-wire/src/lib.rs @@ -17,10 +17,12 @@ pub mod alg { pub const ECDSA_P256_SHA256: u16 = 0x0010; pub const ECDSA_P384_SHA384: u16 = 0x0011; pub const ED25519: u16 = 0x0020; + pub const DHKEM_P256: u16 = 0x0030; pub const ML_DSA_65: u16 = 0x0100; pub const ML_DSA_87: u16 = 0x0101; pub const ML_KEM_768: u16 = 0x0200; pub const ML_KEM_1024: u16 = 0x0201; + pub const HYBRID_P256_ML_KEM_768: u16 = 0x0210; } pub mod usage { @@ -32,7 +34,8 @@ pub mod usage { pub const UNWRAP: u32 = 1 << 5; pub const DERIVE: u32 = 1 << 6; pub const DECAPSULATE: u32 = 1 << 7; - pub const ALL: u32 = (1 << 8) - 1; + pub const EXPORT: u32 = 1 << 8; + pub const ALL: u32 = (1 << 9) - 1; } pub mod cap { diff --git a/host/internal/wire/msg.go b/host/internal/wire/msg.go index 86ec32b..c10b9a1 100644 --- a/host/internal/wire/msg.go +++ b/host/internal/wire/msg.go @@ -15,14 +15,16 @@ const ( ) const ( - AlgAES256GCM uint16 = 0x0001 - AlgECDSAP256SHA256 uint16 = 0x0010 - AlgECDSAP384SHA384 uint16 = 0x0011 - AlgEd25519 uint16 = 0x0020 - AlgMLDSA65 uint16 = 0x0100 - AlgMLDSA87 uint16 = 0x0101 - AlgMLKEM768 uint16 = 0x0200 - AlgMLKEM1024 uint16 = 0x0201 + AlgAES256GCM uint16 = 0x0001 + AlgECDSAP256SHA256 uint16 = 0x0010 + AlgECDSAP384SHA384 uint16 = 0x0011 + AlgEd25519 uint16 = 0x0020 + AlgDHKEMP256 uint16 = 0x0030 + AlgMLDSA65 uint16 = 0x0100 + AlgMLDSA87 uint16 = 0x0101 + AlgMLKEM768 uint16 = 0x0200 + AlgMLKEM1024 uint16 = 0x0201 + AlgHybridP256MLKEM768 uint16 = 0x0210 ) const ( @@ -34,7 +36,8 @@ const ( UsageUnwrap uint32 = 1 << 5 UsageDerive uint32 = 1 << 6 UsageDecapsulate uint32 = 1 << 7 - UsageAll uint32 = (1 << 8) - 1 + UsageExport uint32 = 1 << 8 + UsageAll uint32 = (1 << 9) - 1 ) const ( From e805442965f5341465da6dbc26575405bc8a8913 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:52:52 +0900 Subject: [PATCH 04/48] =?UTF-8?q?=ED=82=A4=20=EB=82=B4=EB=B3=B4=EB=82=B4?= =?UTF-8?q?=EA=B8=B0=EC=99=80=20=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EA=B5=AC=ED=98=84=20-=20?= =?UTF-8?q?=EC=88=98=EC=8B=A0=EC=9E=90=20KEM=20=EA=B3=B5=EA=B0=9C=ED=82=A4?= =?UTF-8?q?=EB=A1=9C=20=EC=BA=A1=EC=8A=90=ED=99=94=ED=95=9C=20=EA=B3=B5?= =?UTF-8?q?=EC=9C=A0=20=EB=B9=84=EB=B0=80=EC=97=90=EC=84=9C=20=ED=8C=8C?= =?UTF-8?q?=EC=83=9D=ED=95=9C=20AES-256-GCM=EC=9C=BC=EB=A1=9C=20=ED=82=A4?= =?UTF-8?q?=20=EC=9E=AC=EB=A3=8C=20=EB=B4=89=EC=9D=B8=20-=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=ED=97=A4=EB=8D=94(kem=20alg,=20alg,=20usa?= =?UTF-8?q?ge,=20key=20id,=20label,=20context,=20salt,=20enc)=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EC=9D=B8=EC=A6=9D=20-=20=EB=82=B4=EB=B3=B4?= =?UTF-8?q?=EB=82=B4=EA=B8=B0=EB=8A=94=20EXPORT=20usage=EC=99=80=20?= =?UTF-8?q?=EB=B6=80=EB=B6=84=EC=A7=91=ED=95=A9=20usage=EB=A7=8C=20?= =?UTF-8?q?=ED=97=88=EC=9A=A9,=20=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0?= =?UTF-8?q?=EB=8A=94=20context=20=EC=9D=BC=EC=B9=98=EB=A5=BC=20=EA=B0=95?= =?UTF-8?q?=EC=A0=9C=20-=20=EB=8C=80=EC=B9=AD=20=ED=82=A4=EB=A7=8C=20?= =?UTF-8?q?=EA=B5=90=ED=99=98=20=EB=8C=80=EC=83=81=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=9C=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/crates/kms-engine/src/export.rs | 175 ++++++++++++++++++++ core/crates/kms-engine/src/lib.rs | 236 +++++++++++++++++++++++++++ core/crates/kms-wire/src/msg.rs | 2 + 3 files changed, 413 insertions(+) create mode 100644 core/crates/kms-engine/src/export.rs diff --git a/core/crates/kms-engine/src/export.rs b/core/crates/kms-engine/src/export.rs new file mode 100644 index 0000000..03432c9 --- /dev/null +++ b/core/crates/kms-engine/src/export.rs @@ -0,0 +1,175 @@ +use aws_lc_rs::aead::Aad; +use aws_lc_rs::constant_time::verify_slices_are_equal; +use kms_wire::msg::{KEY_ID_LEN, MAX_CONTEXT, MAX_LABEL, MAX_PACKAGE}; +use kms_wire::{Reader, Writer}; +use zeroize::Zeroizing; + +use crate::alg::{Alg, AlgKind}; +use crate::error::Error; +use crate::kdf::{self, SALT_LEN, TAG_LEN}; +use crate::kem; +use crate::usage::Usage; + +const VERSION: u8 = 1; +const INFO: &[u8] = b"pairier/kms/key-export/v1"; + +pub(crate) struct Export<'a> { + pub(crate) kem_alg: Alg, + pub(crate) alg: Alg, + pub(crate) usage: Usage, + pub(crate) key_id: [u8; KEY_ID_LEN], + pub(crate) label: &'a str, + pub(crate) context: &'a [u8], +} + +pub(crate) struct Imported { + pub(crate) alg: Alg, + pub(crate) usage: Usage, + pub(crate) key_id: [u8; KEY_ID_LEN], + pub(crate) label: String, + pub(crate) secret: Zeroizing>, +} + +struct ParsedHeader<'a> { + kem_alg: Alg, + alg: Alg, + usage: Usage, + key_id: [u8; KEY_ID_LEN], + label: &'a str, + context: &'a [u8], + salt: [u8; SALT_LEN], + enc: &'a [u8], +} + +fn encode_header(e: &Export<'_>, salt: &[u8; SALT_LEN], enc: &[u8]) -> Result, Error> { + let mut w = Writer::new(); + w.write_u8(VERSION).map_err(|_| Error::Internal)?; + w.write_u16(e.kem_alg.wire_id()) + .map_err(|_| Error::Internal)?; + w.write_u16(e.alg.wire_id()).map_err(|_| Error::Internal)?; + w.write_u32(e.usage.bits()).map_err(|_| Error::Internal)?; + w.write_bytes(&e.key_id).map_err(|_| Error::Internal)?; + w.write_str(e.label).map_err(|_| Error::Internal)?; + w.write_bytes(e.context).map_err(|_| Error::Internal)?; + w.write_bytes(salt).map_err(|_| Error::Internal)?; + w.write_bytes(enc).map_err(|_| Error::Internal)?; + Ok(w.finish()) +} + +fn parse_header<'a>(r: &mut Reader<'a>) -> Result, Error> { + let version = r.read_u8().map_err(|_| Error::InvalidArgument)?; + if version != VERSION { + return Err(Error::InvalidArgument); + } + let kem_alg = r.read_u16().map_err(|_| Error::InvalidArgument)?; + let alg = r.read_u16().map_err(|_| Error::InvalidArgument)?; + let usage = r.read_u32().map_err(|_| Error::InvalidArgument)?; + let key_id = r + .read_array::() + .map_err(|_| Error::InvalidArgument)?; + let label = r.read_str(MAX_LABEL).map_err(|_| Error::InvalidArgument)?; + let context = r + .read_bytes(MAX_CONTEXT) + .map_err(|_| Error::InvalidArgument)?; + let salt = r + .read_array::() + .map_err(|_| Error::InvalidArgument)?; + let kem_alg = Alg::try_from(kem_alg).map_err(|_| Error::InvalidArgument)?; + let enc_len = kem_alg.kem_ciphertext_len().ok_or(Error::InvalidArgument)?; + let enc = r.read_bytes(enc_len).map_err(|_| Error::InvalidArgument)?; + if enc.len() != enc_len { + return Err(Error::InvalidArgument); + } + let alg = Alg::try_from(alg).map_err(|_| Error::InvalidArgument)?; + let usage = Usage::from_bits(usage).map_err(|_| Error::InvalidArgument)?; + usage.check_for(alg)?; + Ok(ParsedHeader { + kem_alg, + alg, + usage, + key_id, + label, + context, + salt, + enc, + }) +} + +pub(crate) fn seal( + e: &Export<'_>, + recipient_public_key: &[u8], + secret: &[u8], +) -> Result, Error> { + if e.kem_alg.kind() != AlgKind::Kem { + return Err(Error::InvalidArgument); + } + if e.label.len() > MAX_LABEL { + return Err(Error::InvalidArgument); + } + if e.context.len() > MAX_CONTEXT { + return Err(Error::LimitExceeded); + } + let (enc, ss) = kem::encapsulate(e.kem_alg, recipient_public_key)?; + let salt = kdf::random_salt()?; + let aad = encode_header(e, &salt, &enc)?; + let d = kdf::derive_aead(&ss, &salt, &[INFO, &e.key_id])?; + let mut body = Zeroizing::new(secret.to_vec()); + d.key + .seal_in_place_append_tag(d.nonce, Aad::from(aad.as_slice()), &mut *body) + .map_err(|_| Error::Internal)?; + let total = aad + .len() + .checked_add(body.len()) + .ok_or(Error::LimitExceeded)?; + if total > MAX_PACKAGE { + return Err(Error::LimitExceeded); + } + let mut out = aad; + out.extend_from_slice(&body); + Ok(out) +} + +pub(crate) fn open( + kem_alg: Alg, + kem_secret: &[u8], + package: &[u8], + expected_context: &[u8], +) -> Result { + if package.len() > MAX_PACKAGE || expected_context.len() > MAX_CONTEXT { + return Err(Error::LimitExceeded); + } + let mut r = Reader::new(package); + let h = parse_header(&mut r)?; + if h.kem_alg != kem_alg { + return Err(Error::InvalidArgument); + } + if verify_slices_are_equal(h.context, expected_context).is_err() { + return Err(Error::InvalidArgument); + } + let header_len = package + .len() + .checked_sub(r.remaining()) + .ok_or(Error::InvalidArgument)?; + let (aad, body) = package + .split_at_checked(header_len) + .ok_or(Error::InvalidArgument)?; + if body.len() < TAG_LEN { + return Err(Error::InvalidArgument); + } + let ss = kem::decapsulate(kem_alg, kem_secret, h.enc)?; + let d = kdf::derive_aead(&ss, &h.salt, &[INFO, &h.key_id])?; + let mut buf = Zeroizing::new(body.to_vec()); + let n = d + .key + .open_in_place(d.nonce, Aad::from(aad), buf.as_mut_slice()) + .map_err(|_| Error::CryptoFailure)? + .len(); + buf.truncate(n); + Ok(Imported { + alg: h.alg, + usage: h.usage, + key_id: h.key_id, + label: h.label.to_owned(), + secret: buf, + }) +} diff --git a/core/crates/kms-engine/src/lib.rs b/core/crates/kms-engine/src/lib.rs index 4b023d8..0795c49 100644 --- a/core/crates/kms-engine/src/lib.rs +++ b/core/crates/kms-engine/src/lib.rs @@ -4,6 +4,7 @@ mod aead; mod alg; mod blob; mod error; +mod export; mod kdf; mod kem; mod root; @@ -41,6 +42,15 @@ pub struct KeyInfo { pub label: String, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ImportedKey { + pub key_id: [u8; KEY_ID_LEN], + pub blob: Vec, + pub alg: Alg, + pub usage: Usage, + pub label: String, +} + #[derive(Debug)] pub struct Engine { root: RootKey, @@ -206,6 +216,68 @@ impl Engine { kem::encapsulate(alg, encapsulation_key) } + pub fn export_key( + &self, + blob: &[u8], + kem_alg: Alg, + recipient_public_key: &[u8], + usage: Usage, + context: &[u8], + ) -> Result, Error> { + self.check_enabled(kem_alg)?; + if kem_alg.kind() != AlgKind::Kem { + return Err(Error::InvalidArgument); + } + if context.len() > kms_wire::msg::MAX_CONTEXT { + return Err(Error::LimitExceeded); + } + let k = self.open_for(blob, AlgKind::Symmetric, kms_wire::usage::EXPORT)?; + usage.check_for(k.alg)?; + if !usage.is_subset_of(k.usage) { + return Err(Error::UsageDenied); + } + let e = export::Export { + kem_alg, + alg: k.alg, + usage, + key_id: k.key_id, + label: &k.label, + context, + }; + export::seal(&e, recipient_public_key, &k.secret) + } + + pub fn import_key( + &self, + kem_blob: &[u8], + package: &[u8], + context: &[u8], + ) -> Result { + if package.len() > kms_wire::msg::MAX_PACKAGE || context.len() > kms_wire::msg::MAX_CONTEXT + { + return Err(Error::LimitExceeded); + } + let k = self.open_for(kem_blob, AlgKind::Kem, kms_wire::usage::DECAPSULATE)?; + let imported = export::open(k.alg, &k.secret, package, context)?; + if imported.alg.kind() != AlgKind::Symmetric || imported.secret.len() != aead::KEY_LEN { + return Err(Error::InvalidArgument); + } + let header = blob::Header { + alg: imported.alg, + usage: imported.usage, + key_id: imported.key_id, + label: &imported.label, + }; + let blob = blob::seal(&self.root, &header, &imported.secret)?; + Ok(ImportedKey { + key_id: imported.key_id, + blob, + alg: imported.alg, + usage: imported.usage, + label: imported.label, + }) + } + pub fn decapsulate(&self, blob: &[u8], ciphertext: &[u8]) -> Result>, Error> { let k = self.open_for(blob, AlgKind::Kem, kms_wire::usage::DECAPSULATE)?; kem::decapsulate(k.alg, &k.secret, ciphertext) @@ -444,6 +516,170 @@ mod tests { ); } + fn exchange_roundtrip(sender: &Engine, recipient: &Engine, kem_alg: Alg) { + let decap = Usage::from_bits(usage::DECAPSULATE).unwrap(); + let kem = recipient.generate_key(kem_alg, decap, "kem").unwrap(); + let full = Usage::from_bits(usage::ENCRYPT | usage::DECRYPT | usage::EXPORT).unwrap(); + let k = sender.generate_key(Alg::Aes256Gcm, full, "shared").unwrap(); + let ct = sender.aead_encrypt(&k.blob, b"payload", b"aad").unwrap(); + let grant = Usage::from_bits(usage::DECRYPT).unwrap(); + let ctx = b"from=a;to=b;t=1"; + let pkg = sender + .export_key(&k.blob, kem_alg, &kem.public_key, grant, ctx) + .unwrap(); + assert!(pkg.len() <= kms_wire::msg::MAX_PACKAGE); + + let imp = recipient.import_key(&kem.blob, &pkg, ctx).unwrap(); + assert_eq!(imp.key_id, k.key_id); + assert_eq!(imp.alg, Alg::Aes256Gcm); + assert_eq!(imp.usage, grant); + assert_eq!(imp.label, "shared"); + assert_eq!( + recipient + .aead_decrypt(&imp.blob, &ct, b"aad") + .unwrap() + .as_slice(), + b"payload" + ); + assert_eq!( + recipient.aead_encrypt(&imp.blob, b"x", b""), + Err(Error::UsageDenied) + ); + assert_eq!( + recipient.export_key(&imp.blob, kem_alg, &kem.public_key, grant, ctx), + Err(Error::UsageDenied) + ); + let info = recipient.inspect(&imp.blob).unwrap(); + assert_eq!(info.key_id, k.key_id); + assert_eq!(info.usage, grant); + + let pkg2 = sender + .export_key(&k.blob, kem_alg, &kem.public_key, grant, ctx) + .unwrap(); + assert_ne!(pkg, pkg2); + + assert_eq!( + recipient.import_key(&kem.blob, &pkg, b"from=a;to=c;t=1"), + Err(Error::InvalidArgument) + ); + assert_eq!( + recipient.import_key(&kem.blob, &pkg, b""), + Err(Error::InvalidArgument) + ); + let other = recipient.generate_key(kem_alg, decap, "kem2").unwrap(); + assert_eq!( + recipient.import_key(&other.blob, &pkg, ctx), + Err(Error::CryptoFailure) + ); + assert_eq!( + sender.import_key(&kem.blob, &pkg, ctx), + Err(Error::InvalidKeyBlob) + ); + for i in 0..pkg.len() { + let mut t = pkg.clone(); + t[i] ^= 0x01; + assert!( + recipient.import_key(&kem.blob, &t, ctx).is_err(), + "byte {i}" + ); + } + assert!( + recipient + .import_key(&kem.blob, &pkg[..pkg.len() - 1], ctx) + .is_err() + ); + let mut extended = pkg.clone(); + extended.push(0); + assert!(recipient.import_key(&kem.blob, &extended, ctx).is_err()); + assert_eq!( + recipient.import_key(&kem.blob, &[], ctx), + Err(Error::InvalidArgument) + ); + } + + #[test] + fn export_import_classical() { + let a = engine(); + let b = engine(); + exchange_roundtrip(&a, &b, Alg::DhkemP256); + } + + #[cfg(feature = "pqc")] + #[test] + fn export_import_pqc() { + let a = engine(); + let b = engine(); + for alg in [Alg::MlKem768, Alg::MlKem1024, Alg::HybridP256MlKem768] { + exchange_roundtrip(&a, &b, alg); + } + } + + #[test] + fn export_enforces_usage_and_kinds() { + let a = engine(); + let b = engine(); + let decap = Usage::from_bits(usage::DECAPSULATE).unwrap(); + let kem = b.generate_key(Alg::DhkemP256, decap, "").unwrap(); + let ctx = b"ctx"; + + let no_export = Usage::from_bits(usage::ENCRYPT | usage::DECRYPT).unwrap(); + let k = a.generate_key(Alg::Aes256Gcm, no_export, "").unwrap(); + let grant = Usage::from_bits(usage::DECRYPT).unwrap(); + assert_eq!( + a.export_key(&k.blob, Alg::DhkemP256, &kem.public_key, grant, ctx), + Err(Error::UsageDenied) + ); + + let exportable = Usage::from_bits(usage::DECRYPT | usage::EXPORT).unwrap(); + let k = a.generate_key(Alg::Aes256Gcm, exportable, "").unwrap(); + let escalate = Usage::from_bits(usage::ENCRYPT | usage::DECRYPT).unwrap(); + assert_eq!( + a.export_key(&k.blob, Alg::DhkemP256, &kem.public_key, escalate, ctx), + Err(Error::UsageDenied) + ); + let wrong_kind = Usage::from_bits(usage::SIGN).unwrap(); + assert_eq!( + a.export_key(&k.blob, Alg::DhkemP256, &kem.public_key, wrong_kind, ctx), + Err(Error::InvalidArgument) + ); + assert_eq!( + a.export_key(&k.blob, Alg::Aes256Gcm, &kem.public_key, grant, ctx), + Err(Error::InvalidArgument) + ); + assert_eq!( + a.export_key(&k.blob, Alg::DhkemP256, &kem.public_key[1..], grant, ctx), + Err(Error::InvalidArgument) + ); + let big = vec![0u8; kms_wire::msg::MAX_CONTEXT + 1]; + assert_eq!( + a.export_key(&k.blob, Alg::DhkemP256, &kem.public_key, grant, &big), + Err(Error::LimitExceeded) + ); + let pkg = a + .export_key(&k.blob, Alg::DhkemP256, &kem.public_key, grant, ctx) + .unwrap(); + assert_eq!(b.import_key(&k.blob, &pkg, ctx), Err(Error::InvalidKeyBlob)); + assert_eq!( + b.import_key(&kem.blob, &pkg, &big), + Err(Error::LimitExceeded) + ); + + let signer = a + .generate_key(Alg::Ed25519, Usage::from_bits(usage::SIGN).unwrap(), "") + .unwrap(); + assert_eq!( + a.export_key(&signer.blob, Alg::DhkemP256, &kem.public_key, grant, ctx), + Err(Error::InvalidArgument) + ); + + let off = Engine::new(RootKey::generate().unwrap(), false); + let k2 = off.generate_key(Alg::Aes256Gcm, exportable, "").unwrap(); + assert_eq!( + off.export_key(&k2.blob, Alg::HybridP256MlKem768, &[0u8; 1249], grant, ctx), + Err(Error::CapabilityDisabled) + ); + } + #[test] fn random_is_bounded_and_fresh() { let e = engine(); diff --git a/core/crates/kms-wire/src/msg.rs b/core/crates/kms-wire/src/msg.rs index ba7968f..62be81c 100644 --- a/core/crates/kms-wire/src/msg.rs +++ b/core/crates/kms-wire/src/msg.rs @@ -15,6 +15,8 @@ pub const MAX_PLAINTEXT: usize = 64 * 1024; pub const MAX_CIPHERTEXT: usize = MAX_PLAINTEXT + 64; pub const MAX_MESSAGE: usize = 64 * 1024; pub const MAX_RANDOM: u32 = 1024; +pub const MAX_CONTEXT: usize = 1024; +pub const MAX_PACKAGE: usize = 8 * 1024; pub trait Message<'a>: Sized { const OP: Op; From a26ad91de13c0e80624f5f1d3fde73020a60135c Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:58:33 +0900 Subject: [PATCH 05/48] =?UTF-8?q?=EC=99=80=EC=9D=B4=EC=96=B4=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=86=A0=EC=BD=9C=EC=97=90=20=ED=82=A4=20=EA=B5=90?= =?UTF-8?q?=ED=99=98=20op=20=EC=B6=94=EA=B0=80=20-=20KeyInspect=200x0011,?= =?UTF-8?q?=20KeyExport=200x0030,=20KeyImport=200x0031=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=EC=99=80=20=ED=95=9C=EB=8F=84=20(kms-wire,?= =?UTF-8?q?=20Go=20wire)=20-=20=EC=A0=81=ED=95=A9=EC=84=B1=20=EB=B2=A1?= =?UTF-8?q?=ED=84=B0=20=EC=83=9D=EC=84=B1=EA=B8=B0=EB=A5=BC=20conformance/?= =?UTF-8?q?wire=EC=97=90=20=EB=91=90=EA=B3=A0=20=EB=B2=A1=ED=84=B0=20?= =?UTF-8?q?=EC=9E=AC=EC=83=9D=EC=84=B1=20(frame=2031,=20reject=2036)=20-?= =?UTF-8?q?=20kms-core=20=EC=84=9C=EB=B2=84=20=EB=94=94=EC=8A=A4=ED=8C=A8?= =?UTF-8?q?=EC=B9=98=EC=99=80=20=EB=91=90=20=EC=BD=94=EC=96=B4=20=EA=B0=84?= =?UTF-8?q?=20=EA=B5=90=ED=99=98=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- conformance/wire/gen_v1.py | 123 ++++++++++++ conformance/wire/v1.txt | 13 ++ core/crates/kms-core/src/server.rs | 216 +++++++++++++++++++++- core/crates/kms-wire/src/header.rs | 9 + core/crates/kms-wire/src/msg.rs | 153 +++++++++++++++ core/crates/kms-wire/tests/conformance.rs | 12 +- host/internal/wire/header.go | 6 +- host/internal/wire/msg.go | 169 +++++++++++++++++ 8 files changed, 694 insertions(+), 7 deletions(-) create mode 100644 conformance/wire/gen_v1.py diff --git a/conformance/wire/gen_v1.py b/conformance/wire/gen_v1.py new file mode 100644 index 0000000..492160d --- /dev/null +++ b/conformance/wire/gen_v1.py @@ -0,0 +1,123 @@ +import struct + +MAGIC = b"KWP1" +REQ, RSP = 1, 2 +HELLO, PING, KEYGEN, AEADENC, AEADDEC, SIGN, VERIFY, RANDOM = 0x1, 0x2, 0x10, 0x20, 0x21, 0x22, 0x23, 0x40 +KEYINSPECT, KEYEXPORT, KEYIMPORT = 0x11, 0x30, 0x31 +OK, PROTOCOL, CRYPTO = 0, 1, 6 + + +def hdr(kind, status, op, id_, ln): + return MAGIC + bytes([kind, status]) + struct.pack(">HII", op, id_, ln) + + +def u8(v): return bytes([v]) +def u16(v): return struct.pack(">H", v) +def u32(v): return struct.pack(">I", v) +def u64(v): return struct.pack(">Q", v) +def b(x): return u32(len(x)) + x +def s(x): return b(x.encode()) + + +def frame(kind, op, id_, body, status=0): + return hdr(kind, status, op, id_, len(body)) + body + + +blob = bytes((0xA0 + i) & 0xFF for i in range(40)) +pk = b"\x04" + bytes(range(64)) +sig = bytes(range(0x40, 0x40 + 64)) +ct = bytes((0xC0 + i) & 0xFF for i in range(65)) +kid = bytes(range(16)) +kid2 = bytes(range(16, 32)) +pkg = bytes((0xD0 + i) & 0xFF for i in range(120)) +kempk = b"\x04" + bytes(range(0x80, 0x80 + 64)) + +frames = [ + ("hello_request", frame(REQ, HELLO, 1, u32(1))), + ("hello_response", frame(RSP, HELLO, 1, u32(1) + u64(3) + s("aws-lc-rs") + s("1.18.2"))), + ("ping_request", frame(REQ, PING, 2, b"")), + ("ping_response", frame(RSP, PING, 2, b"")), + ("key_generate_request", frame(REQ, KEYGEN, 3, u16(1) + u32(3) + s("tenant-a/db"))), + ("key_generate_response_sym", frame(RSP, KEYGEN, 3, b(kid) + b(blob) + b(b""))), + ("key_generate_request_pqc", frame(REQ, KEYGEN, 5, u16(0x100) + u32(4) + s(""))), + ("key_generate_response_asym", frame(RSP, KEYGEN, 5, b(kid2) + b(blob) + b(pk))), + ("aead_encrypt_request", frame(REQ, AEADENC, 4, b(blob) + b(b"hello") + b(b"ctx"))), + ("aead_encrypt_response", frame(RSP, AEADENC, 4, b(ct))), + ("aead_decrypt_request", frame(REQ, AEADDEC, 6, b(blob) + b(ct) + b(b"ctx"))), + ("aead_decrypt_response", frame(RSP, AEADDEC, 6, b(b"hello"))), + ("aead_encrypt_request_empty", frame(REQ, AEADENC, 12, b(blob) + b(b"") + b(b""))), + ("sign_request", frame(REQ, SIGN, 7, b(blob) + b(b"message"))), + ("sign_response", frame(RSP, SIGN, 7, b(sig))), + ("verify_request", frame(REQ, VERIFY, 8, u16(0x10) + b(pk) + b(b"message") + b(sig))), + ("verify_response_true", frame(RSP, VERIFY, 8, u8(1))), + ("verify_response_false", frame(RSP, VERIFY, 9, u8(0))), + ("random_request", frame(REQ, RANDOM, 10, u32(32))), + ("random_response", frame(RSP, RANDOM, 10, b(bytes(range(32))))), + ("random_request_max", frame(REQ, RANDOM, 13, u32(1024))), + ("error_response_crypto", hdr(RSP, CRYPTO, AEADENC, 4, 0)), + ("error_response_protocol", hdr(RSP, PROTOCOL, 0xFFFF, 0, 0)), + ("ping_request_max_id", frame(REQ, PING, 0xFFFFFFFF, b"")), + ("key_inspect_request", frame(REQ, KEYINSPECT, 14, b(blob))), + ("key_inspect_response", frame(RSP, KEYINSPECT, 14, u16(1) + u32(0x103) + b(kid) + s("tenant-a/db"))), + ("key_export_request", frame(REQ, KEYEXPORT, 15, b(blob) + u16(0x30) + b(kempk) + u32(2) + b(b"from=a;to=b"))), + ("key_export_response", frame(RSP, KEYEXPORT, 15, b(pkg))), + ("key_import_request", frame(REQ, KEYIMPORT, 16, b(blob) + b(pkg) + b(b"from=a;to=b"))), + ("key_import_response", frame(RSP, KEYIMPORT, 16, b(kid) + b(blob) + u16(1) + u32(2) + s("tenant-a/db"))), + ("key_export_request_hybrid_empty_context", frame(REQ, KEYEXPORT, 17, b(blob) + u16(0x210) + b(bytes(1249)) + u32(0x102) + b(b""))), +] + +reject_headers = [ + ("bad_magic", b"KWP0" + bytes([REQ, 0]) + struct.pack(">HII", PING, 1, 0)), + ("bad_magic_lower", b"kwp1" + bytes([REQ, 0]) + struct.pack(">HII", PING, 1, 0)), + ("bad_kind_zero", hdr(0, 0, PING, 1, 0)), + ("bad_kind_three", hdr(3, 0, PING, 1, 0)), + ("request_with_status", hdr(REQ, 1, PING, 1, 0)), + ("error_with_body", hdr(RSP, 1, PING, 1, 4)), + ("unknown_status", hdr(RSP, 200, PING, 1, 0)), + ("unknown_status_ten", hdr(RSP, 10, PING, 1, 0)), + ("body_too_large", hdr(REQ, 0, PING, 1, (1 << 20) + 1)), + ("body_too_large_max", hdr(REQ, 0, PING, 1, 0xFFFFFFFF)), +] + +reject_bodies = [ + ("hello_request_trailing", HELLO, REQ, u32(1) + b"\x00"), + ("hello_request_truncated", HELLO, REQ, b"\x00\x00\x00"), + ("hello_request_empty", HELLO, REQ, b""), + ("ping_request_nonempty", PING, REQ, b"\x00"), + ("ping_response_nonempty", PING, RSP, b"\x00"), + ("key_generate_response_short_id", KEYGEN, RSP, b(bytes(15)) + b(blob) + b(b"")), + ("key_generate_response_long_id", KEYGEN, RSP, b(bytes(17)) + b(blob) + b(b"")), + ("key_generate_request_label_too_long", KEYGEN, REQ, u16(1) + u32(1) + b(b"a" * 257)), + ("key_generate_request_label_bad_utf8", KEYGEN, REQ, u16(1) + u32(1) + b(b"\xff\xfe")), + ("random_request_too_large", RANDOM, REQ, u32(1025)), + ("random_response_too_large", RANDOM, RSP, b(bytes(1025))), + ("verify_response_bad_bool", VERIFY, RSP, u8(2)), + ("verify_response_bad_bool_ff", VERIFY, RSP, u8(0xFF)), + ("hello_response_bad_utf8", HELLO, RSP, u32(1) + u64(0) + b(b"\xff\xfe") + s("x")), + ("hello_response_name_too_long", HELLO, RSP, u32(1) + u64(0) + b(b"a" * 65) + s("x")), + ("aead_encrypt_request_length_overrun", AEADENC, REQ, b(blob) + u32(10) + b"abc"), + ("aead_encrypt_request_length_too_large", AEADENC, REQ, b(blob) + u32(65537)), + ("aead_encrypt_response_too_large", AEADENC, RSP, u32(65601) + b"\x00" * 65601), + ("sign_request_blob_too_large", SIGN, REQ, u32(16385) + b"\x00" * 16385 + b(b"m")), + ("unknown_op_body", 0x7777, REQ, b""), + ("key_inspect_request_trailing", KEYINSPECT, REQ, b(blob) + b"\x00"), + ("key_inspect_response_label_too_long", KEYINSPECT, RSP, u16(1) + u32(1) + b(kid) + b(b"a" * 257)), + ("key_export_request_context_too_long", KEYEXPORT, REQ, b(blob) + u16(0x30) + b(kempk) + u32(2) + b(b"\x00" * 1025)), + ("key_export_response_package_too_large", KEYEXPORT, RSP, u32(8193) + b"\x00" * 8193), + ("key_import_request_truncated", KEYIMPORT, REQ, b(blob) + u32(5) + b"ab"), + ("key_import_response_short_id", KEYIMPORT, RSP, b(bytes(15)) + b(blob) + u16(1) + u32(2) + s("")), +] + +out = ["# kms wire protocol v1 conformance vectors", "# frame ", "# reject_header ", "# reject_body ", ""] +for n, f in frames: + out.append(f"frame {n} {f.hex()}") +out.append("") +for n, h in reject_headers: + assert len(h) == 16 + out.append(f"reject_header {n} {h.hex()}") +out.append("") +for n, op, kind, body in reject_bodies: + out.append(f"reject_body {n} 0x{op:04x} {'req' if kind == REQ else 'rsp'} {body.hex()}") +import pathlib +pathlib.Path(__file__).resolve().with_name("v1.txt").write_text("\n".join(out) + "\n") +print(len(frames), len(reject_headers), len(reject_bodies)) diff --git a/conformance/wire/v1.txt b/conformance/wire/v1.txt index 9506143..3aee6eb 100644 --- a/conformance/wire/v1.txt +++ b/conformance/wire/v1.txt @@ -27,6 +27,13 @@ frame random_request_max 4b575031010000400000000d0000000400000400 frame error_response_crypto 4b575031020600200000000400000000 frame error_response_protocol 4b5750310201ffff0000000000000000 frame ping_request_max_id 4b57503101000002ffffffff00000000 +frame key_inspect_request 4b575031010000110000000e0000002c00000028a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7 +frame key_inspect_response 4b575031020000110000000e0000002900010000010300000010000102030405060708090a0b0c0d0e0f0000000b74656e616e742d612f6462 +frame key_export_request 4b575031010000300000000f0000008600000028a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c700300000004104808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf000000020000000b66726f6d3d613b746f3d62 +frame key_export_response 4b575031020000300000000f0000007c00000078d0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f4041424344454647 +frame key_import_request 4b5750310100003100000010000000b700000028a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c700000078d0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40414243444546470000000b66726f6d3d613b746f3d62 +frame key_import_response 4b57503102000031000000100000005500000010000102030405060708090a0b0c0d0e0f00000028a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c70001000000020000000b74656e616e742d612f6462 +frame key_export_request_hybrid_empty_context 4b57503101000030000000110000051b00000028a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c70210000004e1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010200000000 reject_header bad_magic 4b575030010000020000000100000000 reject_header bad_magic_lower 6b777031010000020000000100000000 @@ -59,3 +66,9 @@ reject_body aead_encrypt_request_length_too_large 0x0020 req 00000028a0a1a2a3a4a reject_body aead_encrypt_response_too_large 0x0020 rsp 00010041000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 reject_body sign_request_blob_too_large 0x0022 req 000040010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016d reject_body unknown_op_body 0x7777 req +reject_body key_inspect_request_trailing 0x0011 req 00000028a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c700 +reject_body key_inspect_response_label_too_long 0x0011 rsp 00010000000100000010000102030405060708090a0b0c0d0e0f000001016161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161 +reject_body key_export_request_context_too_long 0x0030 req 00000028a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c700300000004104808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf00000002000004010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +reject_body key_export_response_package_too_large 0x0030 rsp 00002001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +reject_body key_import_request_truncated 0x0031 req 00000028a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7000000056162 +reject_body key_import_response_short_id 0x0031 rsp 0000000f00000000000000000000000000000000000028a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c700010000000200000000 diff --git a/core/crates/kms-core/src/server.rs b/core/crates/kms-core/src/server.rs index 831959b..4a6e616 100644 --- a/core/crates/kms-core/src/server.rs +++ b/core/crates/kms-core/src/server.rs @@ -4,8 +4,10 @@ use std::io::{self, Read, Write}; use kms_engine::{Alg, Engine, Usage}; use kms_wire::msg::{ AeadDecryptRequest, AeadDecryptResponse, AeadEncryptRequest, AeadEncryptResponse, HelloRequest, - HelloResponse, KeyGenerateRequest, KeyGenerateResponse, Message, PingRequest, PingResponse, - RandomRequest, RandomResponse, SignRequest, SignResponse, VerifyRequest, VerifyResponse, + HelloResponse, KeyExportRequest, KeyExportResponse, KeyGenerateRequest, KeyGenerateResponse, + KeyImportRequest, KeyImportResponse, KeyInspectRequest, KeyInspectResponse, Message, + PingRequest, PingResponse, RandomRequest, RandomResponse, SignRequest, SignResponse, + VerifyRequest, VerifyResponse, }; use kms_wire::{HEADER_LEN, Header, Kind, Op, PROTOCOL_VERSION, Status}; @@ -109,6 +111,9 @@ impl Server { Op::Hello => self.hello(body)?, Op::Ping => ping(body), Op::KeyGenerate => self.key_generate(body), + Op::KeyInspect => self.key_inspect(body), + Op::KeyExport => self.key_export(body), + Op::KeyImport => self.key_import(body), Op::AeadEncrypt => self.aead_encrypt(body), Op::AeadDecrypt => self.aead_decrypt(body), Op::Sign => self.sign(body), @@ -160,6 +165,49 @@ impl Server { }) } + fn key_inspect(&self, body: &[u8]) -> Result, Status> { + let req = decode::>(body)?; + let info = self.engine.inspect(req.key_blob).map_err(Status::from)?; + encode(&KeyInspectResponse { + alg: info.alg.wire_id(), + usage: info.usage.bits(), + key_id: info.key_id, + label: &info.label, + }) + } + + fn key_export(&self, body: &[u8]) -> Result, Status> { + let req = decode::>(body)?; + let kem_alg = Alg::try_from(req.kem_alg).map_err(Status::from)?; + let usage = Usage::from_bits(req.usage).map_err(Status::from)?; + let package = self + .engine + .export_key( + req.key_blob, + kem_alg, + req.recipient_public_key, + usage, + req.context, + ) + .map_err(Status::from)?; + encode(&KeyExportResponse { package: &package }) + } + + fn key_import(&self, body: &[u8]) -> Result, Status> { + let req = decode::>(body)?; + let imported = self + .engine + .import_key(req.kem_key_blob, req.package, req.context) + .map_err(Status::from)?; + encode(&KeyImportResponse { + key_id: imported.key_id, + key_blob: &imported.blob, + alg: imported.alg.wire_id(), + usage: imported.usage.bits(), + label: &imported.label, + }) + } + fn aead_encrypt(&self, body: &[u8]) -> Result, Status> { let req = decode::>(body)?; let ct = self @@ -230,8 +278,10 @@ mod tests { use kms_engine::{Engine, RootKey}; use kms_wire::msg::{ AeadDecryptRequest, AeadDecryptResponse, AeadEncryptRequest, AeadEncryptResponse, - HelloRequest, HelloResponse, KeyGenerateRequest, KeyGenerateResponse, Message, PingRequest, - RandomRequest, RandomResponse, SignRequest, SignResponse, VerifyRequest, VerifyResponse, + HelloRequest, HelloResponse, KeyExportRequest, KeyExportResponse, KeyGenerateRequest, + KeyGenerateResponse, KeyImportRequest, KeyImportResponse, KeyInspectRequest, + KeyInspectResponse, Message, PingRequest, RandomRequest, RandomResponse, SignRequest, + SignResponse, VerifyRequest, VerifyResponse, }; use kms_wire::{Header, Kind, Op, PROTOCOL_VERSION, Status, alg, usage}; @@ -468,4 +518,162 @@ mod tests { }; assert_eq!(h.status, expect); } + + #[test] + fn key_exchange_between_two_cores() { + let mut a = server(); + let mut b = server(); + hello(&mut a); + hello(&mut b); + let kem_alg = if cfg!(feature = "pqc") { + alg::HYBRID_P256_ML_KEM_768 + } else { + alg::DHKEM_P256 + }; + let (h, body) = call( + &mut b, + 2, + &KeyGenerateRequest { + alg: kem_alg, + usage: usage::DECAPSULATE, + label: "b/kem", + }, + ) + .unwrap(); + assert_eq!(h.status, Status::Ok); + let kem = KeyGenerateResponse::decode_body(&body).unwrap(); + let kem_blob = kem.key_blob.to_vec(); + let kem_pk = kem.public_key.to_vec(); + + let (h, body) = call( + &mut a, + 2, + &KeyGenerateRequest { + alg: alg::AES_256_GCM, + usage: usage::ENCRYPT | usage::DECRYPT | usage::EXPORT, + label: "a/shared", + }, + ) + .unwrap(); + assert_eq!(h.status, Status::Ok); + let k = KeyGenerateResponse::decode_body(&body).unwrap(); + let blob = k.key_blob.to_vec(); + let key_id = k.key_id; + + let (h, body) = call(&mut a, 3, &KeyInspectRequest { key_blob: &blob }).unwrap(); + assert_eq!(h.status, Status::Ok); + let info = KeyInspectResponse::decode_body(&body).unwrap(); + assert_eq!(info.alg, alg::AES_256_GCM); + assert_eq!(info.usage, usage::ENCRYPT | usage::DECRYPT | usage::EXPORT); + assert_eq!(info.key_id, key_id); + assert_eq!(info.label, "a/shared"); + let (h, _) = call(&mut b, 3, &KeyInspectRequest { key_blob: &blob }).unwrap(); + assert_eq!(h.status, Status::InvalidKeyBlob); + + let (h, body) = call( + &mut a, + 4, + &AeadEncryptRequest { + key_blob: &blob, + plaintext: b"for b", + aad: b"", + }, + ) + .unwrap(); + assert_eq!(h.status, Status::Ok); + let ct = AeadEncryptResponse::decode_body(&body) + .unwrap() + .ciphertext + .to_vec(); + + let ctx = b"from=a;to=b"; + let (h, body) = call( + &mut a, + 5, + &KeyExportRequest { + key_blob: &blob, + kem_alg, + recipient_public_key: &kem_pk, + usage: usage::DECRYPT, + context: ctx, + }, + ) + .unwrap(); + assert_eq!(h.status, Status::Ok); + let package = KeyExportResponse::decode_body(&body) + .unwrap() + .package + .to_vec(); + + let (h, _) = call( + &mut b, + 4, + &KeyImportRequest { + kem_key_blob: &kem_blob, + package: &package, + context: b"from=c;to=b", + }, + ) + .unwrap(); + assert_eq!(h.status, Status::InvalidArgument); + + let (h, body) = call( + &mut b, + 5, + &KeyImportRequest { + kem_key_blob: &kem_blob, + package: &package, + context: ctx, + }, + ) + .unwrap(); + assert_eq!(h.status, Status::Ok); + let imported = KeyImportResponse::decode_body(&body).unwrap(); + assert_eq!(imported.key_id, key_id); + assert_eq!(imported.alg, alg::AES_256_GCM); + assert_eq!(imported.usage, usage::DECRYPT); + assert_eq!(imported.label, "a/shared"); + let imported_blob = imported.key_blob.to_vec(); + + let (h, body) = call( + &mut b, + 6, + &AeadDecryptRequest { + key_blob: &imported_blob, + ciphertext: &ct, + aad: b"", + }, + ) + .unwrap(); + assert_eq!(h.status, Status::Ok); + assert_eq!( + AeadDecryptResponse::decode_body(&body).unwrap().plaintext, + b"for b" + ); + let (h, _) = call( + &mut b, + 7, + &AeadEncryptRequest { + key_blob: &imported_blob, + plaintext: b"x", + aad: b"", + }, + ) + .unwrap(); + assert_eq!(h.status, Status::UsageDenied); + + let (h, _) = call( + &mut a, + 6, + &KeyExportRequest { + key_blob: &blob, + kem_alg: alg::AES_256_GCM, + recipient_public_key: &kem_pk, + usage: usage::DECRYPT, + context: ctx, + }, + ) + .unwrap(); + assert_eq!(h.status, Status::InvalidArgument); + } } diff --git a/core/crates/kms-wire/src/header.rs b/core/crates/kms-wire/src/header.rs index bf5ff94..b4fb0a1 100644 --- a/core/crates/kms-wire/src/header.rs +++ b/core/crates/kms-wire/src/header.rs @@ -91,10 +91,13 @@ pub enum Op { Hello = 0x0001, Ping = 0x0002, KeyGenerate = 0x0010, + KeyInspect = 0x0011, AeadEncrypt = 0x0020, AeadDecrypt = 0x0021, Sign = 0x0022, Verify = 0x0023, + KeyExport = 0x0030, + KeyImport = 0x0031, Random = 0x0040, } @@ -104,10 +107,13 @@ impl From for u16 { Op::Hello => 0x0001, Op::Ping => 0x0002, Op::KeyGenerate => 0x0010, + Op::KeyInspect => 0x0011, Op::AeadEncrypt => 0x0020, Op::AeadDecrypt => 0x0021, Op::Sign => 0x0022, Op::Verify => 0x0023, + Op::KeyExport => 0x0030, + Op::KeyImport => 0x0031, Op::Random => 0x0040, } } @@ -121,10 +127,13 @@ impl TryFrom for Op { 0x0001 => Ok(Self::Hello), 0x0002 => Ok(Self::Ping), 0x0010 => Ok(Self::KeyGenerate), + 0x0011 => Ok(Self::KeyInspect), 0x0020 => Ok(Self::AeadEncrypt), 0x0021 => Ok(Self::AeadDecrypt), 0x0022 => Ok(Self::Sign), 0x0023 => Ok(Self::Verify), + 0x0030 => Ok(Self::KeyExport), + 0x0031 => Ok(Self::KeyImport), 0x0040 => Ok(Self::Random), _ => Err(Error::UnknownOp), } diff --git a/core/crates/kms-wire/src/msg.rs b/core/crates/kms-wire/src/msg.rs index 62be81c..faf7e5b 100644 --- a/core/crates/kms-wire/src/msg.rs +++ b/core/crates/kms-wire/src/msg.rs @@ -382,3 +382,156 @@ impl<'a> Message<'a> for RandomResponse<'a> { }) } } + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct KeyInspectRequest<'a> { + pub key_blob: &'a [u8], +} + +impl<'a> Message<'a> for KeyInspectRequest<'a> { + const OP: Op = Op::KeyInspect; + + fn encode(&self, w: &mut Writer) -> Result<(), Error> { + w.write_bytes(self.key_blob) + } + + fn decode(r: &mut Reader<'a>) -> Result { + Ok(Self { + key_blob: r.read_bytes(MAX_KEY_BLOB)?, + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct KeyInspectResponse<'a> { + pub alg: u16, + pub usage: u32, + pub key_id: [u8; KEY_ID_LEN], + pub label: &'a str, +} + +impl<'a> Message<'a> for KeyInspectResponse<'a> { + const OP: Op = Op::KeyInspect; + + fn encode(&self, w: &mut Writer) -> Result<(), Error> { + w.write_u16(self.alg)?; + w.write_u32(self.usage)?; + w.write_bytes(&self.key_id)?; + w.write_str(self.label) + } + + fn decode(r: &mut Reader<'a>) -> Result { + Ok(Self { + alg: r.read_u16()?, + usage: r.read_u32()?, + key_id: r.read_array::()?, + label: r.read_str(MAX_LABEL)?, + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct KeyExportRequest<'a> { + pub key_blob: &'a [u8], + pub kem_alg: u16, + pub recipient_public_key: &'a [u8], + pub usage: u32, + pub context: &'a [u8], +} + +impl<'a> Message<'a> for KeyExportRequest<'a> { + const OP: Op = Op::KeyExport; + + fn encode(&self, w: &mut Writer) -> Result<(), Error> { + w.write_bytes(self.key_blob)?; + w.write_u16(self.kem_alg)?; + w.write_bytes(self.recipient_public_key)?; + w.write_u32(self.usage)?; + w.write_bytes(self.context) + } + + fn decode(r: &mut Reader<'a>) -> Result { + Ok(Self { + key_blob: r.read_bytes(MAX_KEY_BLOB)?, + kem_alg: r.read_u16()?, + recipient_public_key: r.read_bytes(MAX_PUBLIC_KEY)?, + usage: r.read_u32()?, + context: r.read_bytes(MAX_CONTEXT)?, + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct KeyExportResponse<'a> { + pub package: &'a [u8], +} + +impl<'a> Message<'a> for KeyExportResponse<'a> { + const OP: Op = Op::KeyExport; + + fn encode(&self, w: &mut Writer) -> Result<(), Error> { + w.write_bytes(self.package) + } + + fn decode(r: &mut Reader<'a>) -> Result { + Ok(Self { + package: r.read_bytes(MAX_PACKAGE)?, + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct KeyImportRequest<'a> { + pub kem_key_blob: &'a [u8], + pub package: &'a [u8], + pub context: &'a [u8], +} + +impl<'a> Message<'a> for KeyImportRequest<'a> { + const OP: Op = Op::KeyImport; + + fn encode(&self, w: &mut Writer) -> Result<(), Error> { + w.write_bytes(self.kem_key_blob)?; + w.write_bytes(self.package)?; + w.write_bytes(self.context) + } + + fn decode(r: &mut Reader<'a>) -> Result { + Ok(Self { + kem_key_blob: r.read_bytes(MAX_KEY_BLOB)?, + package: r.read_bytes(MAX_PACKAGE)?, + context: r.read_bytes(MAX_CONTEXT)?, + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct KeyImportResponse<'a> { + pub key_id: [u8; KEY_ID_LEN], + pub key_blob: &'a [u8], + pub alg: u16, + pub usage: u32, + pub label: &'a str, +} + +impl<'a> Message<'a> for KeyImportResponse<'a> { + const OP: Op = Op::KeyImport; + + fn encode(&self, w: &mut Writer) -> Result<(), Error> { + w.write_bytes(&self.key_id)?; + w.write_bytes(self.key_blob)?; + w.write_u16(self.alg)?; + w.write_u32(self.usage)?; + w.write_str(self.label) + } + + fn decode(r: &mut Reader<'a>) -> Result { + Ok(Self { + key_id: r.read_array::()?, + key_blob: r.read_bytes(MAX_KEY_BLOB)?, + alg: r.read_u16()?, + usage: r.read_u32()?, + label: r.read_str(MAX_LABEL)?, + }) + } +} diff --git a/core/crates/kms-wire/tests/conformance.rs b/core/crates/kms-wire/tests/conformance.rs index fc457fb..f4abda2 100644 --- a/core/crates/kms-wire/tests/conformance.rs +++ b/core/crates/kms-wire/tests/conformance.rs @@ -8,8 +8,10 @@ use kms_wire::msg::{ AeadDecryptRequest, AeadDecryptResponse, AeadEncryptRequest, AeadEncryptResponse, HelloRequest, - HelloResponse, KeyGenerateRequest, KeyGenerateResponse, Message, PingRequest, PingResponse, - RandomRequest, RandomResponse, SignRequest, SignResponse, VerifyRequest, VerifyResponse, + HelloResponse, KeyExportRequest, KeyExportResponse, KeyGenerateRequest, KeyGenerateResponse, + KeyImportRequest, KeyImportResponse, KeyInspectRequest, KeyInspectResponse, Message, + PingRequest, PingResponse, RandomRequest, RandomResponse, SignRequest, SignResponse, + VerifyRequest, VerifyResponse, }; use kms_wire::{Error, HEADER_LEN, Header, Kind, Op, Status}; @@ -44,6 +46,12 @@ fn roundtrip(op: u16, kind: Kind, body: &[u8]) -> Result, Error> { (Op::Ping, Kind::Response) => PingResponse::decode_body(body)?.encode_body(), (Op::KeyGenerate, Kind::Request) => KeyGenerateRequest::decode_body(body)?.encode_body(), (Op::KeyGenerate, Kind::Response) => KeyGenerateResponse::decode_body(body)?.encode_body(), + (Op::KeyInspect, Kind::Request) => KeyInspectRequest::decode_body(body)?.encode_body(), + (Op::KeyInspect, Kind::Response) => KeyInspectResponse::decode_body(body)?.encode_body(), + (Op::KeyExport, Kind::Request) => KeyExportRequest::decode_body(body)?.encode_body(), + (Op::KeyExport, Kind::Response) => KeyExportResponse::decode_body(body)?.encode_body(), + (Op::KeyImport, Kind::Request) => KeyImportRequest::decode_body(body)?.encode_body(), + (Op::KeyImport, Kind::Response) => KeyImportResponse::decode_body(body)?.encode_body(), (Op::AeadEncrypt, Kind::Request) => AeadEncryptRequest::decode_body(body)?.encode_body(), (Op::AeadEncrypt, Kind::Response) => AeadEncryptResponse::decode_body(body)?.encode_body(), (Op::AeadDecrypt, Kind::Request) => AeadDecryptRequest::decode_body(body)?.encode_body(), diff --git a/host/internal/wire/header.go b/host/internal/wire/header.go index 7dff63c..9db90e5 100644 --- a/host/internal/wire/header.go +++ b/host/internal/wire/header.go @@ -86,16 +86,20 @@ const ( OpHello Op = 0x0001 OpPing Op = 0x0002 OpKeyGenerate Op = 0x0010 + OpKeyInspect Op = 0x0011 OpAeadEncrypt Op = 0x0020 OpAeadDecrypt Op = 0x0021 OpSign Op = 0x0022 OpVerify Op = 0x0023 + OpKeyExport Op = 0x0030 + OpKeyImport Op = 0x0031 OpRandom Op = 0x0040 ) func (o Op) Known() bool { switch o { - case OpHello, OpPing, OpKeyGenerate, OpAeadEncrypt, OpAeadDecrypt, OpSign, OpVerify, OpRandom: + case OpHello, OpPing, OpKeyGenerate, OpKeyInspect, OpAeadEncrypt, OpAeadDecrypt, OpSign, OpVerify, + OpKeyExport, OpKeyImport, OpRandom: return true default: return false diff --git a/host/internal/wire/msg.go b/host/internal/wire/msg.go index c10b9a1..50943f9 100644 --- a/host/internal/wire/msg.go +++ b/host/internal/wire/msg.go @@ -12,6 +12,8 @@ const ( MaxCiphertext = MaxPlaintext + 64 MaxMessage = 64 * 1024 MaxRandom = 1024 + MaxContext = 1024 + MaxPackage = 8 * 1024 ) const ( @@ -355,6 +357,161 @@ func (m *RandomResponse) Decode(r *Reader) (err error) { return err } +type KeyInspectRequest struct { + KeyBlob []byte +} + +func (*KeyInspectRequest) Op() Op { return OpKeyInspect } + +func (m *KeyInspectRequest) Encode(w *Writer) { w.Bytes(m.KeyBlob) } + +func (m *KeyInspectRequest) Decode(r *Reader) (err error) { + m.KeyBlob, err = r.Bytes(MaxKeyBlob) + return err +} + +type KeyInspectResponse struct { + Alg uint16 + Usage uint32 + KeyID [KeyIDLen]byte + Label string +} + +func (*KeyInspectResponse) Op() Op { return OpKeyInspect } + +func (m *KeyInspectResponse) Encode(w *Writer) { + w.U16(m.Alg) + w.U32(m.Usage) + w.Bytes(m.KeyID[:]) + w.String(m.Label) +} + +func (m *KeyInspectResponse) Decode(r *Reader) (err error) { + if m.Alg, err = r.U16(); err != nil { + return err + } + if m.Usage, err = r.U32(); err != nil { + return err + } + id, err := r.Array(KeyIDLen) + if err != nil { + return err + } + copy(m.KeyID[:], id) + m.Label, err = r.String(MaxLabel) + return err +} + +type KeyExportRequest struct { + KeyBlob []byte + KemAlg uint16 + RecipientPublicKey []byte + Usage uint32 + Context []byte +} + +func (*KeyExportRequest) Op() Op { return OpKeyExport } + +func (m *KeyExportRequest) Encode(w *Writer) { + w.Bytes(m.KeyBlob) + w.U16(m.KemAlg) + w.Bytes(m.RecipientPublicKey) + w.U32(m.Usage) + w.Bytes(m.Context) +} + +func (m *KeyExportRequest) Decode(r *Reader) (err error) { + if m.KeyBlob, err = r.Bytes(MaxKeyBlob); err != nil { + return err + } + if m.KemAlg, err = r.U16(); err != nil { + return err + } + if m.RecipientPublicKey, err = r.Bytes(MaxPublicKey); err != nil { + return err + } + if m.Usage, err = r.U32(); err != nil { + return err + } + m.Context, err = r.Bytes(MaxContext) + return err +} + +type KeyExportResponse struct { + Package []byte +} + +func (*KeyExportResponse) Op() Op { return OpKeyExport } + +func (m *KeyExportResponse) Encode(w *Writer) { w.Bytes(m.Package) } + +func (m *KeyExportResponse) Decode(r *Reader) (err error) { + m.Package, err = r.Bytes(MaxPackage) + return err +} + +type KeyImportRequest struct { + KemKeyBlob []byte + Package []byte + Context []byte +} + +func (*KeyImportRequest) Op() Op { return OpKeyImport } + +func (m *KeyImportRequest) Encode(w *Writer) { + w.Bytes(m.KemKeyBlob) + w.Bytes(m.Package) + w.Bytes(m.Context) +} + +func (m *KeyImportRequest) Decode(r *Reader) (err error) { + if m.KemKeyBlob, err = r.Bytes(MaxKeyBlob); err != nil { + return err + } + if m.Package, err = r.Bytes(MaxPackage); err != nil { + return err + } + m.Context, err = r.Bytes(MaxContext) + return err +} + +type KeyImportResponse struct { + KeyID [KeyIDLen]byte + KeyBlob []byte + Alg uint16 + Usage uint32 + Label string +} + +func (*KeyImportResponse) Op() Op { return OpKeyImport } + +func (m *KeyImportResponse) Encode(w *Writer) { + w.Bytes(m.KeyID[:]) + w.Bytes(m.KeyBlob) + w.U16(m.Alg) + w.U32(m.Usage) + w.String(m.Label) +} + +func (m *KeyImportResponse) Decode(r *Reader) (err error) { + id, err := r.Array(KeyIDLen) + if err != nil { + return err + } + copy(m.KeyID[:], id) + if m.KeyBlob, err = r.Bytes(MaxKeyBlob); err != nil { + return err + } + if m.Alg, err = r.U16(); err != nil { + return err + } + if m.Usage, err = r.U32(); err != nil { + return err + } + m.Label, err = r.String(MaxLabel) + return err +} + func NewMessage(op Op, kind Kind) Message { switch kind { case KindRequest: @@ -365,6 +522,12 @@ func NewMessage(op Op, kind Kind) Message { return &PingRequest{} case OpKeyGenerate: return &KeyGenerateRequest{} + case OpKeyInspect: + return &KeyInspectRequest{} + case OpKeyExport: + return &KeyExportRequest{} + case OpKeyImport: + return &KeyImportRequest{} case OpAeadEncrypt: return &AeadEncryptRequest{} case OpAeadDecrypt: @@ -384,6 +547,12 @@ func NewMessage(op Op, kind Kind) Message { return &PingResponse{} case OpKeyGenerate: return &KeyGenerateResponse{} + case OpKeyInspect: + return &KeyInspectResponse{} + case OpKeyExport: + return &KeyExportResponse{} + case OpKeyImport: + return &KeyImportResponse{} case OpAeadEncrypt: return &AeadEncryptResponse{} case OpAeadDecrypt: From f1204ebaa602812c423dfdfb4c80135c3e723f0a Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:58:33 +0900 Subject: [PATCH 06/48] =?UTF-8?q?Go=20=EC=BD=94=EC=96=B4=20=ED=81=B4?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8=EC=97=90=20=ED=82=A4=20?= =?UTF-8?q?=EA=B5=90=ED=99=98=20=ED=98=B8=EC=B6=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?-=20Inspect,=20ExportKey,=20ImportKey=EC=99=80=20=ED=95=9C?= =?UTF-8?q?=EB=8F=84=20=EC=82=AC=EC=A0=84=20=EA=B2=80=EC=82=AC=20-=20?= =?UTF-8?q?=EA=B0=80=EC=A7=9C=20=EC=BD=94=EC=96=B4=20=EB=8B=A8=EC=9C=84=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8,=20=EB=91=90=20=EC=BD=94=EC=96=B4?= =?UTF-8?q?=20=ED=94=84=EB=A1=9C=EC=84=B8=EC=8A=A4=20=EA=B0=84=20=EC=8B=A4?= =?UTF-8?q?=EC=A0=9C=20=EA=B5=90=ED=99=98=20=ED=86=B5=ED=95=A9=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20-=20=ED=98=B8=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=97=90=20race=20detector=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 2 +- host/internal/core/client.go | 47 +++++++++++ host/internal/core/client_test.go | 135 ++++++++++++++++++++++++++++++ 3 files changed, 183 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 5dc515b..7f81604 100644 --- a/Makefile +++ b/Makefile @@ -42,7 +42,7 @@ test-core: test-host: cd $(CORE_DIR) && $(CARGO) build --frozen -p kms-core --features pqc - cd $(HOST_DIR) && KMS_CORE_BIN=$(KMS_CORE_BIN) $(GO) test ./... + cd $(HOST_DIR) && KMS_CORE_BIN=$(KMS_CORE_BIN) $(GO) test -race ./... lint: lint-core lint-host diff --git a/host/internal/core/client.go b/host/internal/core/client.go index ab7e796..87350fe 100644 --- a/host/internal/core/client.go +++ b/host/internal/core/client.go @@ -36,6 +36,21 @@ type GeneratedKey struct { PublicKey []byte } +type KeyInfo struct { + Alg uint16 + Usage uint32 + KeyID [wire.KeyIDLen]byte + Label string +} + +type ImportedKey struct { + KeyID [wire.KeyIDLen]byte + Blob []byte + Alg uint16 + Usage uint32 + Label string +} + type Client struct { mu sync.Mutex conn net.Conn @@ -132,6 +147,38 @@ func (c *Client) Verify(ctx context.Context, alg uint16, publicKey, message, sig return rsp.Valid, nil } +func (c *Client) Inspect(ctx context.Context, blob []byte) (*KeyInfo, error) { + var rsp wire.KeyInspectResponse + if err := c.call(ctx, &wire.KeyInspectRequest{KeyBlob: blob}, &rsp); err != nil { + return nil, err + } + return &KeyInfo{Alg: rsp.Alg, Usage: rsp.Usage, KeyID: rsp.KeyID, Label: rsp.Label}, nil +} + +func (c *Client) ExportKey(ctx context.Context, blob []byte, kemAlg uint16, recipientPublicKey []byte, usage uint32, context []byte) ([]byte, error) { + if len(context) > wire.MaxContext { + return nil, &StatusError{Op: wire.OpKeyExport, Status: wire.StatusLimitExceeded} + } + var rsp wire.KeyExportResponse + req := &wire.KeyExportRequest{KeyBlob: blob, KemAlg: kemAlg, RecipientPublicKey: recipientPublicKey, Usage: usage, Context: context} + if err := c.call(ctx, req, &rsp); err != nil { + return nil, err + } + return rsp.Package, nil +} + +func (c *Client) ImportKey(ctx context.Context, kemBlob, pkg, context []byte) (*ImportedKey, error) { + if len(pkg) > wire.MaxPackage || len(context) > wire.MaxContext { + return nil, &StatusError{Op: wire.OpKeyImport, Status: wire.StatusLimitExceeded} + } + var rsp wire.KeyImportResponse + req := &wire.KeyImportRequest{KemKeyBlob: kemBlob, Package: pkg, Context: context} + if err := c.call(ctx, req, &rsp); err != nil { + return nil, err + } + return &ImportedKey{KeyID: rsp.KeyID, Blob: rsp.KeyBlob, Alg: rsp.Alg, Usage: rsp.Usage, Label: rsp.Label}, nil +} + func (c *Client) Random(ctx context.Context, n int) ([]byte, error) { if n < 0 || n > wire.MaxRandom { return nil, &StatusError{Op: wire.OpRandom, Status: wire.StatusLimitExceeded} diff --git a/host/internal/core/client_test.go b/host/internal/core/client_test.go index 96d9f66..f93c706 100644 --- a/host/internal/core/client_test.go +++ b/host/internal/core/client_test.go @@ -64,6 +64,35 @@ func helloHandler(caps uint64) handler { rb, _ := wire.EncodeBody(&wire.RandomResponse{Bytes: make([]byte, req.Len)}) rh, _ := wire.NewResponse(h.Op, h.ID, uint32(len(rb))) return rh, rb + case wire.OpKeyInspect: + var req wire.KeyInspectRequest + if err := wire.DecodeBody(&req, body); err != nil { + return wire.NewError(h.Op, h.ID, wire.StatusProtocol), nil + } + rsp := &wire.KeyInspectResponse{Alg: wire.AlgAES256GCM, Usage: wire.UsageDecrypt | wire.UsageExport, Label: "fake/" + string(req.KeyBlob)} + copy(rsp.KeyID[:], req.KeyBlob) + rb, _ := wire.EncodeBody(rsp) + rh, _ := wire.NewResponse(h.Op, h.ID, uint32(len(rb))) + return rh, rb + case wire.OpKeyExport: + var req wire.KeyExportRequest + if err := wire.DecodeBody(&req, body); err != nil { + return wire.NewError(h.Op, h.ID, wire.StatusProtocol), nil + } + rb, _ := wire.EncodeBody(&wire.KeyExportResponse{Package: append(append([]byte{}, req.Context...), req.RecipientPublicKey...)}) + rh, _ := wire.NewResponse(h.Op, h.ID, uint32(len(rb))) + return rh, rb + case wire.OpKeyImport: + var req wire.KeyImportRequest + if err := wire.DecodeBody(&req, body); err != nil { + return wire.NewError(h.Op, h.ID, wire.StatusProtocol), nil + } + if !bytes.HasPrefix(req.Package, req.Context) { + return wire.NewError(h.Op, h.ID, wire.StatusInvalidArgument), nil + } + rb, _ := wire.EncodeBody(&wire.KeyImportResponse{KeyBlob: req.KemKeyBlob, Alg: wire.AlgAES256GCM, Usage: wire.UsageDecrypt, Label: "imported"}) + rh, _ := wire.NewResponse(h.Op, h.ID, uint32(len(rb))) + return rh, rb default: return wire.NewError(h.Op, h.ID, wire.StatusUnsupportedOp), nil } @@ -92,6 +121,45 @@ func TestHelloAndCapabilities(t *testing.T) { } } +func TestExchangeCallsOverFakeCore(t *testing.T) { + conn := fakeCore(t, helloHandler(0)) + c, err := newClient(context.Background(), conn, nil, time.Second) + if err != nil { + t.Fatal(err) + } + defer c.Close() + ctx := context.Background() + info, err := c.Inspect(ctx, []byte("blob")) + if err != nil { + t.Fatal(err) + } + if info.Alg != wire.AlgAES256GCM || info.Usage != wire.UsageDecrypt|wire.UsageExport || info.Label != "fake/blob" || string(info.KeyID[:4]) != "blob" { + t.Fatalf("unexpected info %+v", info) + } + pkg, err := c.ExportKey(ctx, []byte("blob"), wire.AlgDHKEMP256, []byte("pk"), wire.UsageDecrypt, []byte("ctx")) + if err != nil || string(pkg) != "ctxpk" { + t.Fatalf("export: %q %v", pkg, err) + } + imp, err := c.ImportKey(ctx, []byte("kem"), pkg, []byte("ctx")) + if err != nil || string(imp.Blob) != "kem" || imp.Usage != wire.UsageDecrypt || imp.Label != "imported" { + t.Fatalf("import: %+v %v", imp, err) + } + _, err = c.ImportKey(ctx, []byte("kem"), pkg, []byte("other")) + var se *StatusError + if !errors.As(err, &se) || se.Status != wire.StatusInvalidArgument { + t.Fatalf("expected invalid argument, got %v", err) + } + if _, err := c.ExportKey(ctx, nil, 0, nil, 0, make([]byte, wire.MaxContext+1)); !errors.As(err, &se) || se.Status != wire.StatusLimitExceeded { + t.Fatalf("expected limit error, got %v", err) + } + if _, err := c.ImportKey(ctx, nil, make([]byte, wire.MaxPackage+1), nil); !errors.As(err, &se) || se.Status != wire.StatusLimitExceeded { + t.Fatalf("expected limit error, got %v", err) + } + if err := c.Ping(ctx); err != nil { + t.Fatal(err) + } +} + func TestStatusErrorsAreNotFatal(t *testing.T) { conn := fakeCore(t, helloHandler(0)) c, err := newClient(context.Background(), conn, nil, time.Second) @@ -306,6 +374,73 @@ func TestRealCoreSpawn(t *testing.T) { } } +func TestRealCoreExchange(t *testing.T) { + bin := realBinary(t) + ctx := context.Background() + spawn := func() *Client { + var stderr bytes.Buffer + c, err := Spawn(ctx, Options{Binary: bin, EphemeralRoot: true, PQC: true, AllowUnlockedMemory: true, Stderr: &stderr}) + if err != nil { + t.Fatalf("spawn: %v\n%s", err, stderr.String()) + } + return c + } + a := spawn() + defer a.Close() + b := spawn() + defer b.Close() + + kemAlg := wire.AlgDHKEMP256 + if b.PQC() { + kemAlg = wire.AlgHybridP256MLKEM768 + } + kem, err := b.GenerateKey(ctx, kemAlg, wire.UsageDecapsulate, "b/kem") + if err != nil { + t.Fatal(err) + } + k, err := a.GenerateKey(ctx, wire.AlgAES256GCM, wire.UsageEncrypt|wire.UsageDecrypt|wire.UsageExport, "a/shared") + if err != nil { + t.Fatal(err) + } + info, err := a.Inspect(ctx, k.Blob) + if err != nil || info.KeyID != k.KeyID || info.Label != "a/shared" || info.Alg != wire.AlgAES256GCM { + t.Fatalf("inspect: %+v %v", info, err) + } + _, err = b.Inspect(ctx, k.Blob) + var se *StatusError + if !errors.As(err, &se) || se.Status != wire.StatusInvalidKeyBlob { + t.Fatalf("foreign blob should be rejected, got %v", err) + } + ct, err := a.Encrypt(ctx, k.Blob, []byte("for b"), nil) + if err != nil { + t.Fatal(err) + } + pkg, err := a.ExportKey(ctx, k.Blob, kemAlg, kem.PublicKey, wire.UsageDecrypt, []byte("from=a;to=b")) + if err != nil { + t.Fatal(err) + } + if _, err := b.ImportKey(ctx, kem.Blob, pkg, []byte("from=a;to=c")); !errors.As(err, &se) || se.Status != wire.StatusInvalidArgument { + t.Fatalf("context mismatch should fail, got %v", err) + } + imp, err := b.ImportKey(ctx, kem.Blob, pkg, []byte("from=a;to=b")) + if err != nil { + t.Fatal(err) + } + if imp.KeyID != k.KeyID || imp.Usage != wire.UsageDecrypt || imp.Label != "a/shared" { + t.Fatalf("imported: %+v", imp) + } + pt, err := b.Decrypt(ctx, imp.Blob, ct, nil) + if err != nil || string(pt) != "for b" { + t.Fatalf("decrypt: %q %v", pt, err) + } + if _, err := b.Encrypt(ctx, imp.Blob, []byte("x"), nil); !errors.As(err, &se) || se.Status != wire.StatusUsageDenied { + t.Fatalf("granted usage should be enforced, got %v", err) + } + if _, err := a.ExportKey(ctx, k.Blob, kemAlg, kem.PublicKey, wire.UsageEncrypt|wire.UsageDecrypt|wire.UsageWrap, []byte("x")); !errors.As(err, &se) || se.Status != wire.StatusUsageDenied { + t.Fatalf("usage escalation should fail, got %v", err) + } +} + func TestRealCoreConnect(t *testing.T) { bin := realBinary(t) dir := t.TempDir() From 8676f630c12d410ef72f3927fd1b8314837b5acd Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:00:27 +0900 Subject: [PATCH 07/48] =?UTF-8?q?=EA=B5=AC=EC=84=B1=EC=9B=90=20=EA=B5=90?= =?UTF-8?q?=ED=99=98=20=EB=B4=89=ED=88=AC=EC=99=80=20=EC=8B=A0=EC=9B=90=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=ED=8F=AC=EB=A7=B7=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?-=20=EC=BD=94=EC=96=B4=EA=B0=80=20=EC=9D=B8=EC=A6=9D=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EA=B5=90=ED=99=98=20=EC=BB=A8=ED=85=8D=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20(=EC=86=A1=EC=8B=A0=EC=9E=90,=20=EC=88=98=EC=8B=A0?= =?UTF-8?q?=EC=9E=90,=20=EC=8B=9C=EA=B0=81,=20nonce)=20-=20=EC=86=A1?= =?UTF-8?q?=EC=8B=A0=EC=9E=90=20=EC=8B=A0=EC=9B=90=20=ED=82=A4=EB=A1=9C=20?= =?UTF-8?q?=EC=84=9C=EB=AA=85=EB=90=98=EB=8A=94=20KXP1=20=EB=B4=89?= =?UTF-8?q?=ED=88=AC,=20=EC=9E=90=EA=B8=B0=20=EC=84=9C=EB=AA=85=20KID1=20?= =?UTF-8?q?=EC=8B=A0=EC=9B=90=20=EC=B9=B4=EB=93=9C=EC=99=80=20=EC=A7=80?= =?UTF-8?q?=EB=AC=B8=20-=20=EA=B5=AC=EC=84=B1=EC=9B=90=20id=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99=EA=B3=BC=20=EC=95=8C=EA=B3=A0=EB=A6=AC=EC=A6=98=20?= =?UTF-8?q?=EC=A2=85=EB=A5=98=20=EA=B2=80=EC=82=AC,=20=EC=8B=A4=EC=A0=9C?= =?UTF-8?q?=20=EC=BD=94=EC=96=B4=20=EA=B8=B0=EB=B0=98=20=EC=84=9C=EB=AA=85?= =?UTF-8?q?=20=EA=B2=80=EC=A6=9D=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- host/internal/exchange/card.go | 183 +++++++++++++++++++++ host/internal/exchange/context.go | 106 ++++++++++++ host/internal/exchange/envelope.go | 146 +++++++++++++++++ host/internal/exchange/exchange_test.go | 207 ++++++++++++++++++++++++ 4 files changed, 642 insertions(+) create mode 100644 host/internal/exchange/card.go create mode 100644 host/internal/exchange/context.go create mode 100644 host/internal/exchange/envelope.go create mode 100644 host/internal/exchange/exchange_test.go diff --git a/host/internal/exchange/card.go b/host/internal/exchange/card.go new file mode 100644 index 0000000..c59f81f --- /dev/null +++ b/host/internal/exchange/card.go @@ -0,0 +1,183 @@ +package exchange + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "strings" + + "github.com/Quant-Off/pairier/host/internal/wire" +) + +const ( + CardVersion = 1 + MaxCard = 4 + 1 + 4 + MaxMemberID + 2 + 4 + wire.MaxPublicKey + 2 + 4 + wire.MaxPublicKey + 8 + 4 + wire.MaxSignature +) + +var cardMagic = [4]byte{'K', 'I', 'D', '1'} + +type Card struct { + MemberID string + IdentityAlg uint16 + IdentityPublicKey []byte + KemAlg uint16 + KemPublicKey []byte + CreatedAt uint64 + Signature []byte +} + +func (c *Card) validate() error { + if !ValidMemberID(c.MemberID) { + return ErrBadMemberID + } + if !IsSignatureAlg(c.IdentityAlg) || !IsKemAlg(c.KemAlg) { + return ErrAlgorithm + } + if len(c.IdentityPublicKey) == 0 || len(c.IdentityPublicKey) > wire.MaxPublicKey || + len(c.KemPublicKey) == 0 || len(c.KemPublicKey) > wire.MaxPublicKey || + len(c.Signature) > wire.MaxSignature { + return ErrFormat + } + return nil +} + +func (c *Card) signedBytes() ([]byte, error) { + if err := c.validate(); err != nil { + return nil, err + } + w := wire.NewWriter() + w.U8(CardVersion) + w.String(c.MemberID) + w.U16(c.IdentityAlg) + w.Bytes(c.IdentityPublicKey) + w.U16(c.KemAlg) + w.Bytes(c.KemPublicKey) + w.U64(c.CreatedAt) + body, err := w.Finish() + if err != nil { + return nil, err + } + return append(cardMagic[:], body...), nil +} + +func (c *Card) Encode() ([]byte, error) { + head, err := c.signedBytes() + if err != nil { + return nil, err + } + w := wire.NewWriter() + w.Bytes(c.Signature) + tail, err := w.Finish() + if err != nil { + return nil, err + } + return append(head, tail...), nil +} + +func DecodeCard(b []byte) (*Card, error) { + if len(b) < 4 || len(b) > MaxCard || [4]byte(b[0:4]) != cardMagic { + return nil, ErrFormat + } + r := wire.NewReader(b[4:]) + v, err := r.U8() + if err != nil || v != CardVersion { + return nil, ErrFormat + } + c := &Card{} + if c.MemberID, err = r.String(MaxMemberID); err != nil { + return nil, ErrFormat + } + if c.IdentityAlg, err = r.U16(); err != nil { + return nil, ErrFormat + } + if c.IdentityPublicKey, err = r.Bytes(wire.MaxPublicKey); err != nil { + return nil, ErrFormat + } + if c.KemAlg, err = r.U16(); err != nil { + return nil, ErrFormat + } + if c.KemPublicKey, err = r.Bytes(wire.MaxPublicKey); err != nil { + return nil, ErrFormat + } + if c.CreatedAt, err = r.U64(); err != nil { + return nil, ErrFormat + } + if c.Signature, err = r.Bytes(wire.MaxSignature); err != nil { + return nil, ErrFormat + } + if err := r.Finish(); err != nil { + return nil, ErrFormat + } + if err := c.validate(); err != nil { + return nil, err + } + return c, nil +} + +func (c *Card) Fingerprint() (string, error) { + msg, err := c.signedBytes() + if err != nil { + return "", err + } + sum := sha256.Sum256(msg) + return FormatFingerprint(sum[:]), nil +} + +func FormatFingerprint(sum []byte) string { + h := hex.EncodeToString(sum) + var b strings.Builder + for i := 0; i < len(h); i += 4 { + if i > 0 { + b.WriteByte('-') + } + end := i + 4 + if end > len(h) { + end = len(h) + } + b.WriteString(h[i:end]) + } + return b.String() +} + +func NormalizeFingerprint(s string) string { + return strings.ToLower(strings.NewReplacer("-", "", " ", "", ":", "").Replace(strings.TrimSpace(s))) +} + +func (c *Card) MatchesFingerprint(s string) bool { + fp, err := c.Fingerprint() + if err != nil { + return false + } + want := NormalizeFingerprint(fp) + got := NormalizeFingerprint(s) + return len(got) == len(want) && got == want +} + +func SignCard(ctx context.Context, s Signer, identityBlob []byte, c *Card) error { + msg, err := c.signedBytes() + if err != nil { + return err + } + sig, err := s.Sign(ctx, identityBlob, msg) + if err != nil { + return fmt.Errorf("exchange: sign card: %w", err) + } + c.Signature = sig + return nil +} + +func (c *Card) Verify(ctx context.Context, v Verifier) error { + msg, err := c.signedBytes() + if err != nil { + return err + } + ok, err := v.Verify(ctx, c.IdentityAlg, c.IdentityPublicKey, msg, c.Signature) + if err != nil { + return fmt.Errorf("exchange: verify card: %w", err) + } + if !ok { + return ErrSignature + } + return nil +} diff --git a/host/internal/exchange/context.go b/host/internal/exchange/context.go new file mode 100644 index 0000000..699b122 --- /dev/null +++ b/host/internal/exchange/context.go @@ -0,0 +1,106 @@ +package exchange + +import ( + "context" + "crypto/rand" + "errors" + "fmt" + "regexp" + "time" + + "github.com/Quant-Off/pairier/host/internal/wire" +) + +const ( + ContextVersion = 1 + MaxMemberID = 64 + NonceLen = 16 +) + +var ( + ErrBadMemberID = errors.New("exchange: invalid member id") + ErrFormat = errors.New("exchange: malformed object") + ErrSignature = errors.New("exchange: signature verification failed") + ErrAlgorithm = errors.New("exchange: unsupported algorithm") +) + +var memberIDPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,63}$`) + +func ValidMemberID(id string) bool { + return len(id) <= MaxMemberID && memberIDPattern.MatchString(id) +} + +type Signer interface { + Sign(ctx context.Context, blob, message []byte) ([]byte, error) +} + +type Verifier interface { + Verify(ctx context.Context, alg uint16, publicKey, message, signature []byte) (bool, error) +} + +type Context struct { + Sender string + Recipient string + CreatedAt uint64 + Nonce [NonceLen]byte +} + +func NewContext(sender, recipient string, now time.Time) (*Context, error) { + if !ValidMemberID(sender) || !ValidMemberID(recipient) { + return nil, ErrBadMemberID + } + if sender == recipient { + return nil, fmt.Errorf("%w: sender and recipient are the same", ErrBadMemberID) + } + c := &Context{Sender: sender, Recipient: recipient, CreatedAt: uint64(now.Unix())} + if _, err := rand.Read(c.Nonce[:]); err != nil { + return nil, fmt.Errorf("exchange: nonce: %w", err) + } + return c, nil +} + +func (c *Context) Encode() ([]byte, error) { + if !ValidMemberID(c.Sender) || !ValidMemberID(c.Recipient) { + return nil, ErrBadMemberID + } + w := wire.NewWriter() + w.U8(ContextVersion) + w.String(c.Sender) + w.String(c.Recipient) + w.U64(c.CreatedAt) + w.Bytes(c.Nonce[:]) + return w.Finish() +} + +func DecodeContext(b []byte) (*Context, error) { + if len(b) > wire.MaxContext { + return nil, ErrFormat + } + r := wire.NewReader(b) + v, err := r.U8() + if err != nil || v != ContextVersion { + return nil, ErrFormat + } + c := &Context{} + if c.Sender, err = r.String(MaxMemberID); err != nil { + return nil, ErrFormat + } + if c.Recipient, err = r.String(MaxMemberID); err != nil { + return nil, ErrFormat + } + if c.CreatedAt, err = r.U64(); err != nil { + return nil, ErrFormat + } + nonce, err := r.Array(NonceLen) + if err != nil { + return nil, ErrFormat + } + copy(c.Nonce[:], nonce) + if err := r.Finish(); err != nil { + return nil, ErrFormat + } + if !ValidMemberID(c.Sender) || !ValidMemberID(c.Recipient) || c.Sender == c.Recipient { + return nil, ErrBadMemberID + } + return c, nil +} diff --git a/host/internal/exchange/envelope.go b/host/internal/exchange/envelope.go new file mode 100644 index 0000000..1652840 --- /dev/null +++ b/host/internal/exchange/envelope.go @@ -0,0 +1,146 @@ +package exchange + +import ( + "context" + "fmt" + + "github.com/Quant-Off/pairier/host/internal/wire" +) + +const MaxEnvelope = 4 + 4 + wire.MaxContext + 4 + wire.MaxPackage + 2 + 4 + wire.MaxSignature + +var envelopeMagic = [4]byte{'K', 'X', 'P', '1'} + +type Envelope struct { + Context []byte + Package []byte + SigAlg uint16 + Signature []byte +} + +func (e *Envelope) signedBytes() ([]byte, error) { + if len(e.Context) > wire.MaxContext || len(e.Package) > wire.MaxPackage { + return nil, ErrFormat + } + w := wire.NewWriter() + w.Bytes(e.Context) + w.Bytes(e.Package) + w.U16(e.SigAlg) + body, err := w.Finish() + if err != nil { + return nil, err + } + return append(envelopeMagic[:], body...), nil +} + +func (e *Envelope) Encode() ([]byte, error) { + if len(e.Signature) > wire.MaxSignature { + return nil, ErrFormat + } + head, err := e.signedBytes() + if err != nil { + return nil, err + } + w := wire.NewWriter() + w.Bytes(e.Signature) + tail, err := w.Finish() + if err != nil { + return nil, err + } + return append(head, tail...), nil +} + +func DecodeEnvelope(b []byte) (*Envelope, error) { + if len(b) < 4 || len(b) > MaxEnvelope || [4]byte(b[0:4]) != envelopeMagic { + return nil, ErrFormat + } + r := wire.NewReader(b[4:]) + e := &Envelope{} + var err error + if e.Context, err = r.Bytes(wire.MaxContext); err != nil { + return nil, ErrFormat + } + if e.Package, err = r.Bytes(wire.MaxPackage); err != nil { + return nil, ErrFormat + } + if e.SigAlg, err = r.U16(); err != nil { + return nil, ErrFormat + } + if e.Signature, err = r.Bytes(wire.MaxSignature); err != nil { + return nil, ErrFormat + } + if err := r.Finish(); err != nil { + return nil, ErrFormat + } + if !IsSignatureAlg(e.SigAlg) { + return nil, ErrAlgorithm + } + return e, nil +} + +func Seal(ctx context.Context, s Signer, identityBlob []byte, sigAlg uint16, ectx *Context, pkg []byte) (*Envelope, error) { + if !IsSignatureAlg(sigAlg) { + return nil, ErrAlgorithm + } + cb, err := ectx.Encode() + if err != nil { + return nil, err + } + e := &Envelope{Context: cb, Package: pkg, SigAlg: sigAlg} + msg, err := e.signedBytes() + if err != nil { + return nil, err + } + sig, err := s.Sign(ctx, identityBlob, msg) + if err != nil { + return nil, fmt.Errorf("exchange: sign: %w", err) + } + e.Signature = sig + return e, nil +} + +func (e *Envelope) Verify(ctx context.Context, v Verifier, senderPublicKey []byte) (*Context, error) { + c, err := DecodeContext(e.Context) + if err != nil { + return nil, err + } + msg, err := e.signedBytes() + if err != nil { + return nil, err + } + ok, err := v.Verify(ctx, e.SigAlg, senderPublicKey, msg, e.Signature) + if err != nil { + return nil, fmt.Errorf("exchange: verify: %w", err) + } + if !ok { + return nil, ErrSignature + } + return c, nil +} + +func IsSignatureAlg(alg uint16) bool { + switch alg { + case wire.AlgECDSAP256SHA256, wire.AlgECDSAP384SHA384, wire.AlgEd25519, wire.AlgMLDSA65, wire.AlgMLDSA87: + return true + default: + return false + } +} + +func IsKemAlg(alg uint16) bool { + switch alg { + case wire.AlgDHKEMP256, wire.AlgMLKEM768, wire.AlgMLKEM1024, wire.AlgHybridP256MLKEM768: + return true + default: + return false + } +} + +func IsPQCAlg(alg uint16) bool { + switch alg { + case wire.AlgMLDSA65, wire.AlgMLDSA87, wire.AlgMLKEM768, wire.AlgMLKEM1024, wire.AlgHybridP256MLKEM768: + return true + default: + return false + } +} diff --git a/host/internal/exchange/exchange_test.go b/host/internal/exchange/exchange_test.go new file mode 100644 index 0000000..367acfb --- /dev/null +++ b/host/internal/exchange/exchange_test.go @@ -0,0 +1,207 @@ +package exchange + +import ( + "bytes" + "context" + "errors" + "os" + "testing" + "time" + + "github.com/Quant-Off/pairier/host/internal/core" + "github.com/Quant-Off/pairier/host/internal/wire" +) + +func TestContextRoundtripAndValidation(t *testing.T) { + c, err := NewContext("alice", "bob", time.Unix(1_700_000_000, 0)) + if err != nil { + t.Fatal(err) + } + b, err := c.Encode() + if err != nil { + t.Fatal(err) + } + d, err := DecodeContext(b) + if err != nil { + t.Fatal(err) + } + if d.Sender != "alice" || d.Recipient != "bob" || d.CreatedAt != 1_700_000_000 || d.Nonce != c.Nonce { + t.Fatalf("mismatch %+v", d) + } + if _, err := DecodeContext(append(b, 0)); !errors.Is(err, ErrFormat) { + t.Fatalf("trailing bytes accepted: %v", err) + } + if _, err := DecodeContext(b[:len(b)-1]); !errors.Is(err, ErrFormat) { + t.Fatalf("truncated accepted: %v", err) + } + for _, bad := range []string{"", "Alice", "-a", "a b", "a/b", string(make([]byte, 65))} { + if _, err := NewContext(bad, "bob", time.Now()); !errors.Is(err, ErrBadMemberID) { + t.Fatalf("member id %q accepted: %v", bad, err) + } + } + if _, err := NewContext("alice", "alice", time.Now()); !errors.Is(err, ErrBadMemberID) { + t.Fatal("self exchange accepted") + } + c2, _ := NewContext("alice", "bob", time.Now()) + if c2.Nonce == c.Nonce { + t.Fatal("nonce not fresh") + } +} + +func TestEnvelopeAndCardEncoding(t *testing.T) { + cb, _ := (&Context{Sender: "a", Recipient: "b", CreatedAt: 1}).Encode() + e := &Envelope{Context: cb, Package: []byte("pkg"), SigAlg: wire.AlgECDSAP256SHA256, Signature: []byte("sig")} + b, err := e.Encode() + if err != nil { + t.Fatal(err) + } + d, err := DecodeEnvelope(b) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(d.Context, cb) || string(d.Package) != "pkg" || d.SigAlg != e.SigAlg || string(d.Signature) != "sig" { + t.Fatalf("mismatch %+v", d) + } + if _, err := DecodeEnvelope(append(b, 0)); !errors.Is(err, ErrFormat) { + t.Fatal("trailing bytes accepted") + } + bad := append([]byte{}, b...) + bad[0] = 'X' + if _, err := DecodeEnvelope(bad); !errors.Is(err, ErrFormat) { + t.Fatal("bad magic accepted") + } + e.SigAlg = wire.AlgAES256GCM + b2, _ := e.Encode() + if _, err := DecodeEnvelope(b2); !errors.Is(err, ErrAlgorithm) { + t.Fatal("non-signature alg accepted") + } + + card := &Card{MemberID: "bob", IdentityAlg: wire.AlgEd25519, IdentityPublicKey: make([]byte, 32), KemAlg: wire.AlgDHKEMP256, KemPublicKey: make([]byte, 65), CreatedAt: 7, Signature: []byte("s")} + cb2, err := card.Encode() + if err != nil { + t.Fatal(err) + } + dc, err := DecodeCard(cb2) + if err != nil { + t.Fatal(err) + } + fp1, _ := card.Fingerprint() + fp2, _ := dc.Fingerprint() + if fp1 != fp2 || len(NormalizeFingerprint(fp1)) != 64 { + t.Fatalf("fingerprint %q %q", fp1, fp2) + } + if !dc.MatchesFingerprint(" "+fp1+" ") || dc.MatchesFingerprint(fp1[:10]) { + t.Fatal("fingerprint matching") + } + dc.KemPublicKey[0] ^= 1 + fp3, _ := dc.Fingerprint() + if fp3 == fp1 { + t.Fatal("fingerprint ignores kem key") + } + card.KemAlg = wire.AlgEd25519 + if _, err := card.Encode(); !errors.Is(err, ErrAlgorithm) { + t.Fatal("kem alg not validated") + } + card.KemAlg = wire.AlgDHKEMP256 + card.MemberID = "Bob" + if _, err := card.Encode(); !errors.Is(err, ErrBadMemberID) { + t.Fatal("member id not validated") + } +} + +func realCore(t *testing.T) *core.Client { + t.Helper() + bin := os.Getenv("KMS_CORE_BIN") + if bin == "" { + t.Skip("KMS_CORE_BIN not set") + } + c, err := core.Spawn(context.Background(), core.Options{Binary: bin, EphemeralRoot: true, PQC: true, AllowUnlockedMemory: true}) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = c.Close() }) + return c +} + +func TestSignedEnvelopeAndCardWithRealCore(t *testing.T) { + a := realCore(t) + b := realCore(t) + ctx := context.Background() + sigAlg, kemAlg := uint16(wire.AlgECDSAP256SHA256), uint16(wire.AlgDHKEMP256) + if a.PQC() { + sigAlg, kemAlg = wire.AlgMLDSA65, wire.AlgHybridP256MLKEM768 + } + aid, err := a.GenerateKey(ctx, sigAlg, wire.UsageSign, "a/identity") + if err != nil { + t.Fatal(err) + } + bid, err := b.GenerateKey(ctx, sigAlg, wire.UsageSign, "b/identity") + if err != nil { + t.Fatal(err) + } + bkem, err := b.GenerateKey(ctx, kemAlg, wire.UsageDecapsulate, "b/kem") + if err != nil { + t.Fatal(err) + } + card := &Card{MemberID: "bob", IdentityAlg: sigAlg, IdentityPublicKey: bid.PublicKey, KemAlg: kemAlg, KemPublicKey: bkem.PublicKey, CreatedAt: 1} + if err := SignCard(ctx, b, bid.Blob, card); err != nil { + t.Fatal(err) + } + enc, _ := card.Encode() + received, err := DecodeCard(enc) + if err != nil { + t.Fatal(err) + } + if err := received.Verify(ctx, a); err != nil { + t.Fatalf("card verify: %v", err) + } + forged := *received + forged.KemPublicKey = append([]byte{}, received.KemPublicKey...) + forged.KemPublicKey[1] ^= 1 + if err := forged.Verify(ctx, a); !errors.Is(err, ErrSignature) { + t.Fatalf("forged card accepted: %v", err) + } + + k, err := a.GenerateKey(ctx, wire.AlgAES256GCM, wire.UsageEncrypt|wire.UsageDecrypt|wire.UsageExport, "shared") + if err != nil { + t.Fatal(err) + } + ectx, _ := NewContext("alice", "bob", time.Now()) + cb, _ := ectx.Encode() + pkg, err := a.ExportKey(ctx, k.Blob, received.KemAlg, received.KemPublicKey, wire.UsageDecrypt, cb) + if err != nil { + t.Fatal(err) + } + env, err := Seal(ctx, a, aid.Blob, sigAlg, ectx, pkg) + if err != nil { + t.Fatal(err) + } + raw, _ := env.Encode() + got, err := DecodeEnvelope(raw) + if err != nil { + t.Fatal(err) + } + gctx, err := got.Verify(ctx, b, aid.PublicKey) + if err != nil { + t.Fatal(err) + } + if gctx.Sender != "alice" || gctx.Recipient != "bob" { + t.Fatalf("context %+v", gctx) + } + if _, err := got.Verify(ctx, b, bid.PublicKey); !errors.Is(err, ErrSignature) { + t.Fatalf("wrong signer accepted: %v", err) + } + tampered := *got + tampered.Package = append([]byte{}, got.Package...) + tampered.Package[len(tampered.Package)-1] ^= 1 + if _, err := tampered.Verify(ctx, b, aid.PublicKey); !errors.Is(err, ErrSignature) { + t.Fatalf("tampered package accepted: %v", err) + } + imp, err := b.ImportKey(ctx, bkem.Blob, got.Package, got.Context) + if err != nil { + t.Fatal(err) + } + if imp.KeyID != k.KeyID || imp.Usage != wire.UsageDecrypt { + t.Fatalf("imported %+v", imp) + } +} From 16e48ee942aec44b8e9704857d9ead301ee8c63d Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:02:24 +0900 Subject: [PATCH 08/48] =?UTF-8?q?=ED=98=B8=EC=8A=A4=ED=8A=B8=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=80=EC=9E=A5=EC=86=8C=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?-=20=ED=82=A4,=20=EA=B5=AC=EC=84=B1=EC=9B=90=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C,=20=EB=85=B8=EB=93=9C=20=EC=8B=A0=EC=9B=90,=20inbox?= =?UTF-8?q?=20=EB=A0=88=EC=BD=94=EB=93=9C=EB=A5=BC=200700=20=EB=94=94?= =?UTF-8?q?=EB=A0=89=ED=84=B0=EB=A6=AC=EC=99=80=200600=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EB=A1=9C=20=EB=B3=B4=EA=B4=80=20-=20=EC=9E=84?= =?UTF-8?q?=EC=8B=9C=20=ED=8C=8C=EC=9D=BC=EA=B3=BC=20fsync,=20link=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=EC=9B=90=EC=9E=90=EC=A0=81=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1,=20=EB=8D=AE=EC=96=B4=EC=93=B0=EA=B8=B0=20=EA=B8=88?= =?UTF-8?q?=EC=A7=80=20-=20=EC=86=8C=EC=9C=A0=EC=9E=90,=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C,=20=EC=8B=AC=EB=B3=BC=EB=A6=AD=20=EB=A7=81=ED=81=AC,?= =?UTF-8?q?=20=EB=AF=B8=EC=A7=80=20=ED=95=84=EB=93=9C,=20=ED=9B=84?= =?UTF-8?q?=ED=96=89=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EA=B2=80=EC=82=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- host/internal/store/store.go | 420 ++++++++++++++++++++++++++++++ host/internal/store/store_test.go | 184 +++++++++++++ 2 files changed, 604 insertions(+) create mode 100644 host/internal/store/store.go create mode 100644 host/internal/store/store_test.go diff --git a/host/internal/store/store.go b/host/internal/store/store.go new file mode 100644 index 0000000..9233919 --- /dev/null +++ b/host/internal/store/store.go @@ -0,0 +1,420 @@ +package store + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + "syscall" +) + +const ( + dirMode = 0o700 + fileMode = 0o600 + + keysDir = "keys" + membersDir = "members" + inboxDir = "inbox" + nodeFile = "node.json" + rootFile = "root.key" + auditFile = "audit.log" +) + +var ( + ErrExists = errors.New("store: object already exists") + ErrNotFound = errors.New("store: object not found") + ErrCorrupt = errors.New("store: corrupt object") + ErrInsecure = errors.New("store: insecure directory") + ErrBadID = errors.New("store: invalid identifier") + ErrNotInited = errors.New("store: directory is not initialized") +) + +var ( + hexIDPattern = regexp.MustCompile(`^[0-9a-f]{32}$`) + memberIDPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,63}$`) +) + +type Store struct { + dir string +} + +type Origin struct { + Sender string `json:"sender"` + SentAt int64 `json:"sent_at"` +} + +type KeyRecord struct { + KeyID string `json:"key_id"` + Alg uint16 `json:"alg"` + Usage uint32 `json:"usage"` + Label string `json:"label"` + Blob []byte `json:"blob"` + PublicKey []byte `json:"public_key,omitempty"` + CreatedAt int64 `json:"created_at"` + Origin *Origin `json:"origin,omitempty"` +} + +type MemberRecord struct { + MemberID string `json:"member_id"` + Card []byte `json:"card"` + Fingerprint string `json:"fingerprint"` + AddedAt int64 `json:"added_at"` +} + +type Node struct { + MemberID string `json:"member_id"` + IdentityKeyID string `json:"identity_key_id"` + KemKeyID string `json:"kem_key_id"` + PQC bool `json:"pqc"` + CreatedAt int64 `json:"created_at"` +} + +type InboxRecord struct { + ID string `json:"id"` + Envelope []byte `json:"envelope"` + ReceivedAt int64 `json:"received_at"` +} + +func Init(dir string) (*Store, error) { + if err := os.Mkdir(dir, dirMode); err != nil { + if errors.Is(err, fs.ErrExist) { + entries, rerr := os.ReadDir(dir) + if rerr != nil || len(entries) != 0 { + return nil, fmt.Errorf("%w: %s is not empty", ErrExists, dir) + } + if err := os.Chmod(dir, dirMode); err != nil { + return nil, err + } + } else { + return nil, err + } + } + for _, sub := range []string{keysDir, membersDir, inboxDir} { + if err := os.Mkdir(filepath.Join(dir, sub), dirMode); err != nil && !errors.Is(err, fs.ErrExist) { + return nil, err + } + } + if err := syncDir(dir); err != nil { + return nil, err + } + return Open(dir) +} + +func Open(dir string) (*Store, error) { + abs, err := filepath.Abs(dir) + if err != nil { + return nil, err + } + if err := checkDir(abs); err != nil { + return nil, err + } + for _, sub := range []string{keysDir, membersDir, inboxDir} { + if err := checkDir(filepath.Join(abs, sub)); err != nil { + return nil, fmt.Errorf("%w: %s", ErrNotInited, sub) + } + } + return &Store{dir: abs}, nil +} + +func checkDir(path string) error { + fi, err := os.Lstat(path) + if err != nil { + return err + } + if fi.Mode()&fs.ModeSymlink != 0 { + return fmt.Errorf("%w: %s is a symlink", ErrInsecure, path) + } + if !fi.IsDir() { + return fmt.Errorf("%w: %s is not a directory", ErrInsecure, path) + } + if fi.Mode().Perm()&0o077 != 0 { + return fmt.Errorf("%w: %s is accessible by group or others", ErrInsecure, path) + } + if st, ok := fi.Sys().(*syscall.Stat_t); ok && int(st.Uid) != os.Getuid() { + return fmt.Errorf("%w: %s is not owned by the current user", ErrInsecure, path) + } + return nil +} + +func (s *Store) Dir() string { return s.dir } +func (s *Store) RootKeyPath() string { return filepath.Join(s.dir, rootFile) } +func (s *Store) AuditLogPath() string { return filepath.Join(s.dir, auditFile) } + +func ValidKeyID(id string) bool { return hexIDPattern.MatchString(id) } +func ValidMemberID(id string) bool { return memberIDPattern.MatchString(id) } + +func KeyIDString(id [16]byte) string { return hex.EncodeToString(id[:]) } + +func InboxID(envelope []byte) string { + sum := sha256.Sum256(envelope) + return hex.EncodeToString(sum[:16]) +} + +func (s *Store) PutKey(r *KeyRecord) error { + if !ValidKeyID(r.KeyID) { + return ErrBadID + } + return s.writeNew(filepath.Join(keysDir, r.KeyID+".json"), r) +} + +func (s *Store) GetKey(id string) (*KeyRecord, error) { + if !ValidKeyID(id) { + return nil, ErrBadID + } + var r KeyRecord + if err := s.read(filepath.Join(keysDir, id+".json"), &r); err != nil { + return nil, err + } + if r.KeyID != id || len(r.Blob) == 0 { + return nil, ErrCorrupt + } + return &r, nil +} + +func (s *Store) ListKeys() ([]*KeyRecord, error) { + ids, err := s.listIDs(keysDir, ".json", ValidKeyID) + if err != nil { + return nil, err + } + out := make([]*KeyRecord, 0, len(ids)) + for _, id := range ids { + r, err := s.GetKey(id) + if err != nil { + return nil, fmt.Errorf("%w: key %s: %v", ErrCorrupt, id, err) + } + out = append(out, r) + } + sort.Slice(out, func(i, j int) bool { + if out[i].CreatedAt != out[j].CreatedAt { + return out[i].CreatedAt < out[j].CreatedAt + } + return out[i].KeyID < out[j].KeyID + }) + return out, nil +} + +func (s *Store) PutMember(m *MemberRecord) error { + if !ValidMemberID(m.MemberID) { + return ErrBadID + } + return s.writeNew(filepath.Join(membersDir, m.MemberID+".json"), m) +} + +func (s *Store) GetMember(id string) (*MemberRecord, error) { + if !ValidMemberID(id) { + return nil, ErrBadID + } + var m MemberRecord + if err := s.read(filepath.Join(membersDir, id+".json"), &m); err != nil { + return nil, err + } + if m.MemberID != id || len(m.Card) == 0 { + return nil, ErrCorrupt + } + return &m, nil +} + +func (s *Store) ListMembers() ([]*MemberRecord, error) { + ids, err := s.listIDs(membersDir, ".json", ValidMemberID) + if err != nil { + return nil, err + } + out := make([]*MemberRecord, 0, len(ids)) + for _, id := range ids { + m, err := s.GetMember(id) + if err != nil { + return nil, fmt.Errorf("%w: member %s: %v", ErrCorrupt, id, err) + } + out = append(out, m) + } + sort.Slice(out, func(i, j int) bool { return out[i].MemberID < out[j].MemberID }) + return out, nil +} + +func (s *Store) PutNode(n *Node) error { + if !ValidMemberID(n.MemberID) || !ValidKeyID(n.IdentityKeyID) || !ValidKeyID(n.KemKeyID) { + return ErrBadID + } + return s.writeNew(nodeFile, n) +} + +func (s *Store) GetNode() (*Node, error) { + var n Node + if err := s.read(nodeFile, &n); err != nil { + if errors.Is(err, ErrNotFound) { + return nil, ErrNotInited + } + return nil, err + } + if !ValidMemberID(n.MemberID) || !ValidKeyID(n.IdentityKeyID) || !ValidKeyID(n.KemKeyID) { + return nil, ErrCorrupt + } + return &n, nil +} + +func (s *Store) PutInbox(r *InboxRecord) error { + if !ValidKeyID(r.ID) || r.ID != InboxID(r.Envelope) { + return ErrBadID + } + return s.writeNew(filepath.Join(inboxDir, r.ID+".json"), r) +} + +func (s *Store) GetInbox(id string) (*InboxRecord, error) { + if !ValidKeyID(id) { + return nil, ErrBadID + } + var r InboxRecord + if err := s.read(filepath.Join(inboxDir, id+".json"), &r); err != nil { + return nil, err + } + if r.ID != id || InboxID(r.Envelope) != id { + return nil, ErrCorrupt + } + return &r, nil +} + +func (s *Store) ListInbox() ([]*InboxRecord, error) { + ids, err := s.listIDs(inboxDir, ".json", ValidKeyID) + if err != nil { + return nil, err + } + out := make([]*InboxRecord, 0, len(ids)) + for _, id := range ids { + r, err := s.GetInbox(id) + if err != nil { + return nil, fmt.Errorf("%w: inbox %s: %v", ErrCorrupt, id, err) + } + out = append(out, r) + } + sort.Slice(out, func(i, j int) bool { + if out[i].ReceivedAt != out[j].ReceivedAt { + return out[i].ReceivedAt < out[j].ReceivedAt + } + return out[i].ID < out[j].ID + }) + return out, nil +} + +func (s *Store) DeleteInbox(id string) error { + if !ValidKeyID(id) { + return ErrBadID + } + path := filepath.Join(s.dir, inboxDir, id+".json") + if err := os.Remove(path); err != nil { + if errors.Is(err, fs.ErrNotExist) { + return ErrNotFound + } + return err + } + return syncDir(filepath.Dir(path)) +} + +func (s *Store) listIDs(sub, ext string, valid func(string) bool) ([]string, error) { + entries, err := os.ReadDir(filepath.Join(s.dir, sub)) + if err != nil { + return nil, err + } + ids := make([]string, 0, len(entries)) + for _, e := range entries { + name := e.Name() + if e.Type()&fs.ModeType != 0 || !strings.HasSuffix(name, ext) { + continue + } + id := strings.TrimSuffix(name, ext) + if !valid(id) { + return nil, fmt.Errorf("%w: unexpected file %s/%s", ErrCorrupt, sub, name) + } + ids = append(ids, id) + } + return ids, nil +} + +func (s *Store) read(rel string, v any) error { + path := filepath.Join(s.dir, rel) + fi, err := os.Lstat(path) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return ErrNotFound + } + return err + } + if !fi.Mode().IsRegular() { + return fmt.Errorf("%w: %s is not a regular file", ErrInsecure, rel) + } + if fi.Mode().Perm()&0o077 != 0 { + return fmt.Errorf("%w: %s is accessible by group or others", ErrInsecure, rel) + } + b, err := os.ReadFile(path) + if err != nil { + return err + } + dec := json.NewDecoder(bytes.NewReader(b)) + dec.DisallowUnknownFields() + if err := dec.Decode(v); err != nil { + return fmt.Errorf("%w: %s: %v", ErrCorrupt, rel, err) + } + if dec.More() { + return fmt.Errorf("%w: %s: trailing data", ErrCorrupt, rel) + } + return nil +} + +func (s *Store) writeNew(rel string, v any) error { + path := filepath.Join(s.dir, rel) + if _, err := os.Lstat(path); err == nil { + return ErrExists + } else if !errors.Is(err, fs.ErrNotExist) { + return err + } + data, err := json.Marshal(v) + if err != nil { + return err + } + tmp := path + ".tmp" + f, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_EXCL, fileMode) + if err != nil { + return err + } + if _, err := f.Write(data); err != nil { + _ = f.Close() + _ = os.Remove(tmp) + return err + } + if err := f.Sync(); err != nil { + _ = f.Close() + _ = os.Remove(tmp) + return err + } + if err := f.Close(); err != nil { + _ = os.Remove(tmp) + return err + } + if err := os.Link(tmp, path); err != nil { + _ = os.Remove(tmp) + if errors.Is(err, fs.ErrExist) { + return ErrExists + } + return err + } + if err := os.Remove(tmp); err != nil { + return err + } + return syncDir(filepath.Dir(path)) +} + +func syncDir(path string) error { + d, err := os.Open(path) + if err != nil { + return err + } + defer d.Close() + return d.Sync() +} diff --git a/host/internal/store/store_test.go b/host/internal/store/store_test.go new file mode 100644 index 0000000..e2cff73 --- /dev/null +++ b/host/internal/store/store_test.go @@ -0,0 +1,184 @@ +package store + +import ( + "errors" + "os" + "path/filepath" + "testing" +) + +func newStore(t *testing.T) *Store { + t.Helper() + s, err := Init(filepath.Join(t.TempDir(), "node")) + if err != nil { + t.Fatal(err) + } + return s +} + +func TestInitAndOpenEnforceLayoutAndPermissions(t *testing.T) { + base := t.TempDir() + dir := filepath.Join(base, "node") + s, err := Init(dir) + if err != nil { + t.Fatal(err) + } + if _, err := Init(dir); err == nil { + t.Fatal("init over an initialized directory must fail") + } + if _, err := Open(dir); err != nil { + t.Fatal(err) + } + if _, err := Open(filepath.Join(base, "missing")); err == nil { + t.Fatal("open of missing dir succeeded") + } + plain := filepath.Join(base, "plain") + if err := os.Mkdir(plain, 0o700); err != nil { + t.Fatal(err) + } + if _, err := Open(plain); !errors.Is(err, ErrNotInited) { + t.Fatalf("expected ErrNotInited, got %v", err) + } + if err := os.Chmod(dir, 0o750); err != nil { + t.Fatal(err) + } + if _, err := Open(dir); !errors.Is(err, ErrInsecure) { + t.Fatalf("expected ErrInsecure, got %v", err) + } + if err := os.Chmod(dir, 0o700); err != nil { + t.Fatal(err) + } + link := filepath.Join(base, "link") + if err := os.Symlink(dir, link); err != nil { + t.Fatal(err) + } + if _, err := Open(link); !errors.Is(err, ErrInsecure) { + t.Fatalf("expected ErrInsecure for symlink, got %v", err) + } + if _, err := s.GetNode(); !errors.Is(err, ErrNotInited) { + t.Fatalf("expected ErrNotInited, got %v", err) + } +} + +func TestKeyRecords(t *testing.T) { + s := newStore(t) + id := KeyIDString([16]byte{1, 2, 3}) + r := &KeyRecord{KeyID: id, Alg: 1, Usage: 3, Label: "l", Blob: []byte("blob"), CreatedAt: 10} + if err := s.PutKey(r); err != nil { + t.Fatal(err) + } + if err := s.PutKey(r); !errors.Is(err, ErrExists) { + t.Fatalf("expected ErrExists, got %v", err) + } + got, err := s.GetKey(id) + if err != nil || got.Label != "l" || string(got.Blob) != "blob" || got.Origin != nil { + t.Fatalf("get: %+v %v", got, err) + } + if _, err := s.GetKey("nope"); !errors.Is(err, ErrBadID) { + t.Fatalf("expected ErrBadID, got %v", err) + } + if _, err := s.GetKey(KeyIDString([16]byte{9})); !errors.Is(err, ErrNotFound) { + t.Fatalf("expected ErrNotFound, got %v", err) + } + r2 := &KeyRecord{KeyID: KeyIDString([16]byte{0}), Alg: 1, Usage: 2, Blob: []byte("b"), CreatedAt: 5, Origin: &Origin{Sender: "bob", SentAt: 4}} + if err := s.PutKey(r2); err != nil { + t.Fatal(err) + } + list, err := s.ListKeys() + if err != nil || len(list) != 2 || list[0].KeyID != r2.KeyID || list[1].Origin != nil || list[0].Origin.Sender != "bob" { + t.Fatalf("list: %+v %v", list, err) + } + fi, err := os.Stat(filepath.Join(s.Dir(), "keys", id+".json")) + if err != nil || fi.Mode().Perm() != 0o600 { + t.Fatalf("file mode %v %v", fi.Mode(), err) + } + if err := os.Chmod(filepath.Join(s.Dir(), "keys", id+".json"), 0o640); err != nil { + t.Fatal(err) + } + if _, err := s.GetKey(id); !errors.Is(err, ErrInsecure) { + t.Fatalf("expected ErrInsecure, got %v", err) + } + if err := os.WriteFile(filepath.Join(s.Dir(), "keys", "junk.json"), []byte("{}"), 0o600); err != nil { + t.Fatal(err) + } + if _, err := s.ListKeys(); !errors.Is(err, ErrCorrupt) { + t.Fatalf("expected ErrCorrupt for junk file, got %v", err) + } +} + +func TestCorruptAndUnknownFieldsRejected(t *testing.T) { + s := newStore(t) + id := KeyIDString([16]byte{7}) + path := filepath.Join(s.Dir(), "keys", id+".json") + if err := os.WriteFile(path, []byte(`{"key_id":"`+id+`","alg":1,"usage":1,"blob":"YQ==","created_at":1,"extra":true}`), 0o600); err != nil { + t.Fatal(err) + } + if _, err := s.GetKey(id); !errors.Is(err, ErrCorrupt) { + t.Fatalf("unknown field accepted: %v", err) + } + if err := os.WriteFile(path, []byte(`{"key_id":"`+KeyIDString([16]byte{8})+`","alg":1,"usage":1,"blob":"YQ==","created_at":1}`), 0o600); err != nil { + t.Fatal(err) + } + if _, err := s.GetKey(id); !errors.Is(err, ErrCorrupt) { + t.Fatalf("id mismatch accepted: %v", err) + } + if err := os.WriteFile(path, []byte(`{"key_id":"`+id+`","alg":1,"usage":1,"blob":"YQ==","created_at":1}{}`), 0o600); err != nil { + t.Fatal(err) + } + if _, err := s.GetKey(id); !errors.Is(err, ErrCorrupt) { + t.Fatalf("trailing data accepted: %v", err) + } +} + +func TestMembersNodeAndInbox(t *testing.T) { + s := newStore(t) + m := &MemberRecord{MemberID: "bob", Card: []byte("card"), Fingerprint: "fp", AddedAt: 1} + if err := s.PutMember(m); err != nil { + t.Fatal(err) + } + if err := s.PutMember(&MemberRecord{MemberID: "Bob", Card: []byte("c")}); !errors.Is(err, ErrBadID) { + t.Fatalf("expected ErrBadID, got %v", err) + } + if err := s.PutMember(&MemberRecord{MemberID: "alice", Card: []byte("c"), AddedAt: 2}); err != nil { + t.Fatal(err) + } + list, err := s.ListMembers() + if err != nil || len(list) != 2 || list[0].MemberID != "alice" || list[1].Fingerprint != "fp" { + t.Fatalf("members: %+v %v", list, err) + } + n := &Node{MemberID: "alice", IdentityKeyID: KeyIDString([16]byte{1}), KemKeyID: KeyIDString([16]byte{2}), PQC: true, CreatedAt: 3} + if err := s.PutNode(n); err != nil { + t.Fatal(err) + } + if err := s.PutNode(n); !errors.Is(err, ErrExists) { + t.Fatalf("expected ErrExists, got %v", err) + } + got, err := s.GetNode() + if err != nil || got.MemberID != "alice" || !got.PQC || got.KemKeyID != n.KemKeyID { + t.Fatalf("node: %+v %v", got, err) + } + env := []byte("envelope") + rec := &InboxRecord{ID: InboxID(env), Envelope: env, ReceivedAt: 9} + if err := s.PutInbox(rec); err != nil { + t.Fatal(err) + } + if err := s.PutInbox(rec); !errors.Is(err, ErrExists) { + t.Fatalf("duplicate envelope accepted: %v", err) + } + if err := s.PutInbox(&InboxRecord{ID: KeyIDString([16]byte{3}), Envelope: env}); !errors.Is(err, ErrBadID) { + t.Fatalf("mismatched inbox id accepted: %v", err) + } + inbox, err := s.ListInbox() + if err != nil || len(inbox) != 1 || string(inbox[0].Envelope) != "envelope" { + t.Fatalf("inbox: %+v %v", inbox, err) + } + if err := s.DeleteInbox(rec.ID); err != nil { + t.Fatal(err) + } + if err := s.DeleteInbox(rec.ID); !errors.Is(err, ErrNotFound) { + t.Fatalf("expected ErrNotFound, got %v", err) + } + if _, err := os.Lstat(filepath.Join(s.Dir(), "inbox", rec.ID+".json.tmp")); !errors.Is(err, os.ErrNotExist) { + t.Fatal("temp file left behind") + } +} From 8c8bdf924a156deb3b48ac8b8a895eaa96089204 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:04:13 +0900 Subject: [PATCH 09/48] =?UTF-8?q?=EB=85=B8=EB=93=9C=20=EA=B3=84=EC=B8=B5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20-=20=EC=B4=88=EA=B8=B0=ED=99=94=20?= =?UTF-8?q?=EC=8B=9C=20=EB=A3=A8=ED=8A=B8=20=ED=82=A4=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?,=20=EC=8B=A0=EC=9B=90=20=EC=84=9C=EB=AA=85=20=ED=82=A4?= =?UTF-8?q?=EC=99=80=20KEM=20=ED=82=A4=20=EC=83=9D=EC=84=B1,=20=EB=85=B8?= =?UTF-8?q?=EB=93=9C=20=EB=A0=88=EC=BD=94=EB=93=9C=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?-=20=EC=B9=B4=EB=93=9C=20=EB=82=B4=EB=B3=B4=EB=82=B4=EA=B8=B0,?= =?UTF-8?q?=20=EC=A7=80=EB=AC=B8=20=EA=B2=80=EC=A6=9D=20=ED=9B=84=20?= =?UTF-8?q?=EA=B5=AC=EC=84=B1=EC=9B=90=20=EB=93=B1=EB=A1=9D,=20=EC=97=B4?= =?UTF-8?q?=20=EB=95=8C=EB=A7=88=EB=8B=A4=20=EC=B9=B4=EB=93=9C=20=EC=9E=90?= =?UTF-8?q?=EA=B8=B0=20=EC=84=9C=EB=AA=85=20=EC=9E=AC=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?-=20=ED=82=A4=20=EC=83=9D=EC=84=B1,=20=EC=95=94=EB=B3=B5?= =?UTF-8?q?=ED=98=B8=ED=99=94,=20=EA=B5=AC=EC=84=B1=EC=9B=90=EC=97=90?= =?UTF-8?q?=EA=B2=8C=20=EB=B3=B4=EB=82=B4=EA=B8=B0=EC=99=80=20=EB=B0=9B?= =?UTF-8?q?=EA=B8=B0,=20=EC=A0=80=EC=9E=A5=20=EB=A9=94=ED=83=80=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=EB=A5=BC=20=EC=BD=94=EC=96=B4=20=ED=97=A4?= =?UTF-8?q?=EB=8D=94=EC=99=80=20=EB=8C=80=EC=A1=B0=20-=20pqc=20=EC=97=86?= =?UTF-8?q?=EC=9D=B4=20=EC=B4=88=EA=B8=B0=ED=99=94=ED=95=9C=20=EB=85=B8?= =?UTF-8?q?=EB=93=9C=EB=8A=94=20pqc=20=EC=B9=B4=EB=93=9C=EB=A5=BC=20?= =?UTF-8?q?=EA=B1=B0=EB=B6=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- host/internal/node/node.go | 372 ++++++++++++++++++++++++++++++++ host/internal/node/node_test.go | 247 +++++++++++++++++++++ 2 files changed, 619 insertions(+) create mode 100644 host/internal/node/node.go create mode 100644 host/internal/node/node_test.go diff --git a/host/internal/node/node.go b/host/internal/node/node.go new file mode 100644 index 0000000..fd633e5 --- /dev/null +++ b/host/internal/node/node.go @@ -0,0 +1,372 @@ +package node + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "os/exec" + "strings" + "time" + + "github.com/Quant-Off/pairier/host/internal/core" + "github.com/Quant-Off/pairier/host/internal/exchange" + "github.com/Quant-Off/pairier/host/internal/store" + "github.com/Quant-Off/pairier/host/internal/wire" +) + +var ( + ErrBadMemberID = errors.New("node: invalid member id") + ErrPQCUnavailable = errors.New("node: core does not provide pqc") + ErrFingerprint = errors.New("node: fingerprint does not match the card") + ErrSelf = errors.New("node: card belongs to this node") + ErrUnknownMember = errors.New("node: unknown member") + ErrNotRecipient = errors.New("node: envelope is addressed to another member") + ErrMetadata = errors.New("node: stored metadata does not match the core") + ErrNotSymmetric = errors.New("node: only symmetric keys can be generated or exchanged") + ErrSignatureAlg = errors.New("node: envelope signature algorithm differs from the member card") + ErrUsageNotAllowed = errors.New("node: usage not allowed for this key kind") +) + +const SymmetricUsage = wire.UsageEncrypt | wire.UsageDecrypt | wire.UsageWrap | wire.UsageUnwrap | wire.UsageDerive | wire.UsageExport + +type Options struct { + Dir string + CoreBinary string + AllowUnlockedMemory bool + Timeout time.Duration + Stderr io.Writer + Now func() time.Time +} + +type Node struct { + opts Options + st *store.Store + core *core.Client + info *store.Node + identity *store.KeyRecord + kem *store.KeyRecord +} + +func (o *Options) now() time.Time { + if o.Now != nil { + return o.Now() + } + return time.Now() +} + +func (o *Options) spawn(ctx context.Context, rootKey string, pqc bool) (*core.Client, error) { + if o.CoreBinary == "" { + return nil, errors.New("node: core binary path is required") + } + return core.Spawn(ctx, core.Options{ + Binary: o.CoreBinary, + RootKeyFile: rootKey, + PQC: pqc, + AllowUnlockedMemory: o.AllowUnlockedMemory, + Timeout: o.Timeout, + Stderr: o.Stderr, + }) +} + +func Init(ctx context.Context, o Options, memberID string, pqc bool) (*Node, error) { + if !exchange.ValidMemberID(memberID) { + return nil, ErrBadMemberID + } + if o.CoreBinary == "" { + return nil, errors.New("node: core binary path is required") + } + st, err := store.Init(o.Dir) + if err != nil { + return nil, err + } + if err := generateRootKey(ctx, o, st.RootKeyPath()); err != nil { + return nil, err + } + c, err := o.spawn(ctx, st.RootKeyPath(), pqc) + if err != nil { + return nil, err + } + n := &Node{opts: o, st: st, core: c} + if err := n.createIdentity(ctx, memberID, pqc); err != nil { + _ = c.Close() + return nil, err + } + return n, nil +} + +func generateRootKey(ctx context.Context, o Options, path string) error { + cmd := exec.CommandContext(ctx, o.CoreBinary, "--generate-root-key", path) + cmd.Env = []string{} + cmd.Dir = "/" + var stderr bytes.Buffer + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("node: generate root key: %w: %s", err, strings.TrimSpace(stderr.String())) + } + return nil +} + +func (n *Node) createIdentity(ctx context.Context, memberID string, pqc bool) error { + if pqc && !n.core.PQC() { + return ErrPQCUnavailable + } + sigAlg, kemAlg := uint16(wire.AlgECDSAP256SHA256), uint16(wire.AlgDHKEMP256) + if pqc { + sigAlg, kemAlg = wire.AlgMLDSA65, wire.AlgHybridP256MLKEM768 + } + now := n.opts.now().Unix() + id, err := n.core.GenerateKey(ctx, sigAlg, wire.UsageSign, "identity/"+memberID) + if err != nil { + return err + } + kem, err := n.core.GenerateKey(ctx, kemAlg, wire.UsageDecapsulate, "kem/"+memberID) + if err != nil { + return err + } + n.identity = &store.KeyRecord{KeyID: store.KeyIDString(id.KeyID), Alg: sigAlg, Usage: wire.UsageSign, Label: "identity/" + memberID, Blob: id.Blob, PublicKey: id.PublicKey, CreatedAt: now} + n.kem = &store.KeyRecord{KeyID: store.KeyIDString(kem.KeyID), Alg: kemAlg, Usage: wire.UsageDecapsulate, Label: "kem/" + memberID, Blob: kem.Blob, PublicKey: kem.PublicKey, CreatedAt: now} + if err := n.st.PutKey(n.identity); err != nil { + return err + } + if err := n.st.PutKey(n.kem); err != nil { + return err + } + n.info = &store.Node{MemberID: memberID, IdentityKeyID: n.identity.KeyID, KemKeyID: n.kem.KeyID, PQC: pqc, CreatedAt: now} + return n.st.PutNode(n.info) +} + +func Open(ctx context.Context, o Options) (*Node, error) { + st, err := store.Open(o.Dir) + if err != nil { + return nil, err + } + info, err := st.GetNode() + if err != nil { + return nil, err + } + c, err := o.spawn(ctx, st.RootKeyPath(), info.PQC) + if err != nil { + return nil, err + } + n := &Node{opts: o, st: st, core: c, info: info} + if info.PQC && !c.PQC() { + _ = c.Close() + return nil, ErrPQCUnavailable + } + if n.identity, err = n.Key(ctx, info.IdentityKeyID); err != nil { + _ = c.Close() + return nil, fmt.Errorf("node: identity key: %w", err) + } + if n.kem, err = n.Key(ctx, info.KemKeyID); err != nil { + _ = c.Close() + return nil, fmt.Errorf("node: kem key: %w", err) + } + if n.identity.Usage != wire.UsageSign || !exchange.IsSignatureAlg(n.identity.Alg) || + n.kem.Usage != wire.UsageDecapsulate || !exchange.IsKemAlg(n.kem.Alg) || + len(n.identity.PublicKey) == 0 || len(n.kem.PublicKey) == 0 { + _ = c.Close() + return nil, ErrMetadata + } + return n, nil +} + +func (n *Node) Close() error { return n.core.Close() } +func (n *Node) MemberID() string { return n.info.MemberID } +func (n *Node) PQC() bool { return n.info.PQC } +func (n *Node) FIPS() bool { return n.core.FIPS() } +func (n *Node) Core() *core.Client { + return n.core +} + +func (n *Node) Key(ctx context.Context, id string) (*store.KeyRecord, error) { + rec, err := n.st.GetKey(id) + if err != nil { + return nil, err + } + info, err := n.core.Inspect(ctx, rec.Blob) + if err != nil { + return nil, err + } + if store.KeyIDString(info.KeyID) != rec.KeyID || info.Alg != rec.Alg || info.Usage != rec.Usage || info.Label != rec.Label { + return nil, ErrMetadata + } + return rec, nil +} + +func (n *Node) Keys() ([]*store.KeyRecord, error) { return n.st.ListKeys() } + +func (n *Node) GenerateKey(ctx context.Context, label string, usage uint32) (*store.KeyRecord, error) { + if usage == 0 || usage&^SymmetricUsage != 0 { + return nil, ErrUsageNotAllowed + } + if len(label) > wire.MaxLabel { + return nil, fmt.Errorf("node: label exceeds %d bytes", wire.MaxLabel) + } + k, err := n.core.GenerateKey(ctx, wire.AlgAES256GCM, usage, label) + if err != nil { + return nil, err + } + rec := &store.KeyRecord{KeyID: store.KeyIDString(k.KeyID), Alg: wire.AlgAES256GCM, Usage: usage, Label: label, Blob: k.Blob, CreatedAt: n.opts.now().Unix()} + if err := n.st.PutKey(rec); err != nil { + return nil, err + } + return rec, nil +} + +func (n *Node) Encrypt(ctx context.Context, keyID string, plaintext, aad []byte) ([]byte, error) { + rec, err := n.Key(ctx, keyID) + if err != nil { + return nil, err + } + return n.core.Encrypt(ctx, rec.Blob, plaintext, aad) +} + +func (n *Node) Decrypt(ctx context.Context, keyID string, ciphertext, aad []byte) ([]byte, error) { + rec, err := n.Key(ctx, keyID) + if err != nil { + return nil, err + } + return n.core.Decrypt(ctx, rec.Blob, ciphertext, aad) +} + +func (n *Node) Card(ctx context.Context) (*exchange.Card, []byte, error) { + card := &exchange.Card{ + MemberID: n.info.MemberID, + IdentityAlg: n.identity.Alg, + IdentityPublicKey: n.identity.PublicKey, + KemAlg: n.kem.Alg, + KemPublicKey: n.kem.PublicKey, + CreatedAt: uint64(n.info.CreatedAt), + } + if err := exchange.SignCard(ctx, n.core, n.identity.Blob, card); err != nil { + return nil, nil, err + } + raw, err := card.Encode() + if err != nil { + return nil, nil, err + } + return card, raw, nil +} + +func (n *Node) AddMember(ctx context.Context, raw []byte, fingerprint string) (*store.MemberRecord, error) { + card, err := exchange.DecodeCard(raw) + if err != nil { + return nil, err + } + if card.MemberID == n.info.MemberID { + return nil, ErrSelf + } + if err := card.Verify(ctx, n.core); err != nil { + return nil, err + } + if !card.MatchesFingerprint(fingerprint) { + return nil, ErrFingerprint + } + fp, err := card.Fingerprint() + if err != nil { + return nil, err + } + rec := &store.MemberRecord{MemberID: card.MemberID, Card: raw, Fingerprint: fp, AddedAt: n.opts.now().Unix()} + if err := n.st.PutMember(rec); err != nil { + return nil, err + } + return rec, nil +} + +func (n *Node) Members() ([]*store.MemberRecord, error) { return n.st.ListMembers() } + +func (n *Node) Member(ctx context.Context, id string) (*exchange.Card, error) { + rec, err := n.st.GetMember(id) + if err != nil { + if errors.Is(err, store.ErrNotFound) || errors.Is(err, store.ErrBadID) { + return nil, fmt.Errorf("%w: %s", ErrUnknownMember, id) + } + return nil, err + } + card, err := exchange.DecodeCard(rec.Card) + if err != nil { + return nil, err + } + if card.MemberID != id || !card.MatchesFingerprint(rec.Fingerprint) { + return nil, ErrMetadata + } + if err := card.Verify(ctx, n.core); err != nil { + return nil, err + } + return card, nil +} + +func (n *Node) Send(ctx context.Context, keyID, to string, usage uint32) ([]byte, error) { + rec, err := n.Key(ctx, keyID) + if err != nil { + return nil, err + } + if rec.Alg != wire.AlgAES256GCM { + return nil, ErrNotSymmetric + } + card, err := n.Member(ctx, to) + if err != nil { + return nil, err + } + ectx, err := exchange.NewContext(n.info.MemberID, to, n.opts.now()) + if err != nil { + return nil, err + } + cb, err := ectx.Encode() + if err != nil { + return nil, err + } + pkg, err := n.core.ExportKey(ctx, rec.Blob, card.KemAlg, card.KemPublicKey, usage, cb) + if err != nil { + return nil, err + } + env, err := exchange.Seal(ctx, n.core, n.identity.Blob, n.identity.Alg, ectx, pkg) + if err != nil { + return nil, err + } + return env.Encode() +} + +func (n *Node) Receive(ctx context.Context, raw []byte) (*store.KeyRecord, error) { + env, err := exchange.DecodeEnvelope(raw) + if err != nil { + return nil, err + } + peek, err := exchange.DecodeContext(env.Context) + if err != nil { + return nil, err + } + if peek.Recipient != n.info.MemberID { + return nil, ErrNotRecipient + } + card, err := n.Member(ctx, peek.Sender) + if err != nil { + return nil, err + } + if env.SigAlg != card.IdentityAlg { + return nil, ErrSignatureAlg + } + ectx, err := env.Verify(ctx, n.core, card.IdentityPublicKey) + if err != nil { + return nil, err + } + imp, err := n.core.ImportKey(ctx, n.kem.Blob, env.Package, env.Context) + if err != nil { + return nil, err + } + rec := &store.KeyRecord{ + KeyID: store.KeyIDString(imp.KeyID), + Alg: imp.Alg, + Usage: imp.Usage, + Label: imp.Label, + Blob: imp.Blob, + CreatedAt: n.opts.now().Unix(), + Origin: &store.Origin{Sender: ectx.Sender, SentAt: int64(ectx.CreatedAt)}, + } + if err := n.st.PutKey(rec); err != nil { + return nil, err + } + return rec, nil +} diff --git a/host/internal/node/node_test.go b/host/internal/node/node_test.go new file mode 100644 index 0000000..daa6c97 --- /dev/null +++ b/host/internal/node/node_test.go @@ -0,0 +1,247 @@ +package node + +import ( + "context" + "encoding/json" + "errors" + "os" + "path/filepath" + "testing" + "time" + + "github.com/Quant-Off/pairier/host/internal/core" + "github.com/Quant-Off/pairier/host/internal/exchange" + "github.com/Quant-Off/pairier/host/internal/store" + "github.com/Quant-Off/pairier/host/internal/wire" +) + +func opts(t *testing.T, dir string) Options { + t.Helper() + bin := os.Getenv("KMS_CORE_BIN") + if bin == "" { + t.Skip("KMS_CORE_BIN not set") + } + return Options{Dir: dir, CoreBinary: bin, AllowUnlockedMemory: true, Timeout: 5 * time.Second, Now: func() time.Time { return time.Unix(1_700_000_000, 0) }} +} + +func initNode(t *testing.T, id string, pqc bool) (*Node, Options) { + t.Helper() + o := opts(t, filepath.Join(t.TempDir(), id)) + n, err := Init(context.Background(), o, id, pqc) + if err != nil { + t.Fatalf("init %s: %v", id, err) + } + t.Cleanup(func() { _ = n.Close() }) + return n, o +} + +func pair(t *testing.T, a, b *Node) { + t.Helper() + ctx := context.Background() + ca, rawA, err := a.Card(ctx) + if err != nil { + t.Fatal(err) + } + cb, rawB, err := b.Card(ctx) + if err != nil { + t.Fatal(err) + } + fpA, _ := ca.Fingerprint() + fpB, _ := cb.Fingerprint() + if _, err := a.AddMember(ctx, rawB, fpA); !errors.Is(err, ErrFingerprint) { + t.Fatalf("wrong fingerprint accepted: %v", err) + } + if _, err := a.AddMember(ctx, rawA, fpA); !errors.Is(err, ErrSelf) { + t.Fatalf("own card accepted: %v", err) + } + if _, err := a.AddMember(ctx, rawB, fpB); err != nil { + t.Fatal(err) + } + if _, err := a.AddMember(ctx, rawB, fpB); !errors.Is(err, store.ErrExists) { + t.Fatalf("duplicate member accepted: %v", err) + } + if _, err := b.AddMember(ctx, rawA, fpA); err != nil { + t.Fatal(err) + } +} + +func TestExchangeBetweenNodes(t *testing.T) { + ctx := context.Background() + alice, aliceOpts := initNode(t, "alice", false) + bob, _ := initNode(t, "bob", false) + if alice.MemberID() != "alice" || alice.PQC() { + t.Fatalf("node info %s %v", alice.MemberID(), alice.PQC()) + } + pair(t, alice, bob) + members, err := alice.Members() + if err != nil || len(members) != 1 || members[0].MemberID != "bob" { + t.Fatalf("members %+v %v", members, err) + } + + k, err := alice.GenerateKey(ctx, "project/db", wire.UsageEncrypt|wire.UsageDecrypt|wire.UsageExport) + if err != nil { + t.Fatal(err) + } + if _, err := alice.GenerateKey(ctx, "x", wire.UsageSign); !errors.Is(err, ErrUsageNotAllowed) { + t.Fatalf("sign usage accepted for symmetric key: %v", err) + } + ct, err := alice.Encrypt(ctx, k.KeyID, []byte("secret"), []byte("aad")) + if err != nil { + t.Fatal(err) + } + + env, err := alice.Send(ctx, k.KeyID, "bob", wire.UsageDecrypt) + if err != nil { + t.Fatal(err) + } + if _, err := alice.Send(ctx, k.KeyID, "carol", wire.UsageDecrypt); !errors.Is(err, ErrUnknownMember) { + t.Fatalf("unknown member accepted: %v", err) + } + var se *core.StatusError + if _, err := alice.Send(ctx, k.KeyID, "bob", wire.UsageWrap); !errors.As(err, &se) || se.Status != wire.StatusUsageDenied { + t.Fatalf("usage escalation accepted: %v", err) + } + if _, err := alice.Receive(ctx, env); !errors.Is(err, ErrNotRecipient) { + t.Fatalf("sender accepted its own envelope: %v", err) + } + + rec, err := bob.Receive(ctx, env) + if err != nil { + t.Fatal(err) + } + if rec.KeyID != k.KeyID || rec.Usage != wire.UsageDecrypt || rec.Label != "project/db" || rec.Origin == nil || rec.Origin.Sender != "alice" { + t.Fatalf("received %+v", rec) + } + pt, err := bob.Decrypt(ctx, rec.KeyID, ct, []byte("aad")) + if err != nil || string(pt) != "secret" { + t.Fatalf("decrypt %q %v", pt, err) + } + if _, err := bob.Encrypt(ctx, rec.KeyID, []byte("x"), nil); !errors.As(err, &se) || se.Status != wire.StatusUsageDenied { + t.Fatalf("granted usage not enforced: %v", err) + } + if _, err := bob.Receive(ctx, env); !errors.Is(err, store.ErrExists) { + t.Fatalf("replay accepted: %v", err) + } + tampered := append([]byte{}, env...) + tampered[len(tampered)-1] ^= 1 + if _, err := bob.Receive(ctx, tampered); !errors.Is(err, exchange.ErrSignature) { + t.Fatalf("tampered envelope accepted: %v", err) + } + + carol, _ := initNode(t, "carol", false) + pair(t, alice, carol) + env2, err := alice.Send(ctx, k.KeyID, "carol", wire.UsageDecrypt) + if err != nil { + t.Fatal(err) + } + if _, err := bob.Receive(ctx, env2); !errors.Is(err, ErrNotRecipient) { + t.Fatalf("misaddressed envelope accepted: %v", err) + } + if _, err := carol.Receive(ctx, env); !errors.Is(err, ErrNotRecipient) { + t.Fatalf("misaddressed envelope accepted: %v", err) + } + + if err := alice.Close(); err != nil { + t.Fatal(err) + } + reopened, err := Open(ctx, aliceOpts) + if err != nil { + t.Fatal(err) + } + defer reopened.Close() + keys, err := reopened.Keys() + if err != nil || len(keys) != 3 { + t.Fatalf("keys after reopen %d %v", len(keys), err) + } + if _, err := reopened.Key(ctx, k.KeyID); err != nil { + t.Fatal(err) + } + env3, err := reopened.Send(ctx, k.KeyID, "bob", wire.UsageEncrypt|wire.UsageDecrypt) + if err != nil { + t.Fatal(err) + } + if _, err := bob.Receive(ctx, env3); !errors.Is(err, store.ErrExists) { + t.Fatalf("expected duplicate key id, got %v", err) + } +} + +func TestStoredMetadataIsVerifiedAgainstCore(t *testing.T) { + ctx := context.Background() + alice, o := initNode(t, "alice", false) + k, err := alice.GenerateKey(ctx, "label", wire.UsageEncrypt) + if err != nil { + t.Fatal(err) + } + path := filepath.Join(o.Dir, "keys", k.KeyID+".json") + raw, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + var rec store.KeyRecord + if err := json.Unmarshal(raw, &rec); err != nil { + t.Fatal(err) + } + rec.Usage = wire.UsageEncrypt | wire.UsageDecrypt | wire.UsageExport + edited, _ := json.Marshal(&rec) + if err := os.WriteFile(path, edited, 0o600); err != nil { + t.Fatal(err) + } + if _, err := alice.Key(ctx, k.KeyID); !errors.Is(err, ErrMetadata) { + t.Fatalf("metadata tamper not detected: %v", err) + } + if _, err := alice.Encrypt(ctx, k.KeyID, []byte("x"), nil); !errors.Is(err, ErrMetadata) { + t.Fatalf("tampered key usable: %v", err) + } + + bob, bobOpts := initNode(t, "bob", false) + _, rawA, _ := alice.Card(ctx) + ca, _ := exchange.DecodeCard(rawA) + fp, _ := ca.Fingerprint() + if _, err := bob.AddMember(ctx, rawA, fp); err != nil { + t.Fatal(err) + } + mpath := filepath.Join(bobOpts.Dir, "members", "alice.json") + mraw, _ := os.ReadFile(mpath) + var m store.MemberRecord + _ = json.Unmarshal(mraw, &m) + m.Fingerprint = "0000" + medit, _ := json.Marshal(&m) + if err := os.WriteFile(mpath, medit, 0o600); err != nil { + t.Fatal(err) + } + if _, err := bob.Member(ctx, "alice"); !errors.Is(err, ErrMetadata) { + t.Fatalf("member tamper not detected: %v", err) + } +} + +func TestPQCNodes(t *testing.T) { + ctx := context.Background() + alice, _ := initNode(t, "alice", true) + if !alice.Core().PQC() { + t.Skip("core built without pqc") + } + bob, _ := initNode(t, "bob", true) + pair(t, alice, bob) + card, _, _ := alice.Card(ctx) + if card.IdentityAlg != wire.AlgMLDSA65 || card.KemAlg != wire.AlgHybridP256MLKEM768 { + t.Fatalf("pqc algs %x %x", card.IdentityAlg, card.KemAlg) + } + k, err := alice.GenerateKey(ctx, "k", wire.UsageDecrypt|wire.UsageExport) + if err != nil { + t.Fatal(err) + } + env, err := alice.Send(ctx, k.KeyID, "bob", wire.UsageDecrypt) + if err != nil { + t.Fatal(err) + } + if _, err := bob.Receive(ctx, env); err != nil { + t.Fatal(err) + } + classic, _ := initNode(t, "dave", false) + _, rawA, _ := alice.Card(ctx) + fp, _ := card.Fingerprint() + var se *core.StatusError + if _, err := classic.AddMember(ctx, rawA, fp); !errors.As(err, &se) || se.Status != wire.StatusCapabilityDisabled { + t.Fatalf("node without pqc must refuse a pqc card, got %v", err) + } +} From 750e9ab83a71353a85fe07b65eeda2183a8e3083 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:06:41 +0900 Subject: [PATCH 10/48] =?UTF-8?q?kmsctl=20=EB=AA=85=EB=A0=B9=ED=96=89=20?= =?UTF-8?q?=EB=8F=84=EA=B5=AC=20=EA=B5=AC=ED=98=84=20-=20init,=20card,=20m?= =?UTF-8?q?ember=20add/list,=20key=20gen/list/info/send/receive,=20encrypt?= =?UTF-8?q?,=20decrypt,=20version=20-=20=EB=B4=89=ED=88=AC=EC=99=80=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=EB=8A=94=20=ED=8C=8C=EC=9D=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=A3=BC=EA=B3=A0=EB=B0=9B=EC=95=84=20=EB=84=A4=ED=8A=B8?= =?UTF-8?q?=EC=9B=8C=ED=81=AC=20=EC=97=86=EC=9D=B4=20=EA=B5=90=ED=99=98=20?= =?UTF-8?q?=EC=99=84=EA=B2=B0=20-=20=EC=8B=A4=EC=A0=9C=20=EC=BD=94?= =?UTF-8?q?=EC=96=B4=EB=A1=9C=20=EB=91=90=20=EB=85=B8=EB=93=9C=20=EA=B0=84?= =?UTF-8?q?=20=EB=81=9D=EB=8C=80=EB=81=9D=20=ED=85=8C=EC=8A=A4=ED=8A=B8,?= =?UTF-8?q?=20=EB=B9=8C=EB=93=9C=20=EC=8B=9C=20=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=EC=9E=84=EB=B2=A0=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 4 +- host/cmd/kmsctl/main.go | 498 ++++++++++++++++++++++++++++++++++- host/cmd/kmsctl/main_test.go | 134 ++++++++++ 3 files changed, 634 insertions(+), 2 deletions(-) create mode 100644 host/cmd/kmsctl/main_test.go diff --git a/Makefile b/Makefile index 7f81604..bcf7f3c 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,8 @@ export GOFLAGS := -mod=vendor -trimpath export CGO_ENABLED := 0 RUST_TOOLCHAIN := $(shell sed -n 's/^channel = "\(.*\)"/\1/p' $(CORE_DIR)/rust-toolchain.toml) +VERSION := $(shell sed -n 's/^version = "\(.*\)"/\1/p' $(CORE_DIR)/Cargo.toml | head -1) +GO_LDFLAGS := -s -w -X main.version=$(VERSION) CARGO_NET := $(CARGO) +$(RUST_TOOLCHAIN) CORE_MANIFEST := --manifest-path $(CORE_DIR)/Cargo.toml GO_NET := GOPROXY=https://proxy.golang.org,direct GOFLAGS=-trimpath $(GO) @@ -33,7 +35,7 @@ build-core: cd $(CORE_DIR) && $(CARGO) build --release --frozen -p kms-core $(CORE_FLAGS) build-host: - cd $(HOST_DIR) && $(GO) build -o bin/ ./cmd/... + cd $(HOST_DIR) && $(GO) build -ldflags "$(GO_LDFLAGS)" -o bin/ ./cmd/... test: test-core test-host diff --git a/host/cmd/kmsctl/main.go b/host/cmd/kmsctl/main.go index 38dd16d..6874360 100644 --- a/host/cmd/kmsctl/main.go +++ b/host/cmd/kmsctl/main.go @@ -1,3 +1,499 @@ package main -func main() {} +import ( + "context" + "errors" + "flag" + "fmt" + "io" + "os" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/Quant-Off/pairier/host/internal/node" + "github.com/Quant-Off/pairier/host/internal/wire" +) + +var version = "dev" + +const usageText = `usage: kmsctl [--dir DIR] [--core PATH] [flags] + +commands: + init --id MEMBER [--pqc] + card [--out FILE] + member add --card FILE --fingerprint FP + member list + key gen --label LABEL [--usage encrypt,decrypt,export] + key list + key info --id KEYID + key send --id KEYID --to MEMBER [--usage decrypt] [--out FILE] + key receive --in FILE + encrypt --id KEYID [--in FILE] [--out FILE] [--aad STRING] + decrypt --id KEYID [--in FILE] [--out FILE] [--aad STRING] + version +` + +var usageNames = map[string]uint32{ + "encrypt": wire.UsageEncrypt, + "decrypt": wire.UsageDecrypt, + "sign": wire.UsageSign, + "verify": wire.UsageVerify, + "wrap": wire.UsageWrap, + "unwrap": wire.UsageUnwrap, + "derive": wire.UsageDerive, + "decapsulate": wire.UsageDecapsulate, + "export": wire.UsageExport, +} + +var usageOrder = []string{"encrypt", "decrypt", "sign", "verify", "wrap", "unwrap", "derive", "decapsulate", "export"} + +var algNames = map[uint16]string{ + wire.AlgAES256GCM: "aes-256-gcm", + wire.AlgECDSAP256SHA256: "ecdsa-p256-sha256", + wire.AlgECDSAP384SHA384: "ecdsa-p384-sha384", + wire.AlgEd25519: "ed25519", + wire.AlgDHKEMP256: "dhkem-p256", + wire.AlgMLDSA65: "ml-dsa-65", + wire.AlgMLDSA87: "ml-dsa-87", + wire.AlgMLKEM768: "ml-kem-768", + wire.AlgMLKEM1024: "ml-kem-1024", + wire.AlgHybridP256MLKEM768: "hybrid-p256-ml-kem-768", +} + +type cli struct { + stdin io.Reader + stdout io.Writer + stderr io.Writer + opts node.Options +} + +func main() { + os.Exit(run(os.Args[1:], os.Stdin, os.Stdout, os.Stderr)) +} + +func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int { + fs := flag.NewFlagSet("kmsctl", flag.ContinueOnError) + fs.SetOutput(stderr) + dir := fs.String("dir", os.Getenv("KMS_DIR"), "node directory (default $KMS_DIR)") + corePath := fs.String("core", os.Getenv("KMS_CORE"), "kms-core binary (default $KMS_CORE or next to kmsctl)") + allowUnlocked := fs.Bool("allow-unlocked-memory", runtime.GOOS == "darwin", "run the core without locked memory") + timeout := fs.Duration("timeout", 10*time.Second, "core call timeout") + fs.Usage = func() { fmt.Fprint(stderr, usageText) } + if err := fs.Parse(args); err != nil { + return 2 + } + rest := fs.Args() + if len(rest) == 0 { + fs.Usage() + return 2 + } + if rest[0] == "version" { + fmt.Fprintf(stdout, "kmsctl %s\n", version) + return 0 + } + if *dir == "" { + fmt.Fprintln(stderr, "kmsctl: --dir or $KMS_DIR is required") + return 2 + } + if *corePath == "" { + if exe, err := os.Executable(); err == nil { + *corePath = filepath.Join(filepath.Dir(exe), "kms-core") + } + } + c := &cli{stdin: stdin, stdout: stdout, stderr: stderr, opts: node.Options{ + Dir: *dir, + CoreBinary: *corePath, + AllowUnlockedMemory: *allowUnlocked, + Timeout: *timeout, + Stderr: stderr, + }} + code, err := c.dispatch(rest) + if err != nil { + fmt.Fprintf(stderr, "kmsctl: %v\n", err) + if code == 0 { + code = 1 + } + } + return code +} + +func (c *cli) dispatch(args []string) (int, error) { + ctx := context.Background() + switch args[0] { + case "init": + return c.cmdInit(ctx, args[1:]) + case "card": + return c.cmdCard(ctx, args[1:]) + case "member": + if len(args) < 2 { + return 2, errors.New("member: subcommand required (add, list)") + } + switch args[1] { + case "add": + return c.cmdMemberAdd(ctx, args[2:]) + case "list": + return c.cmdMemberList(ctx, args[2:]) + } + return 2, fmt.Errorf("member: unknown subcommand %q", args[1]) + case "key": + if len(args) < 2 { + return 2, errors.New("key: subcommand required (gen, list, info, send, receive)") + } + switch args[1] { + case "gen": + return c.cmdKeyGen(ctx, args[2:]) + case "list": + return c.cmdKeyList(ctx, args[2:]) + case "info": + return c.cmdKeyInfo(ctx, args[2:]) + case "send": + return c.cmdKeySend(ctx, args[2:]) + case "receive": + return c.cmdKeyReceive(ctx, args[2:]) + } + return 2, fmt.Errorf("key: unknown subcommand %q", args[1]) + case "encrypt": + return c.cmdCrypt(ctx, args[1:], true) + case "decrypt": + return c.cmdCrypt(ctx, args[1:], false) + } + fmt.Fprint(c.stderr, usageText) + return 2, fmt.Errorf("unknown command %q", args[0]) +} + +func (c *cli) flags(name string) *flag.FlagSet { + fs := flag.NewFlagSet(name, flag.ContinueOnError) + fs.SetOutput(c.stderr) + return fs +} + +func (c *cli) open(ctx context.Context) (*node.Node, error) { + return node.Open(ctx, c.opts) +} + +func (c *cli) cmdInit(ctx context.Context, args []string) (int, error) { + fs := c.flags("init") + id := fs.String("id", "", "member id") + pqc := fs.Bool("pqc", false, "use post-quantum identity and kem algorithms") + if err := fs.Parse(args); err != nil || fs.NArg() != 0 { + return 2, errors.New("init: invalid arguments") + } + if *id == "" { + return 2, errors.New("init: --id is required") + } + n, err := node.Init(ctx, c.opts, *id, *pqc) + if err != nil { + return 1, err + } + defer n.Close() + card, _, err := n.Card(ctx) + if err != nil { + return 1, err + } + fp, err := card.Fingerprint() + if err != nil { + return 1, err + } + fmt.Fprintf(c.stdout, "member\t%s\nidentity\t%s\nkem\t%s\nfips\t%v\npqc\t%v\nfingerprint\t%s\n", + card.MemberID, algName(card.IdentityAlg), algName(card.KemAlg), n.FIPS(), n.PQC(), fp) + return 0, nil +} + +func (c *cli) cmdCard(ctx context.Context, args []string) (int, error) { + fs := c.flags("card") + out := fs.String("out", "", "output file (default stdout)") + if err := fs.Parse(args); err != nil || fs.NArg() != 0 { + return 2, errors.New("card: invalid arguments") + } + n, err := c.open(ctx) + if err != nil { + return 1, err + } + defer n.Close() + card, raw, err := n.Card(ctx) + if err != nil { + return 1, err + } + fp, _ := card.Fingerprint() + if err := c.writeOut(*out, raw); err != nil { + return 1, err + } + fmt.Fprintf(c.stderr, "member\t%s\nfingerprint\t%s\n", card.MemberID, fp) + return 0, nil +} + +func (c *cli) cmdMemberAdd(ctx context.Context, args []string) (int, error) { + fs := c.flags("member add") + cardPath := fs.String("card", "", "member card file") + fp := fs.String("fingerprint", "", "fingerprint verified out of band") + if err := fs.Parse(args); err != nil || fs.NArg() != 0 { + return 2, errors.New("member add: invalid arguments") + } + if *cardPath == "" || *fp == "" { + return 2, errors.New("member add: --card and --fingerprint are required") + } + raw, err := c.readIn(*cardPath) + if err != nil { + return 1, err + } + n, err := c.open(ctx) + if err != nil { + return 1, err + } + defer n.Close() + rec, err := n.AddMember(ctx, raw, *fp) + if err != nil { + return 1, err + } + fmt.Fprintf(c.stdout, "added\t%s\t%s\n", rec.MemberID, rec.Fingerprint) + return 0, nil +} + +func (c *cli) cmdMemberList(ctx context.Context, args []string) (int, error) { + if len(args) != 0 { + return 2, errors.New("member list: no arguments expected") + } + n, err := c.open(ctx) + if err != nil { + return 1, err + } + defer n.Close() + members, err := n.Members() + if err != nil { + return 1, err + } + for _, m := range members { + card, err := n.Member(ctx, m.MemberID) + if err != nil { + return 1, fmt.Errorf("member %s: %w", m.MemberID, err) + } + fmt.Fprintf(c.stdout, "%s\t%s\t%s\t%s\n", m.MemberID, algName(card.IdentityAlg), algName(card.KemAlg), m.Fingerprint) + } + return 0, nil +} + +func (c *cli) cmdKeyGen(ctx context.Context, args []string) (int, error) { + fs := c.flags("key gen") + label := fs.String("label", "", "key label") + usage := fs.String("usage", "encrypt,decrypt,export", "comma separated usage list") + if err := fs.Parse(args); err != nil || fs.NArg() != 0 { + return 2, errors.New("key gen: invalid arguments") + } + bits, err := parseUsage(*usage) + if err != nil { + return 2, err + } + n, err := c.open(ctx) + if err != nil { + return 1, err + } + defer n.Close() + rec, err := n.GenerateKey(ctx, *label, bits) + if err != nil { + return 1, err + } + fmt.Fprintf(c.stdout, "%s\t%s\t%s\t%s\n", rec.KeyID, algName(rec.Alg), formatUsage(rec.Usage), rec.Label) + return 0, nil +} + +func (c *cli) cmdKeyList(ctx context.Context, args []string) (int, error) { + if len(args) != 0 { + return 2, errors.New("key list: no arguments expected") + } + n, err := c.open(ctx) + if err != nil { + return 1, err + } + defer n.Close() + keys, err := n.Keys() + if err != nil { + return 1, err + } + for _, k := range keys { + origin := "local" + if k.Origin != nil { + origin = "from " + k.Origin.Sender + } + fmt.Fprintf(c.stdout, "%s\t%s\t%s\t%s\t%s\n", k.KeyID, algName(k.Alg), formatUsage(k.Usage), k.Label, origin) + } + return 0, nil +} + +func (c *cli) cmdKeyInfo(ctx context.Context, args []string) (int, error) { + fs := c.flags("key info") + id := fs.String("id", "", "key id") + if err := fs.Parse(args); err != nil || fs.NArg() != 0 || *id == "" { + return 2, errors.New("key info: --id is required") + } + n, err := c.open(ctx) + if err != nil { + return 1, err + } + defer n.Close() + k, err := n.Key(ctx, *id) + if err != nil { + return 1, err + } + fmt.Fprintf(c.stdout, "id\t%s\nalg\t%s\nusage\t%s\nlabel\t%s\ncreated\t%s\n", k.KeyID, algName(k.Alg), formatUsage(k.Usage), k.Label, time.Unix(k.CreatedAt, 0).UTC().Format(time.RFC3339)) + if k.Origin != nil { + fmt.Fprintf(c.stdout, "origin\t%s\t%s\n", k.Origin.Sender, time.Unix(k.Origin.SentAt, 0).UTC().Format(time.RFC3339)) + } + return 0, nil +} + +func (c *cli) cmdKeySend(ctx context.Context, args []string) (int, error) { + fs := c.flags("key send") + id := fs.String("id", "", "key id") + to := fs.String("to", "", "recipient member id") + usage := fs.String("usage", "decrypt", "usage granted to the recipient") + out := fs.String("out", "", "output file (default stdout)") + if err := fs.Parse(args); err != nil || fs.NArg() != 0 { + return 2, errors.New("key send: invalid arguments") + } + if *id == "" || *to == "" { + return 2, errors.New("key send: --id and --to are required") + } + bits, err := parseUsage(*usage) + if err != nil { + return 2, err + } + n, err := c.open(ctx) + if err != nil { + return 1, err + } + defer n.Close() + env, err := n.Send(ctx, *id, *to, bits) + if err != nil { + return 1, err + } + if err := c.writeOut(*out, env); err != nil { + return 1, err + } + fmt.Fprintf(c.stderr, "sent\t%s\tto\t%s\tusage\t%s\n", *id, *to, formatUsage(bits)) + return 0, nil +} + +func (c *cli) cmdKeyReceive(ctx context.Context, args []string) (int, error) { + fs := c.flags("key receive") + in := fs.String("in", "", "envelope file (default stdin)") + if err := fs.Parse(args); err != nil || fs.NArg() != 0 { + return 2, errors.New("key receive: invalid arguments") + } + raw, err := c.readIn(*in) + if err != nil { + return 1, err + } + n, err := c.open(ctx) + if err != nil { + return 1, err + } + defer n.Close() + rec, err := n.Receive(ctx, raw) + if err != nil { + return 1, err + } + fmt.Fprintf(c.stdout, "%s\t%s\t%s\t%s\tfrom %s\n", rec.KeyID, algName(rec.Alg), formatUsage(rec.Usage), rec.Label, rec.Origin.Sender) + return 0, nil +} + +func (c *cli) cmdCrypt(ctx context.Context, args []string, encrypt bool) (int, error) { + name := "decrypt" + if encrypt { + name = "encrypt" + } + fs := c.flags(name) + id := fs.String("id", "", "key id") + in := fs.String("in", "", "input file (default stdin)") + out := fs.String("out", "", "output file (default stdout)") + aad := fs.String("aad", "", "associated data") + if err := fs.Parse(args); err != nil || fs.NArg() != 0 || *id == "" { + return 2, fmt.Errorf("%s: --id is required", name) + } + data, err := c.readIn(*in) + if err != nil { + return 1, err + } + n, err := c.open(ctx) + if err != nil { + return 1, err + } + defer n.Close() + var result []byte + if encrypt { + result, err = n.Encrypt(ctx, *id, data, []byte(*aad)) + } else { + result, err = n.Decrypt(ctx, *id, data, []byte(*aad)) + } + if err != nil { + return 1, err + } + if err := c.writeOut(*out, result); err != nil { + return 1, err + } + return 0, nil +} + +func (c *cli) readIn(path string) ([]byte, error) { + if path == "" { + return io.ReadAll(io.LimitReader(c.stdin, 1<<20)) + } + return os.ReadFile(path) +} + +func (c *cli) writeOut(path string, data []byte) error { + if path == "" { + _, err := c.stdout.Write(data) + return err + } + f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) + if err != nil { + return err + } + if _, err := f.Write(data); err != nil { + _ = f.Close() + return err + } + if err := f.Sync(); err != nil { + _ = f.Close() + return err + } + return f.Close() +} + +func parseUsage(s string) (uint32, error) { + var bits uint32 + for _, part := range strings.Split(s, ",") { + part = strings.TrimSpace(strings.ToLower(part)) + if part == "" { + continue + } + b, ok := usageNames[part] + if !ok { + return 0, fmt.Errorf("unknown usage %q", part) + } + bits |= b + } + if bits == 0 { + return 0, errors.New("usage list is empty") + } + return bits, nil +} + +func formatUsage(bits uint32) string { + var parts []string + for _, name := range usageOrder { + if bits&usageNames[name] != 0 { + parts = append(parts, name) + } + } + return strings.Join(parts, ",") +} + +func algName(alg uint16) string { + if s, ok := algNames[alg]; ok { + return s + } + return fmt.Sprintf("0x%04x", alg) +} diff --git a/host/cmd/kmsctl/main_test.go b/host/cmd/kmsctl/main_test.go new file mode 100644 index 0000000..cacb296 --- /dev/null +++ b/host/cmd/kmsctl/main_test.go @@ -0,0 +1,134 @@ +package main + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "sync" + "testing" +) + +type syncBuffer struct { + mu sync.Mutex + buf bytes.Buffer +} + +func (b *syncBuffer) Write(p []byte) (int, error) { + b.mu.Lock() + defer b.mu.Unlock() + return b.buf.Write(p) +} + +func (b *syncBuffer) String() string { + b.mu.Lock() + defer b.mu.Unlock() + return b.buf.String() +} + +func TestParseAndFormatUsage(t *testing.T) { + bits, err := parseUsage("Encrypt, decrypt,export") + if err != nil || formatUsage(bits) != "encrypt,decrypt,export" { + t.Fatalf("%v %v", bits, err) + } + if _, err := parseUsage("encrypt,bogus"); err == nil { + t.Fatal("unknown usage accepted") + } + if _, err := parseUsage(""); err == nil { + t.Fatal("empty usage accepted") + } +} + +func TestUsageAndVersion(t *testing.T) { + var out, errb bytes.Buffer + if code := run(nil, nil, &out, &errb); code != 2 { + t.Fatalf("code %d", code) + } + if code := run([]string{"version"}, nil, &out, &errb); code != 0 || !strings.HasPrefix(out.String(), "kmsctl ") { + t.Fatalf("version: %d %q", code, out.String()) + } + out.Reset() + if code := run([]string{"key", "list"}, nil, &out, &errb); code != 2 { + t.Fatalf("missing dir should be a usage error, got %d", code) + } +} + +func TestEndToEndExchange(t *testing.T) { + bin := os.Getenv("KMS_CORE_BIN") + if bin == "" { + t.Skip("KMS_CORE_BIN not set") + } + base := t.TempDir() + alice := filepath.Join(base, "alice") + bob := filepath.Join(base, "bob") + exec := func(stdin []byte, args ...string) (string, string, int) { + var out, errb syncBuffer + code := run(append([]string{"--core", bin, "--allow-unlocked-memory"}, args...), bytes.NewReader(stdin), &out, &errb) + return out.String(), errb.String(), code + } + must := func(stdin []byte, args ...string) string { + t.Helper() + out, errOut, code := exec(stdin, args...) + if code != 0 { + t.Fatalf("%v failed (%d): %s%s", args, code, out, errOut) + } + return out + } + field := func(s, key string) string { + for _, line := range strings.Split(s, "\n") { + if strings.HasPrefix(line, key+"\t") { + return strings.TrimPrefix(line, key+"\t") + } + } + t.Fatalf("field %q missing in %q", key, s) + return "" + } + + initA := must(nil, "--dir", alice, "init", "--id", "alice") + initB := must(nil, "--dir", bob, "init", "--id", "bob") + if _, _, code := exec(nil, "--dir", alice, "init", "--id", "alice"); code == 0 { + t.Fatal("re-init succeeded") + } + cardA := filepath.Join(base, "alice.card") + cardB := filepath.Join(base, "bob.card") + must(nil, "--dir", alice, "card", "--out", cardA) + must(nil, "--dir", bob, "card", "--out", cardB) + if _, _, code := exec(nil, "--dir", alice, "member", "add", "--card", cardB, "--fingerprint", field(initA, "fingerprint")); code == 0 { + t.Fatal("wrong fingerprint accepted") + } + must(nil, "--dir", alice, "member", "add", "--card", cardB, "--fingerprint", field(initB, "fingerprint")) + must(nil, "--dir", bob, "member", "add", "--card", cardA, "--fingerprint", strings.ToUpper(field(initA, "fingerprint"))) + if list := must(nil, "--dir", alice, "member", "list"); !strings.HasPrefix(list, "bob\t") { + t.Fatalf("member list %q", list) + } + + gen := must(nil, "--dir", alice, "key", "gen", "--label", "project/db") + keyID := strings.SplitN(gen, "\t", 2)[0] + ct := must([]byte("top secret"), "--dir", alice, "encrypt", "--id", keyID, "--aad", "ctx") + env := filepath.Join(base, "db.kxp") + must(nil, "--dir", alice, "key", "send", "--id", keyID, "--to", "bob", "--out", env) + if _, _, code := exec(nil, "--dir", alice, "key", "send", "--id", keyID, "--to", "bob", "--out", env); code == 0 { + t.Fatal("overwriting an existing output file succeeded") + } + recv := must(nil, "--dir", bob, "key", "receive", "--in", env) + if !strings.HasPrefix(recv, keyID+"\t") || !strings.Contains(recv, "from alice") { + t.Fatalf("receive %q", recv) + } + pt := must([]byte(ct), "--dir", bob, "decrypt", "--id", keyID, "--aad", "ctx") + if pt != "top secret" { + t.Fatalf("decrypt %q", pt) + } + if _, _, code := exec([]byte(ct), "--dir", bob, "decrypt", "--id", keyID, "--aad", "other"); code == 0 { + t.Fatal("wrong aad accepted") + } + if _, _, code := exec([]byte("x"), "--dir", bob, "encrypt", "--id", keyID); code == 0 { + t.Fatal("recipient could encrypt with decrypt-only grant") + } + info := must(nil, "--dir", bob, "key", "info", "--id", keyID) + if !strings.Contains(info, "origin\talice") || !strings.Contains(info, "usage\tdecrypt\n") { + t.Fatalf("info %q", info) + } + if list := must(nil, "--dir", bob, "key", "list"); strings.Count(list, "\n") != 3 { + t.Fatalf("bob key list %q", list) + } +} From 7852574b15ce62fda58c8cf84228d97011f8e4b1 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:08:25 +0900 Subject: [PATCH 11/48] =?UTF-8?q?=EA=B0=90=EC=82=AC=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20-=20=ED=95=B4=EC=8B=9C=20=EC=B2=B4?= =?UTF-8?q?=EC=9D=B8=EC=9C=BC=EB=A1=9C=20=EC=97=B0=EA=B2=B0=EB=90=9C=20app?= =?UTF-8?q?end-only=20JSON=20=EB=9D=BC=EC=9D=B8=20=EB=A1=9C=EA=B7=B8,=20?= =?UTF-8?q?=ED=95=AD=EB=AA=A9=EB=A7=88=EB=8B=A4=20fsync=20-=20=EC=97=B4=20?= =?UTF-8?q?=EB=95=8C=20=EC=A0=84=EC=B2=B4=20=EC=B2=B4=EC=9D=B8=20=EC=9E=AC?= =?UTF-8?q?=EA=B2=80=EC=A6=9D,=20=EB=B3=80=EC=A1=B0=EC=99=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=ED=83=90=EC=A7=80,=20=EA=B6=8C=ED=95=9C=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- host/internal/audit/audit.go | 219 ++++++++++++++++++++++++++++++ host/internal/audit/audit_test.go | 90 ++++++++++++ 2 files changed, 309 insertions(+) create mode 100644 host/internal/audit/audit_test.go diff --git a/host/internal/audit/audit.go b/host/internal/audit/audit.go index 6fd56fb..45b6dda 100644 --- a/host/internal/audit/audit.go +++ b/host/internal/audit/audit.go @@ -1 +1,220 @@ package audit + +import ( + "bufio" + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "io/fs" + "os" + "sort" + "sync" + "time" +) + +const ( + fileMode = 0o600 + maxLine = 64 * 1024 + hashLen = sha256.Size + genesis = "0000000000000000000000000000000000000000000000000000000000000000" + MaxFields = 32 +) + +var ( + ErrCorrupt = errors.New("audit: log chain is corrupt") + ErrInsecure = errors.New("audit: insecure log file") + ErrField = errors.New("audit: invalid event or field") +) + +type Entry struct { + Seq uint64 `json:"seq"` + Time int64 `json:"time"` + Event string `json:"event"` + Fields map[string]string `json:"fields,omitempty"` + Prev string `json:"prev"` + Hash string `json:"hash"` +} + +type Log struct { + mu sync.Mutex + f *os.File + seq uint64 + prev string + now func() time.Time +} + +func Open(path string, now func() time.Time) (*Log, error) { + if now == nil { + now = time.Now + } + f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, fileMode) + if err != nil { + return nil, err + } + fi, err := f.Stat() + if err != nil { + _ = f.Close() + return nil, err + } + if !fi.Mode().IsRegular() || fi.Mode().Perm()&0o077 != 0 { + _ = f.Close() + return nil, fmt.Errorf("%w: %s", ErrInsecure, path) + } + if lfi, err := os.Lstat(path); err != nil || lfi.Mode()&fs.ModeSymlink != 0 { + _ = f.Close() + return nil, fmt.Errorf("%w: %s is a symlink", ErrInsecure, path) + } + seq, prev, err := replay(f) + if err != nil { + _ = f.Close() + return nil, err + } + return &Log{f: f, seq: seq, prev: prev, now: now}, nil +} + +func (l *Log) Close() error { + l.mu.Lock() + defer l.mu.Unlock() + return l.f.Close() +} + +func (l *Log) Seq() uint64 { + l.mu.Lock() + defer l.mu.Unlock() + return l.seq +} + +func (l *Log) Append(event string, fields map[string]string) error { + if event == "" || len(event) > 64 || len(fields) > MaxFields { + return ErrField + } + for k, v := range fields { + if k == "" || len(k) > 64 || len(v) > 1024 { + return ErrField + } + } + l.mu.Lock() + defer l.mu.Unlock() + e := &Entry{Seq: l.seq + 1, Time: l.now().Unix(), Event: event, Fields: fields, Prev: l.prev} + h, err := digest(e) + if err != nil { + return err + } + e.Hash = h + line, err := json.Marshal(e) + if err != nil { + return err + } + line = append(line, '\n') + if _, err := l.f.Write(line); err != nil { + return err + } + if err := l.f.Sync(); err != nil { + return err + } + l.seq = e.Seq + l.prev = e.Hash + return nil +} + +func digest(e *Entry) (string, error) { + keys := make([]string, 0, len(e.Fields)) + for k := range e.Fields { + keys = append(keys, k) + } + sort.Strings(keys) + prev, err := hex.DecodeString(e.Prev) + if err != nil || len(prev) != hashLen { + return "", ErrCorrupt + } + h := sha256.New() + h.Write(prev) + var buf bytes.Buffer + enc := json.NewEncoder(&buf) + for _, v := range []any{e.Seq, e.Time, e.Event} { + if err := enc.Encode(v); err != nil { + return "", err + } + } + for _, k := range keys { + if err := enc.Encode(k); err != nil { + return "", err + } + if err := enc.Encode(e.Fields[k]); err != nil { + return "", err + } + } + h.Write(buf.Bytes()) + return hex.EncodeToString(h.Sum(nil)), nil +} + +func replay(r io.Reader) (uint64, string, error) { + var seq uint64 + prev := genesis + sc := bufio.NewScanner(r) + sc.Buffer(make([]byte, 0, maxLine), maxLine) + for sc.Scan() { + line := sc.Bytes() + if len(bytes.TrimSpace(line)) == 0 { + return 0, "", fmt.Errorf("%w: blank line after entry %d", ErrCorrupt, seq) + } + var e Entry + dec := json.NewDecoder(bytes.NewReader(line)) + dec.DisallowUnknownFields() + if err := dec.Decode(&e); err != nil || dec.More() { + return 0, "", fmt.Errorf("%w: entry %d is not valid json", ErrCorrupt, seq+1) + } + if e.Seq != seq+1 || e.Prev != prev { + return 0, "", fmt.Errorf("%w: entry %d breaks the chain", ErrCorrupt, e.Seq) + } + h, err := digest(&e) + if err != nil || h != e.Hash { + return 0, "", fmt.Errorf("%w: entry %d hash mismatch", ErrCorrupt, e.Seq) + } + seq = e.Seq + prev = e.Hash + } + if err := sc.Err(); err != nil { + return 0, "", fmt.Errorf("%w: %v", ErrCorrupt, err) + } + return seq, prev, nil +} + +func Verify(path string) (uint64, error) { + f, err := os.Open(path) + if err != nil { + return 0, err + } + defer f.Close() + seq, _, err := replay(f) + return seq, err +} + +func Read(path string) ([]Entry, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + if _, _, err := replay(f); err != nil { + return nil, err + } + if _, err := f.Seek(0, io.SeekStart); err != nil { + return nil, err + } + var out []Entry + sc := bufio.NewScanner(f) + sc.Buffer(make([]byte, 0, maxLine), maxLine) + for sc.Scan() { + var e Entry + if err := json.Unmarshal(sc.Bytes(), &e); err != nil { + return nil, ErrCorrupt + } + out = append(out, e) + } + return out, sc.Err() +} diff --git a/host/internal/audit/audit_test.go b/host/internal/audit/audit_test.go new file mode 100644 index 0000000..25da715 --- /dev/null +++ b/host/internal/audit/audit_test.go @@ -0,0 +1,90 @@ +package audit + +import ( + "errors" + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +func TestChainAppendVerifyAndTamper(t *testing.T) { + path := filepath.Join(t.TempDir(), "audit.log") + now := func() time.Time { return time.Unix(1_700_000_000, 0) } + l, err := Open(path, now) + if err != nil { + t.Fatal(err) + } + if err := l.Append("node.init", map[string]string{"member": "alice"}); err != nil { + t.Fatal(err) + } + if err := l.Append("key.generate", map[string]string{"key": "abc", "usage": "encrypt"}); err != nil { + t.Fatal(err) + } + if err := l.Append("", nil); !errors.Is(err, ErrField) { + t.Fatal("empty event accepted") + } + if l.Seq() != 2 { + t.Fatalf("seq %d", l.Seq()) + } + if err := l.Close(); err != nil { + t.Fatal(err) + } + fi, _ := os.Stat(path) + if fi.Mode().Perm() != 0o600 { + t.Fatalf("mode %v", fi.Mode()) + } + n, err := Verify(path) + if err != nil || n != 2 { + t.Fatalf("verify %d %v", n, err) + } + l2, err := Open(path, now) + if err != nil { + t.Fatal(err) + } + if err := l2.Append("key.send", nil); err != nil { + t.Fatal(err) + } + _ = l2.Close() + entries, err := Read(path) + if err != nil || len(entries) != 3 || entries[2].Prev != entries[1].Hash || entries[0].Fields["member"] != "alice" { + t.Fatalf("read %+v %v", entries, err) + } + + raw, _ := os.ReadFile(path) + lines := strings.Split(strings.TrimSpace(string(raw)), "\n") + edited := strings.Replace(lines[1], `"usage":"encrypt"`, `"usage":"export"`, 1) + if edited == lines[1] { + t.Fatal("edit did not apply") + } + tampered := strings.Join([]string{lines[0], edited, lines[2]}, "\n") + "\n" + if err := os.WriteFile(path, []byte(tampered), 0o600); err != nil { + t.Fatal(err) + } + if _, err := Verify(path); !errors.Is(err, ErrCorrupt) { + t.Fatalf("tamper not detected: %v", err) + } + if _, err := Open(path, now); !errors.Is(err, ErrCorrupt) { + t.Fatalf("open on tampered log succeeded: %v", err) + } + truncated := strings.Join([]string{lines[0], lines[2]}, "\n") + "\n" + if err := os.WriteFile(path, []byte(truncated), 0o600); err != nil { + t.Fatal(err) + } + if _, err := Verify(path); !errors.Is(err, ErrCorrupt) { + t.Fatalf("deletion not detected: %v", err) + } + if err := os.WriteFile(path, []byte(lines[0]+"\n"+lines[1]+"\n"), 0o600); err != nil { + t.Fatal(err) + } + if _, err := Open(path, now); err != nil { + t.Fatalf("prefix of a valid chain rejected: %v", err) + } + if err := os.Chmod(path, 0o640); err != nil { + t.Fatal(err) + } + if _, err := Open(path, now); !errors.Is(err, ErrInsecure) { + t.Fatalf("permissive mode accepted: %v", err) + } +} From 06c63ae3ac0f0e76d482c205f68bb3a033fa8095 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:10:58 +0900 Subject: [PATCH 12/48] =?UTF-8?q?=EB=85=B8=EB=93=9C=20=EB=8F=99=EC=9E=91?= =?UTF-8?q?=EC=9D=84=20=EA=B0=90=EC=82=AC=20=EB=A1=9C=EA=B7=B8=EC=97=90=20?= =?UTF-8?q?=EA=B8=B0=EB=A1=9D=20-=20=EC=B4=88=EA=B8=B0=ED=99=94,=20?= =?UTF-8?q?=EC=97=B4=EA=B8=B0,=20=ED=82=A4=20=EC=83=9D=EC=84=B1,=20?= =?UTF-8?q?=EC=95=94=EB=B3=B5=ED=98=B8=ED=99=94,=20=EA=B5=AC=EC=84=B1?= =?UTF-8?q?=EC=9B=90=20=EC=B6=94=EA=B0=80,=20=EB=B3=B4=EB=82=B4=EA=B8=B0,?= =?UTF-8?q?=20=EB=B0=9B=EA=B8=B0=EC=9D=98=20=EC=84=B1=EA=B3=B5=EA=B3=BC=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20=EA=B8=B0=EB=A1=9D=20-=20=EA=B0=90?= =?UTF-8?q?=EC=82=AC=20=EA=B8=B0=EB=A1=9D=20=EC=8B=A4=ED=8C=A8=20=EC=8B=9C?= =?UTF-8?q?=20=EB=8F=99=EC=9E=91=20=EC=9E=90=EC=B2=B4=EB=A5=BC=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=EB=A1=9C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- host/internal/node/node.go | 113 ++++++++++++++++++++++++++++---- host/internal/node/node_test.go | 29 ++++++++ 2 files changed, 128 insertions(+), 14 deletions(-) diff --git a/host/internal/node/node.go b/host/internal/node/node.go index fd633e5..ab3ca4e 100644 --- a/host/internal/node/node.go +++ b/host/internal/node/node.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/Quant-Off/pairier/host/internal/audit" "github.com/Quant-Off/pairier/host/internal/core" "github.com/Quant-Off/pairier/host/internal/exchange" "github.com/Quant-Off/pairier/host/internal/store" @@ -44,11 +45,25 @@ type Node struct { opts Options st *store.Store core *core.Client + log *audit.Log info *store.Node identity *store.KeyRecord kem *store.KeyRecord } +func (n *Node) audit(event string, fields map[string]string, err error) error { + if err != nil { + if fields == nil { + fields = map[string]string{} + } + fields["error"] = err.Error() + } + if aerr := n.log.Append(event, fields); aerr != nil { + return fmt.Errorf("node: audit: %w", aerr) + } + return err +} + func (o *Options) now() time.Time { if o.Now != nil { return o.Now() @@ -88,11 +103,20 @@ func Init(ctx context.Context, o Options, memberID string, pqc bool) (*Node, err if err != nil { return nil, err } - n := &Node{opts: o, st: st, core: c} - if err := n.createIdentity(ctx, memberID, pqc); err != nil { + log, err := audit.Open(st.AuditLogPath(), o.Now) + if err != nil { _ = c.Close() return nil, err } + n := &Node{opts: o, st: st, core: c, log: log} + if err := n.createIdentity(ctx, memberID, pqc); err != nil { + _ = n.Close() + return nil, err + } + if err := n.audit("node.init", map[string]string{"member": memberID, "identity": n.identity.KeyID, "kem": n.kem.KeyID, "pqc": fmt.Sprint(pqc), "fips": fmt.Sprint(c.FIPS())}, nil); err != nil { + _ = n.Close() + return nil, err + } return n, nil } @@ -150,32 +174,47 @@ func Open(ctx context.Context, o Options) (*Node, error) { if err != nil { return nil, err } - n := &Node{opts: o, st: st, core: c, info: info} - if info.PQC && !c.PQC() { + log, err := audit.Open(st.AuditLogPath(), o.Now) + if err != nil { _ = c.Close() + return nil, err + } + n := &Node{opts: o, st: st, core: c, log: log, info: info} + if info.PQC && !c.PQC() { + _ = n.Close() return nil, ErrPQCUnavailable } if n.identity, err = n.Key(ctx, info.IdentityKeyID); err != nil { - _ = c.Close() + _ = n.Close() return nil, fmt.Errorf("node: identity key: %w", err) } if n.kem, err = n.Key(ctx, info.KemKeyID); err != nil { - _ = c.Close() + _ = n.Close() return nil, fmt.Errorf("node: kem key: %w", err) } if n.identity.Usage != wire.UsageSign || !exchange.IsSignatureAlg(n.identity.Alg) || n.kem.Usage != wire.UsageDecapsulate || !exchange.IsKemAlg(n.kem.Alg) || len(n.identity.PublicKey) == 0 || len(n.kem.PublicKey) == 0 { - _ = c.Close() + _ = n.Close() return nil, ErrMetadata } + if err := n.audit("node.open", map[string]string{"member": info.MemberID, "fips": fmt.Sprint(c.FIPS()), "pqc": fmt.Sprint(c.PQC())}, nil); err != nil { + _ = n.Close() + return nil, err + } return n, nil } -func (n *Node) Close() error { return n.core.Close() } -func (n *Node) MemberID() string { return n.info.MemberID } -func (n *Node) PQC() bool { return n.info.PQC } -func (n *Node) FIPS() bool { return n.core.FIPS() } +func (n *Node) Close() error { + cerr := n.core.Close() + lerr := n.log.Close() + return errors.Join(cerr, lerr) +} + +func (n *Node) AuditLogPath() string { return n.st.AuditLogPath() } +func (n *Node) MemberID() string { return n.info.MemberID } +func (n *Node) PQC() bool { return n.info.PQC } +func (n *Node) FIPS() bool { return n.core.FIPS() } func (n *Node) Core() *core.Client { return n.core } @@ -206,10 +245,13 @@ func (n *Node) GenerateKey(ctx context.Context, label string, usage uint32) (*st } k, err := n.core.GenerateKey(ctx, wire.AlgAES256GCM, usage, label) if err != nil { - return nil, err + return nil, n.audit("key.generate", map[string]string{"label": label}, err) } rec := &store.KeyRecord{KeyID: store.KeyIDString(k.KeyID), Alg: wire.AlgAES256GCM, Usage: usage, Label: label, Blob: k.Blob, CreatedAt: n.opts.now().Unix()} if err := n.st.PutKey(rec); err != nil { + return nil, n.audit("key.generate", map[string]string{"key": rec.KeyID, "label": label}, err) + } + if err := n.audit("key.generate", map[string]string{"key": rec.KeyID, "label": label, "usage": fmt.Sprint(usage)}, nil); err != nil { return nil, err } return rec, nil @@ -218,17 +260,25 @@ func (n *Node) GenerateKey(ctx context.Context, label string, usage uint32) (*st func (n *Node) Encrypt(ctx context.Context, keyID string, plaintext, aad []byte) ([]byte, error) { rec, err := n.Key(ctx, keyID) if err != nil { + return nil, n.audit("key.encrypt", map[string]string{"key": keyID}, err) + } + ct, err := n.core.Encrypt(ctx, rec.Blob, plaintext, aad) + if err := n.audit("key.encrypt", map[string]string{"key": keyID, "bytes": fmt.Sprint(len(plaintext))}, err); err != nil { return nil, err } - return n.core.Encrypt(ctx, rec.Blob, plaintext, aad) + return ct, nil } func (n *Node) Decrypt(ctx context.Context, keyID string, ciphertext, aad []byte) ([]byte, error) { rec, err := n.Key(ctx, keyID) if err != nil { + return nil, n.audit("key.decrypt", map[string]string{"key": keyID}, err) + } + pt, err := n.core.Decrypt(ctx, rec.Blob, ciphertext, aad) + if err := n.audit("key.decrypt", map[string]string{"key": keyID, "bytes": fmt.Sprint(len(ciphertext))}, err); err != nil { return nil, err } - return n.core.Decrypt(ctx, rec.Blob, ciphertext, aad) + return pt, nil } func (n *Node) Card(ctx context.Context) (*exchange.Card, []byte, error) { @@ -251,6 +301,19 @@ func (n *Node) Card(ctx context.Context) (*exchange.Card, []byte, error) { } func (n *Node) AddMember(ctx context.Context, raw []byte, fingerprint string) (*store.MemberRecord, error) { + rec, err := n.addMember(ctx, raw, fingerprint) + fields := map[string]string{} + if rec != nil { + fields["member"] = rec.MemberID + fields["fingerprint"] = rec.Fingerprint + } + if err := n.audit("member.add", fields, err); err != nil { + return nil, err + } + return rec, nil +} + +func (n *Node) addMember(ctx context.Context, raw []byte, fingerprint string) (*store.MemberRecord, error) { card, err := exchange.DecodeCard(raw) if err != nil { return nil, err @@ -299,6 +362,14 @@ func (n *Node) Member(ctx context.Context, id string) (*exchange.Card, error) { } func (n *Node) Send(ctx context.Context, keyID, to string, usage uint32) ([]byte, error) { + env, err := n.send(ctx, keyID, to, usage) + if err := n.audit("key.send", map[string]string{"key": keyID, "to": to, "usage": fmt.Sprint(usage)}, err); err != nil { + return nil, err + } + return env, nil +} + +func (n *Node) send(ctx context.Context, keyID, to string, usage uint32) ([]byte, error) { rec, err := n.Key(ctx, keyID) if err != nil { return nil, err @@ -330,6 +401,20 @@ func (n *Node) Send(ctx context.Context, keyID, to string, usage uint32) ([]byte } func (n *Node) Receive(ctx context.Context, raw []byte) (*store.KeyRecord, error) { + rec, err := n.receive(ctx, raw) + fields := map[string]string{"envelope": store.InboxID(raw)} + if rec != nil { + fields["key"] = rec.KeyID + fields["from"] = rec.Origin.Sender + fields["usage"] = fmt.Sprint(rec.Usage) + } + if err := n.audit("key.receive", fields, err); err != nil { + return nil, err + } + return rec, nil +} + +func (n *Node) receive(ctx context.Context, raw []byte) (*store.KeyRecord, error) { env, err := exchange.DecodeEnvelope(raw) if err != nil { return nil, err diff --git a/host/internal/node/node_test.go b/host/internal/node/node_test.go index daa6c97..313b756 100644 --- a/host/internal/node/node_test.go +++ b/host/internal/node/node_test.go @@ -6,9 +6,11 @@ import ( "errors" "os" "path/filepath" + "strings" "testing" "time" + "github.com/Quant-Off/pairier/host/internal/audit" "github.com/Quant-Off/pairier/host/internal/core" "github.com/Quant-Off/pairier/host/internal/exchange" "github.com/Quant-Off/pairier/host/internal/store" @@ -163,6 +165,33 @@ func TestExchangeBetweenNodes(t *testing.T) { if _, err := bob.Receive(ctx, env3); !errors.Is(err, store.ErrExists) { t.Fatalf("expected duplicate key id, got %v", err) } + + entries, err := audit.Read(reopened.AuditLogPath()) + if err != nil { + t.Fatal(err) + } + var events []string + for _, e := range entries { + events = append(events, e.Event) + } + joined := strings.Join(events, ",") + for _, want := range []string{"node.init", "member.add", "key.generate", "key.encrypt", "key.send", "node.open"} { + if !strings.Contains(joined, want) { + t.Fatalf("audit log lacks %s: %s", want, joined) + } + } + var failed bool + for _, e := range entries { + if e.Event == "key.send" && e.Fields["to"] == "carol" && e.Fields["error"] != "" { + failed = true + } + } + if !failed { + t.Fatal("failed send to unknown member not audited") + } + if n, err := audit.Verify(bob.AuditLogPath()); err != nil || n == 0 { + t.Fatalf("bob audit %d %v", n, err) + } } func TestStoredMetadataIsVerifiedAgainstCore(t *testing.T) { From 57a07d7931c31a4017feb37b4ac73a79c49b30cb Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:12:25 +0900 Subject: [PATCH 13/48] =?UTF-8?q?=EA=B0=90=EC=82=AC=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=20=EB=8B=A4=EC=A4=91=20=ED=94=84=EB=A1=9C=EC=84=B8=EC=8A=A4=20?= =?UTF-8?q?=EC=95=88=EC=A0=84=EC=84=B1=20-=20flock=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=A7=81=EB=A0=AC=ED=99=94=ED=95=98=EA=B3=A0=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EC=A0=84=20=EA=BC=AC=EB=A6=AC=EB=A5=BC=20=EC=A6=9D?= =?UTF-8?q?=EB=B6=84=20=EC=9E=AC=EA=B2=80=EC=A6=9D=ED=95=B4=20=EC=B2=B4?= =?UTF-8?q?=EC=9D=B8=20=EC=97=B0=EC=86=8D=EC=84=B1=20=EC=9C=A0=EC=A7=80=20?= =?UTF-8?q?-=20=EB=A1=9C=EA=B7=B8=20=EC=B6=95=EC=86=8C=20=ED=83=90?= =?UTF-8?q?=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- host/internal/audit/audit.go | 71 ++++++++++++++++++++++++++----- host/internal/audit/audit_test.go | 34 +++++++++++++++ 2 files changed, 95 insertions(+), 10 deletions(-) diff --git a/host/internal/audit/audit.go b/host/internal/audit/audit.go index 45b6dda..da55729 100644 --- a/host/internal/audit/audit.go +++ b/host/internal/audit/audit.go @@ -13,6 +13,7 @@ import ( "os" "sort" "sync" + "syscall" "time" ) @@ -40,11 +41,12 @@ type Entry struct { } type Log struct { - mu sync.Mutex - f *os.File - seq uint64 - prev string - now func() time.Time + mu sync.Mutex + f *os.File + seq uint64 + prev string + offset int64 + now func() time.Time } func Open(path string, now func() time.Time) (*Log, error) { @@ -68,12 +70,50 @@ func Open(path string, now func() time.Time) (*Log, error) { _ = f.Close() return nil, fmt.Errorf("%w: %s is a symlink", ErrInsecure, path) } - seq, prev, err := replay(f) + l := &Log{f: f, prev: genesis, now: now} + if err := l.lock(); err != nil { + _ = f.Close() + return nil, err + } + err = l.catchUp() + l.unlock() if err != nil { _ = f.Close() return nil, err } - return &Log{f: f, seq: seq, prev: prev, now: now}, nil + return l, nil +} + +func (l *Log) lock() error { + if err := syscall.Flock(int(l.f.Fd()), syscall.LOCK_EX); err != nil { + return fmt.Errorf("audit: lock: %w", err) + } + return nil +} + +func (l *Log) unlock() { + _ = syscall.Flock(int(l.f.Fd()), syscall.LOCK_UN) +} + +func (l *Log) catchUp() error { + fi, err := l.f.Stat() + if err != nil { + return err + } + size := fi.Size() + if size < l.offset { + return fmt.Errorf("%w: log shrank from %d to %d bytes", ErrCorrupt, l.offset, size) + } + if size == l.offset { + return nil + } + section := io.NewSectionReader(l.f, l.offset, size-l.offset) + seq, prev, err := replayFrom(section, l.seq, l.prev) + if err != nil { + return err + } + l.seq, l.prev, l.offset = seq, prev, size + return nil } func (l *Log) Close() error { @@ -99,6 +139,13 @@ func (l *Log) Append(event string, fields map[string]string) error { } l.mu.Lock() defer l.mu.Unlock() + if err := l.lock(); err != nil { + return err + } + defer l.unlock() + if err := l.catchUp(); err != nil { + return err + } e := &Entry{Seq: l.seq + 1, Time: l.now().Unix(), Event: event, Fields: fields, Prev: l.prev} h, err := digest(e) if err != nil { @@ -110,7 +157,8 @@ func (l *Log) Append(event string, fields map[string]string) error { return err } line = append(line, '\n') - if _, err := l.f.Write(line); err != nil { + n, err := l.f.Write(line) + if err != nil { return err } if err := l.f.Sync(); err != nil { @@ -118,6 +166,7 @@ func (l *Log) Append(event string, fields map[string]string) error { } l.seq = e.Seq l.prev = e.Hash + l.offset += int64(n) return nil } @@ -153,8 +202,10 @@ func digest(e *Entry) (string, error) { } func replay(r io.Reader) (uint64, string, error) { - var seq uint64 - prev := genesis + return replayFrom(r, 0, genesis) +} + +func replayFrom(r io.Reader, seq uint64, prev string) (uint64, string, error) { sc := bufio.NewScanner(r) sc.Buffer(make([]byte, 0, maxLine), maxLine) for sc.Scan() { diff --git a/host/internal/audit/audit_test.go b/host/internal/audit/audit_test.go index 25da715..85aea1c 100644 --- a/host/internal/audit/audit_test.go +++ b/host/internal/audit/audit_test.go @@ -88,3 +88,37 @@ func TestChainAppendVerifyAndTamper(t *testing.T) { t.Fatalf("permissive mode accepted: %v", err) } } + +func TestConcurrentHandlesShareOneChain(t *testing.T) { + path := filepath.Join(t.TempDir(), "audit.log") + a, err := Open(path, nil) + if err != nil { + t.Fatal(err) + } + defer a.Close() + b, err := Open(path, nil) + if err != nil { + t.Fatal(err) + } + defer b.Close() + for i := 0; i < 5; i++ { + if err := a.Append("a", nil); err != nil { + t.Fatal(err) + } + if err := b.Append("b", nil); err != nil { + t.Fatal(err) + } + } + if a.Seq() != 9 || b.Seq() != 10 { + t.Fatalf("seq %d %d", a.Seq(), b.Seq()) + } + if n, err := Verify(path); err != nil || n != 10 { + t.Fatalf("verify %d %v", n, err) + } + if err := os.Truncate(path, 10); err != nil { + t.Fatal(err) + } + if err := a.Append("a", nil); !errors.Is(err, ErrCorrupt) { + t.Fatalf("shrunk log not detected: %v", err) + } +} From 1c6a08686b48780b89f7202b7b9a2095201d0152 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:12:52 +0900 Subject: [PATCH 14/48] =?UTF-8?q?TLS=20=EC=A0=84=EC=86=A1=20=ED=82=A4=20?= =?UTF-8?q?=EB=8F=84=EC=9E=85=20-=20=EB=85=B8=EB=93=9C=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=ED=99=94=20=EC=8B=9C=20P-256=20TLS=20=ED=82=A4?= =?UTF-8?q?=EC=99=80=20=EC=9E=90=EA=B8=B0=20=EC=84=9C=EB=AA=85=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=EC=84=9C=20=EC=83=9D=EC=84=B1,=20=EA=B0=9C=EC=9D=B8?= =?UTF-8?q?=ED=82=A4=EB=8A=94=20=EC=BD=94=EC=96=B4=20=EB=B4=89=EC=9D=B8=20?= =?UTF-8?q?=ED=82=A4=EB=A1=9C=20=EC=95=94=ED=98=B8=ED=99=94=ED=95=B4=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20-=20=EC=8B=A0=EC=9B=90=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=EC=97=90=20TLS=20=EA=B3=B5=EA=B0=9C=ED=82=A4(SPKI)?= =?UTF-8?q?=EB=A5=BC=20=ED=8F=AC=ED=95=A8=ED=95=B4=20=EC=A7=80=EB=AC=B8?= =?UTF-8?q?=EA=B3=BC=20=EC=9E=90=EA=B8=B0=20=EC=84=9C=EB=AA=85=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B0=94=EC=9D=B8=EB=94=A9=20-=20TLS=20=EA=B3=B5?= =?UTF-8?q?=EA=B0=9C=ED=82=A4=EB=A1=9C=20=EA=B5=AC=EC=84=B1=EC=9B=90?= =?UTF-8?q?=EC=9D=84=20=EC=B0=BE=EB=8A=94=20=EC=A1=B0=ED=9A=8C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- host/cmd/kmsctl/main_test.go | 2 +- host/internal/exchange/card.go | 8 +- host/internal/exchange/exchange_test.go | 14 ++- host/internal/node/node.go | 11 ++ host/internal/node/node_test.go | 46 ++++++++- host/internal/node/tls.go | 131 ++++++++++++++++++++++++ host/internal/store/store.go | 29 ++++++ host/internal/store/store_test.go | 10 ++ 8 files changed, 246 insertions(+), 5 deletions(-) create mode 100644 host/internal/node/tls.go diff --git a/host/cmd/kmsctl/main_test.go b/host/cmd/kmsctl/main_test.go index cacb296..da7a903 100644 --- a/host/cmd/kmsctl/main_test.go +++ b/host/cmd/kmsctl/main_test.go @@ -128,7 +128,7 @@ func TestEndToEndExchange(t *testing.T) { if !strings.Contains(info, "origin\talice") || !strings.Contains(info, "usage\tdecrypt\n") { t.Fatalf("info %q", info) } - if list := must(nil, "--dir", bob, "key", "list"); strings.Count(list, "\n") != 3 { + if list := must(nil, "--dir", bob, "key", "list"); strings.Count(list, "\n") != 4 { t.Fatalf("bob key list %q", list) } } diff --git a/host/internal/exchange/card.go b/host/internal/exchange/card.go index c59f81f..d7ba99b 100644 --- a/host/internal/exchange/card.go +++ b/host/internal/exchange/card.go @@ -12,7 +12,7 @@ import ( const ( CardVersion = 1 - MaxCard = 4 + 1 + 4 + MaxMemberID + 2 + 4 + wire.MaxPublicKey + 2 + 4 + wire.MaxPublicKey + 8 + 4 + wire.MaxSignature + MaxCard = 4 + 1 + 4 + MaxMemberID + 2 + 4 + wire.MaxPublicKey + 2 + 4 + wire.MaxPublicKey + 4 + wire.MaxPublicKey + 8 + 4 + wire.MaxSignature ) var cardMagic = [4]byte{'K', 'I', 'D', '1'} @@ -23,6 +23,7 @@ type Card struct { IdentityPublicKey []byte KemAlg uint16 KemPublicKey []byte + TLSPublicKey []byte CreatedAt uint64 Signature []byte } @@ -36,6 +37,7 @@ func (c *Card) validate() error { } if len(c.IdentityPublicKey) == 0 || len(c.IdentityPublicKey) > wire.MaxPublicKey || len(c.KemPublicKey) == 0 || len(c.KemPublicKey) > wire.MaxPublicKey || + len(c.TLSPublicKey) == 0 || len(c.TLSPublicKey) > wire.MaxPublicKey || len(c.Signature) > wire.MaxSignature { return ErrFormat } @@ -53,6 +55,7 @@ func (c *Card) signedBytes() ([]byte, error) { w.Bytes(c.IdentityPublicKey) w.U16(c.KemAlg) w.Bytes(c.KemPublicKey) + w.Bytes(c.TLSPublicKey) w.U64(c.CreatedAt) body, err := w.Finish() if err != nil { @@ -100,6 +103,9 @@ func DecodeCard(b []byte) (*Card, error) { if c.KemPublicKey, err = r.Bytes(wire.MaxPublicKey); err != nil { return nil, ErrFormat } + if c.TLSPublicKey, err = r.Bytes(wire.MaxPublicKey); err != nil { + return nil, ErrFormat + } if c.CreatedAt, err = r.U64(); err != nil { return nil, ErrFormat } diff --git a/host/internal/exchange/exchange_test.go b/host/internal/exchange/exchange_test.go index 367acfb..78bc0eb 100644 --- a/host/internal/exchange/exchange_test.go +++ b/host/internal/exchange/exchange_test.go @@ -76,7 +76,7 @@ func TestEnvelopeAndCardEncoding(t *testing.T) { t.Fatal("non-signature alg accepted") } - card := &Card{MemberID: "bob", IdentityAlg: wire.AlgEd25519, IdentityPublicKey: make([]byte, 32), KemAlg: wire.AlgDHKEMP256, KemPublicKey: make([]byte, 65), CreatedAt: 7, Signature: []byte("s")} + card := &Card{MemberID: "bob", IdentityAlg: wire.AlgEd25519, IdentityPublicKey: make([]byte, 32), KemAlg: wire.AlgDHKEMP256, KemPublicKey: make([]byte, 65), TLSPublicKey: make([]byte, 91), CreatedAt: 7, Signature: []byte("s")} cb2, err := card.Encode() if err != nil { t.Fatal(err) @@ -98,6 +98,16 @@ func TestEnvelopeAndCardEncoding(t *testing.T) { if fp3 == fp1 { t.Fatal("fingerprint ignores kem key") } + dc.KemPublicKey[0] ^= 1 + dc.TLSPublicKey[0] ^= 1 + fp4, _ := dc.Fingerprint() + if fp4 == fp1 { + t.Fatal("fingerprint ignores tls key") + } + dc.TLSPublicKey = nil + if _, err := dc.Encode(); !errors.Is(err, ErrFormat) { + t.Fatal("missing tls key accepted") + } card.KemAlg = wire.AlgEd25519 if _, err := card.Encode(); !errors.Is(err, ErrAlgorithm) { t.Fatal("kem alg not validated") @@ -143,7 +153,7 @@ func TestSignedEnvelopeAndCardWithRealCore(t *testing.T) { if err != nil { t.Fatal(err) } - card := &Card{MemberID: "bob", IdentityAlg: sigAlg, IdentityPublicKey: bid.PublicKey, KemAlg: kemAlg, KemPublicKey: bkem.PublicKey, CreatedAt: 1} + card := &Card{MemberID: "bob", IdentityAlg: sigAlg, IdentityPublicKey: bid.PublicKey, KemAlg: kemAlg, KemPublicKey: bkem.PublicKey, TLSPublicKey: []byte("spki"), CreatedAt: 1} if err := SignCard(ctx, b, bid.Blob, card); err != nil { t.Fatal(err) } diff --git a/host/internal/node/node.go b/host/internal/node/node.go index ab3ca4e..76a10db 100644 --- a/host/internal/node/node.go +++ b/host/internal/node/node.go @@ -3,6 +3,7 @@ package node import ( "bytes" "context" + "crypto/tls" "errors" "fmt" "io" @@ -49,6 +50,7 @@ type Node struct { info *store.Node identity *store.KeyRecord kem *store.KeyRecord + tls tls.Certificate } func (n *Node) audit(event string, fields map[string]string, err error) error { @@ -113,6 +115,10 @@ func Init(ctx context.Context, o Options, memberID string, pqc bool) (*Node, err _ = n.Close() return nil, err } + if err := n.createTLS(ctx); err != nil { + _ = n.Close() + return nil, err + } if err := n.audit("node.init", map[string]string{"member": memberID, "identity": n.identity.KeyID, "kem": n.kem.KeyID, "pqc": fmt.Sprint(pqc), "fips": fmt.Sprint(c.FIPS())}, nil); err != nil { _ = n.Close() return nil, err @@ -198,6 +204,10 @@ func Open(ctx context.Context, o Options) (*Node, error) { _ = n.Close() return nil, ErrMetadata } + if err := n.loadTLS(ctx); err != nil { + _ = n.Close() + return nil, err + } if err := n.audit("node.open", map[string]string{"member": info.MemberID, "fips": fmt.Sprint(c.FIPS()), "pqc": fmt.Sprint(c.PQC())}, nil); err != nil { _ = n.Close() return nil, err @@ -288,6 +298,7 @@ func (n *Node) Card(ctx context.Context) (*exchange.Card, []byte, error) { IdentityPublicKey: n.identity.PublicKey, KemAlg: n.kem.Alg, KemPublicKey: n.kem.PublicKey, + TLSPublicKey: n.TLSPublicKey(), CreatedAt: uint64(n.info.CreatedAt), } if err := exchange.SignCard(ctx, n.core, n.identity.Blob, card); err != nil { diff --git a/host/internal/node/node_test.go b/host/internal/node/node_test.go index 313b756..33706bb 100644 --- a/host/internal/node/node_test.go +++ b/host/internal/node/node_test.go @@ -1,6 +1,7 @@ package node import ( + "bytes" "context" "encoding/json" "errors" @@ -152,7 +153,7 @@ func TestExchangeBetweenNodes(t *testing.T) { } defer reopened.Close() keys, err := reopened.Keys() - if err != nil || len(keys) != 3 { + if err != nil || len(keys) != 4 { t.Fatalf("keys after reopen %d %v", len(keys), err) } if _, err := reopened.Key(ctx, k.KeyID); err != nil { @@ -274,3 +275,46 @@ func TestPQCNodes(t *testing.T) { t.Fatalf("node without pqc must refuse a pqc card, got %v", err) } } + +func TestTLSMaterialAndPeerLookup(t *testing.T) { + ctx := context.Background() + alice, aliceOpts := initNode(t, "alice", false) + bob, _ := initNode(t, "bob", false) + if alice.TLSCertificate().Leaf == nil || len(alice.TLSPublicKey()) == 0 { + t.Fatal("tls material missing after init") + } + card, _, err := alice.Card(ctx) + if err != nil || !bytes.Equal(card.TLSPublicKey, alice.TLSPublicKey()) { + t.Fatalf("card tls key mismatch: %v", err) + } + pair(t, alice, bob) + found, err := alice.MemberByTLSKey(ctx, bob.TLSPublicKey()) + if err != nil || found.MemberID != "bob" { + t.Fatalf("lookup: %+v %v", found, err) + } + if _, err := alice.MemberByTLSKey(ctx, alice.TLSPublicKey()); !errors.Is(err, ErrUnknownMember) { + t.Fatalf("own key resolved to a member: %v", err) + } + if _, err := alice.MemberByTLSKey(ctx, nil); !errors.Is(err, ErrUnknownMember) { + t.Fatalf("empty key resolved: %v", err) + } + spki := alice.TLSPublicKey() + if err := alice.Close(); err != nil { + t.Fatal(err) + } + reopened, err := Open(ctx, aliceOpts) + if err != nil { + t.Fatal(err) + } + defer reopened.Close() + if !bytes.Equal(reopened.TLSPublicKey(), spki) { + t.Fatal("tls key changed after reopen") + } + if reopened.TLSCertificate().Leaf.Subject.CommonName != "alice" { + t.Fatal("certificate subject") + } + keys, _ := reopened.Keys() + if len(keys) != 3 { + t.Fatalf("expected identity, kem and tls-wrap keys, got %d", len(keys)) + } +} diff --git a/host/internal/node/tls.go b/host/internal/node/tls.go new file mode 100644 index 0000000..cf94aaa --- /dev/null +++ b/host/internal/node/tls.go @@ -0,0 +1,131 @@ +package node + +import ( + "bytes" + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "errors" + "fmt" + "math/big" + "time" + + "github.com/Quant-Off/pairier/host/internal/exchange" + "github.com/Quant-Off/pairier/host/internal/store" + "github.com/Quant-Off/pairier/host/internal/wire" +) + +const tlsValidity = 10 * 365 * 24 * time.Hour + +var ErrTLSMaterial = errors.New("node: tls material is inconsistent") + +func (n *Node) createTLS(ctx context.Context) error { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return err + } + keyDER, err := x509.MarshalPKCS8PrivateKey(priv) + if err != nil { + return err + } + serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 127)) + if err != nil { + return err + } + now := n.opts.now() + tmpl := &x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{CommonName: n.info.MemberID}, + NotBefore: now.Add(-time.Hour), + NotAfter: now.Add(tlsValidity), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + } + certDER, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &priv.PublicKey, priv) + if err != nil { + return err + } + wrap, err := n.core.GenerateKey(ctx, wire.AlgAES256GCM, wire.UsageEncrypt|wire.UsageDecrypt, "tls-wrap/"+n.info.MemberID) + if err != nil { + return err + } + wrapRec := &store.KeyRecord{KeyID: store.KeyIDString(wrap.KeyID), Alg: wire.AlgAES256GCM, Usage: wire.UsageEncrypt | wire.UsageDecrypt, Label: "tls-wrap/" + n.info.MemberID, Blob: wrap.Blob, CreatedAt: now.Unix()} + if err := n.st.PutKey(wrapRec); err != nil { + return err + } + ct, err := n.core.Encrypt(ctx, wrap.Blob, keyDER, certDER) + if err != nil { + return err + } + if err := n.st.PutTLS(&store.TLSRecord{Cert: certDER, WrapKeyID: wrapRec.KeyID, KeyCiphertext: ct, CreatedAt: now.Unix()}); err != nil { + return err + } + return n.loadTLS(ctx) +} + +func (n *Node) loadTLS(ctx context.Context) error { + rec, err := n.st.GetTLS() + if err != nil { + return err + } + wrap, err := n.Key(ctx, rec.WrapKeyID) + if err != nil { + return fmt.Errorf("node: tls wrap key: %w", err) + } + keyDER, err := n.core.Decrypt(ctx, wrap.Blob, rec.KeyCiphertext, rec.Cert) + if err != nil { + return fmt.Errorf("node: tls key: %w", err) + } + key, err := x509.ParsePKCS8PrivateKey(keyDER) + if err != nil { + return ErrTLSMaterial + } + priv, ok := key.(*ecdsa.PrivateKey) + if !ok { + return ErrTLSMaterial + } + leaf, err := x509.ParseCertificate(rec.Cert) + if err != nil { + return ErrTLSMaterial + } + pub, ok := leaf.PublicKey.(*ecdsa.PublicKey) + if !ok || !pub.Equal(&priv.PublicKey) || leaf.Subject.CommonName != n.info.MemberID { + return ErrTLSMaterial + } + n.tls = tls.Certificate{Certificate: [][]byte{rec.Cert}, PrivateKey: priv, Leaf: leaf} + return nil +} + +func (n *Node) TLSCertificate() tls.Certificate { return n.tls } + +func (n *Node) TLSPublicKey() []byte { + if n.tls.Leaf == nil { + return nil + } + return n.tls.Leaf.RawSubjectPublicKeyInfo +} + +func (n *Node) MemberByTLSKey(ctx context.Context, spki []byte) (*exchange.Card, error) { + if len(spki) == 0 { + return nil, ErrUnknownMember + } + members, err := n.Members() + if err != nil { + return nil, err + } + for _, m := range members { + card, err := n.Member(ctx, m.MemberID) + if err != nil { + return nil, err + } + if bytes.Equal(card.TLSPublicKey, spki) { + return card, nil + } + } + return nil, ErrUnknownMember +} diff --git a/host/internal/store/store.go b/host/internal/store/store.go index 9233919..6efd1c8 100644 --- a/host/internal/store/store.go +++ b/host/internal/store/store.go @@ -24,6 +24,7 @@ const ( membersDir = "members" inboxDir = "inbox" nodeFile = "node.json" + tlsFile = "tls.json" rootFile = "root.key" auditFile = "audit.log" ) @@ -77,6 +78,13 @@ type Node struct { CreatedAt int64 `json:"created_at"` } +type TLSRecord struct { + Cert []byte `json:"cert"` + WrapKeyID string `json:"wrap_key_id"` + KeyCiphertext []byte `json:"key_ciphertext"` + CreatedAt int64 `json:"created_at"` +} + type InboxRecord struct { ID string `json:"id"` Envelope []byte `json:"envelope"` @@ -260,6 +268,27 @@ func (s *Store) GetNode() (*Node, error) { return &n, nil } +func (s *Store) PutTLS(r *TLSRecord) error { + if !ValidKeyID(r.WrapKeyID) || len(r.Cert) == 0 || len(r.KeyCiphertext) == 0 { + return ErrBadID + } + return s.writeNew(tlsFile, r) +} + +func (s *Store) GetTLS() (*TLSRecord, error) { + var r TLSRecord + if err := s.read(tlsFile, &r); err != nil { + if errors.Is(err, ErrNotFound) { + return nil, ErrNotInited + } + return nil, err + } + if !ValidKeyID(r.WrapKeyID) || len(r.Cert) == 0 || len(r.KeyCiphertext) == 0 { + return nil, ErrCorrupt + } + return &r, nil +} + func (s *Store) PutInbox(r *InboxRecord) error { if !ValidKeyID(r.ID) || r.ID != InboxID(r.Envelope) { return ErrBadID diff --git a/host/internal/store/store_test.go b/host/internal/store/store_test.go index e2cff73..dea1a95 100644 --- a/host/internal/store/store_test.go +++ b/host/internal/store/store_test.go @@ -157,6 +157,16 @@ func TestMembersNodeAndInbox(t *testing.T) { if err != nil || got.MemberID != "alice" || !got.PQC || got.KemKeyID != n.KemKeyID { t.Fatalf("node: %+v %v", got, err) } + if _, err := s.GetTLS(); !errors.Is(err, ErrNotInited) { + t.Fatalf("expected ErrNotInited, got %v", err) + } + tr := &TLSRecord{Cert: []byte("cert"), WrapKeyID: KeyIDString([16]byte{4}), KeyCiphertext: []byte("ct"), CreatedAt: 1} + if err := s.PutTLS(tr); err != nil { + t.Fatal(err) + } + if got, err := s.GetTLS(); err != nil || string(got.Cert) != "cert" || got.WrapKeyID != tr.WrapKeyID { + t.Fatalf("tls: %+v %v", got, err) + } env := []byte("envelope") rec := &InboxRecord{ID: InboxID(env), Envelope: env, ReceivedAt: 9} if err := s.PutInbox(rec); err != nil { From 13545d24a6ee2ba8ce90a88fc8eb3f8ea8298933 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:16:42 +0900 Subject: [PATCH 15/48] =?UTF-8?q?=EA=B5=AC=EC=84=B1=EC=9B=90=20=EA=B0=84?= =?UTF-8?q?=20mTLS=20inbox=20API=20=EA=B5=AC=ED=98=84=20-=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=EC=9D=98=20TLS=20=EA=B3=B5=EA=B0=9C=ED=82=A4=EB=A5=BC?= =?UTF-8?q?=20=EA=B3=A0=EC=A0=95=ED=95=B4=20=EC=84=9C=EB=B2=84=EC=99=80=20?= =?UTF-8?q?=ED=81=B4=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8=EB=A5=BC=20?= =?UTF-8?q?=EC=83=81=ED=98=B8=20=EC=9D=B8=EC=A6=9D,=20CA=20=EC=97=86?= =?UTF-8?q?=EC=9D=B4=20=EB=AA=85=EB=B6=80=EB=A7=8C=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=8B=A0=EB=A2=B0=20=EA=B2=B0=EC=A0=95=20-=20POST=20/v1/inbox?= =?UTF-8?q?=EB=8A=94=20=EC=84=9C=EB=AA=85=EA=B3=BC=20=EC=88=98=EC=8B=A0?= =?UTF-8?q?=EC=9E=90,=20=EC=9D=B8=EC=A6=9D=EB=90=9C=20=ED=94=BC=EC=96=B4?= =?UTF-8?q?=EC=99=80=20=EC=86=A1=EC=8B=A0=EC=9E=90=20=EC=9D=BC=EC=B9=98?= =?UTF-8?q?=EB=A5=BC=20=EA=B2=80=EC=A6=9D=ED=95=9C=20=EB=92=A4=20=EB=B3=B4?= =?UTF-8?q?=EA=B4=80=20-=20=EB=85=B8=EB=93=9C=EC=97=90=20inbox=20=EB=B3=B4?= =?UTF-8?q?=EA=B4=80,=20=EB=AA=A9=EB=A1=9D,=20=EC=88=98=EB=9D=BD,=20?= =?UTF-8?q?=EA=B1=B0=EC=A0=88=20=EC=B6=94=EA=B0=80=EC=99=80=20push=20?= =?UTF-8?q?=ED=81=B4=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- host/internal/api/api.go | 204 ++++++++++++++++++++++++++++++++++ host/internal/api/api_test.go | 155 ++++++++++++++++++++++++++ host/internal/auth/auth.go | 114 +++++++++++++++++++ host/internal/node/node.go | 112 ++++++++++++++++--- 4 files changed, 570 insertions(+), 15 deletions(-) create mode 100644 host/internal/api/api_test.go diff --git a/host/internal/api/api.go b/host/internal/api/api.go index 778f64e..7bbda7a 100644 --- a/host/internal/api/api.go +++ b/host/internal/api/api.go @@ -1 +1,205 @@ package api + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "time" + + "github.com/Quant-Off/pairier/host/internal/auth" + "github.com/Quant-Off/pairier/host/internal/exchange" + "github.com/Quant-Off/pairier/host/internal/node" + "github.com/Quant-Off/pairier/host/internal/store" +) + +const ( + maxInboxBody = exchange.MaxEnvelope + requestTimeout = 15 * time.Second +) + +type Server struct { + node *node.Node + now func() time.Time +} + +func New(n *node.Node, now func() time.Time) *Server { + if now == nil { + now = time.Now + } + return &Server{node: n, now: now} +} + +func (s *Server) Handler() http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("GET /v1/health", s.health) + mux.HandleFunc("GET /v1/card", s.card) + mux.HandleFunc("POST /v1/inbox", s.inbox) + return http.TimeoutHandler(secure(mux), requestTimeout, "timeout") +} + +func (s *Server) TLSConfig() *tls.Config { + return auth.ServerTLSConfig(s.node.TLSCertificate(), s.node, s.now) +} + +func secure(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h := w.Header() + h.Set("Cache-Control", "no-store") + h.Set("X-Content-Type-Options", "nosniff") + h.Set("Referrer-Policy", "no-referrer") + next.ServeHTTP(w, r) + }) +} + +type errorBody struct { + Error string `json:"error"` +} + +func writeJSON(w http.ResponseWriter, status int, v any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(v) +} + +func fail(w http.ResponseWriter, status int, msg string) { + writeJSON(w, status, errorBody{Error: msg}) +} + +func (s *Server) peer(r *http.Request) (*exchange.Card, error) { + ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) + defer cancel() + return auth.Peer(ctx, s.node, r.TLS, s.now()) +} + +func (s *Server) health(w http.ResponseWriter, r *http.Request) { + if _, err := s.peer(r); err != nil { + fail(w, http.StatusUnauthorized, "unauthorized") + return + } + writeJSON(w, http.StatusOK, map[string]any{"member": s.node.MemberID(), "fips": s.node.FIPS(), "pqc": s.node.PQC()}) +} + +func (s *Server) card(w http.ResponseWriter, r *http.Request) { + if _, err := s.peer(r); err != nil { + fail(w, http.StatusUnauthorized, "unauthorized") + return + } + _, raw, err := s.node.Card(r.Context()) + if err != nil { + fail(w, http.StatusInternalServerError, "card unavailable") + return + } + w.Header().Set("Content-Type", "application/octet-stream") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(raw) +} + +func (s *Server) inbox(w http.ResponseWriter, r *http.Request) { + peer, err := s.peer(r) + if err != nil { + fail(w, http.StatusUnauthorized, "unauthorized") + return + } + if r.ContentLength > maxInboxBody { + fail(w, http.StatusRequestEntityTooLarge, "envelope too large") + return + } + raw, err := io.ReadAll(io.LimitReader(r.Body, maxInboxBody+1)) + if err != nil || len(raw) > maxInboxBody { + fail(w, http.StatusRequestEntityTooLarge, "envelope too large") + return + } + rec, err := s.node.StoreInbox(r.Context(), raw, peer.MemberID) + switch { + case err == nil: + writeJSON(w, http.StatusCreated, map[string]string{"id": rec.ID}) + case errors.Is(err, store.ErrExists): + fail(w, http.StatusConflict, "duplicate envelope") + case errors.Is(err, node.ErrNotRecipient), errors.Is(err, node.ErrSenderMismatch), + errors.Is(err, exchange.ErrFormat), errors.Is(err, exchange.ErrSignature), + errors.Is(err, exchange.ErrBadMemberID), errors.Is(err, exchange.ErrAlgorithm), + errors.Is(err, node.ErrSignatureAlg): + fail(w, http.StatusBadRequest, "envelope rejected") + default: + fail(w, http.StatusInternalServerError, "inbox unavailable") + } +} + +type Client struct { + node *node.Node + now func() time.Time +} + +func NewClient(n *node.Node, now func() time.Time) *Client { + if now == nil { + now = time.Now + } + return &Client{node: n, now: now} +} + +func (c *Client) httpClient(peer *exchange.Card) *http.Client { + return &http.Client{ + Timeout: requestTimeout, + Transport: &http.Transport{ + TLSClientConfig: auth.ClientTLSConfig(c.node.TLSCertificate(), peer, c.now), + ForceAttemptHTTP2: false, + Proxy: nil, + }, + } +} + +func (c *Client) Push(ctx context.Context, baseURL, to string, envelope []byte) (string, error) { + peer, err := c.node.Member(ctx, to) + if err != nil { + return "", err + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+"/v1/inbox", bytes.NewReader(envelope)) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/octet-stream") + rsp, err := c.httpClient(peer).Do(req) + if err != nil { + return "", fmt.Errorf("api: push: %w", err) + } + defer rsp.Body.Close() + body, _ := io.ReadAll(io.LimitReader(rsp.Body, 4096)) + if rsp.StatusCode != http.StatusCreated { + var eb errorBody + _ = json.Unmarshal(body, &eb) + return "", fmt.Errorf("api: push rejected: %d %s", rsp.StatusCode, eb.Error) + } + var out struct { + ID string `json:"id"` + } + if err := json.Unmarshal(body, &out); err != nil || !store.ValidKeyID(out.ID) { + return "", errors.New("api: push: malformed response") + } + return out.ID, nil +} + +func (c *Client) Health(ctx context.Context, baseURL, member string) error { + peer, err := c.node.Member(ctx, member) + if err != nil { + return err + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/v1/health", nil) + if err != nil { + return err + } + rsp, err := c.httpClient(peer).Do(req) + if err != nil { + return fmt.Errorf("api: health: %w", err) + } + defer rsp.Body.Close() + _, _ = io.Copy(io.Discard, io.LimitReader(rsp.Body, 4096)) + if rsp.StatusCode != http.StatusOK { + return fmt.Errorf("api: health: status %d", rsp.StatusCode) + } + return nil +} diff --git a/host/internal/api/api_test.go b/host/internal/api/api_test.go new file mode 100644 index 0000000..cc635ea --- /dev/null +++ b/host/internal/api/api_test.go @@ -0,0 +1,155 @@ +package api + +import ( + "context" + "errors" + "net" + "net/http" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/Quant-Off/pairier/host/internal/exchange" + "github.com/Quant-Off/pairier/host/internal/node" + "github.com/Quant-Off/pairier/host/internal/wire" +) + +func initNode(t *testing.T, id string) *node.Node { + t.Helper() + bin := os.Getenv("KMS_CORE_BIN") + if bin == "" { + t.Skip("KMS_CORE_BIN not set") + } + n, err := node.Init(context.Background(), node.Options{Dir: filepath.Join(t.TempDir(), id), CoreBinary: bin, AllowUnlockedMemory: true, Timeout: 5 * time.Second}, id, false) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = n.Close() }) + return n +} + +func trust(t *testing.T, a, b *node.Node) { + t.Helper() + ctx := context.Background() + ca, rawA, _ := a.Card(ctx) + cb, rawB, _ := b.Card(ctx) + fpA, _ := ca.Fingerprint() + fpB, _ := cb.Fingerprint() + if _, err := a.AddMember(ctx, rawB, fpB); err != nil { + t.Fatal(err) + } + if _, err := b.AddMember(ctx, rawA, fpA); err != nil { + t.Fatal(err) + } +} + +func serve(t *testing.T, n *node.Node) string { + t.Helper() + srv := New(n, nil) + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + hs := &http.Server{Handler: srv.Handler(), TLSConfig: srv.TLSConfig(), ReadHeaderTimeout: 5 * time.Second} + go func() { _ = hs.ServeTLS(ln, "", "") }() + t.Cleanup(func() { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + _ = hs.Shutdown(ctx) + }) + return "https://" + ln.Addr().String() +} + +func TestPushInboxAndAccept(t *testing.T) { + ctx := context.Background() + alice := initNode(t, "alice") + bob := initNode(t, "bob") + carol := initNode(t, "carol") + trust(t, alice, bob) + trust(t, alice, carol) + bobURL := serve(t, bob) + + if err := NewClient(alice, nil).Health(ctx, bobURL, "bob"); err != nil { + t.Fatalf("health: %v", err) + } + if err := NewClient(carol, nil).Health(ctx, bobURL, "alice"); err == nil { + t.Fatal("pinned to the wrong member yet connection succeeded") + } + if err := NewClient(carol, nil).Health(ctx, bobURL, "bob"); err == nil { + t.Fatal("unknown client certificate accepted by bob") + } + + k, err := alice.GenerateKey(ctx, "shared", wire.UsageEncrypt|wire.UsageDecrypt|wire.UsageExport) + if err != nil { + t.Fatal(err) + } + ct, err := alice.Encrypt(ctx, k.KeyID, []byte("hello bob"), nil) + if err != nil { + t.Fatal(err) + } + env, err := alice.Send(ctx, k.KeyID, "bob", wire.UsageDecrypt) + if err != nil { + t.Fatal(err) + } + id, err := NewClient(alice, nil).Push(ctx, bobURL, "bob", env) + if err != nil { + t.Fatalf("push: %v", err) + } + if _, err := NewClient(alice, nil).Push(ctx, bobURL, "bob", env); err == nil || !strings.Contains(err.Error(), "409") { + t.Fatalf("duplicate push accepted: %v", err) + } + envCarol, err := alice.Send(ctx, k.KeyID, "carol", wire.UsageDecrypt) + if err != nil { + t.Fatal(err) + } + if _, err := NewClient(alice, nil).Push(ctx, bobURL, "bob", envCarol); err == nil || !strings.Contains(err.Error(), "400") { + t.Fatalf("misaddressed envelope accepted: %v", err) + } + tampered := append([]byte{}, env...) + tampered[len(tampered)-1] ^= 1 + if _, err := NewClient(alice, nil).Push(ctx, bobURL, "bob", tampered); err == nil || !strings.Contains(err.Error(), "400") { + t.Fatalf("tampered envelope accepted: %v", err) + } + + items, err := bob.Inbox() + if err != nil || len(items) != 1 || items[0].ID != id || items[0].Sender != "alice" { + t.Fatalf("inbox %+v %v", items, err) + } + rec, err := bob.Accept(ctx, id) + if err != nil { + t.Fatal(err) + } + if rec.KeyID != k.KeyID || rec.Origin.Sender != "alice" { + t.Fatalf("accepted %+v", rec) + } + pt, err := bob.Decrypt(ctx, rec.KeyID, ct, nil) + if err != nil || string(pt) != "hello bob" { + t.Fatalf("decrypt %q %v", pt, err) + } + if items, _ := bob.Inbox(); len(items) != 0 { + t.Fatal("inbox not cleared after accept") + } + if _, err := bob.Accept(ctx, id); err == nil { + t.Fatal("accept of a missing item succeeded") + } + + env2, _ := alice.Send(ctx, k.KeyID, "bob", wire.UsageDecrypt) + id2, err := NewClient(alice, nil).Push(ctx, bobURL, "bob", env2) + if err != nil { + t.Fatal(err) + } + if err := bob.Reject(ctx, id2); err != nil { + t.Fatal(err) + } + if items, _ := bob.Inbox(); len(items) != 0 { + t.Fatal("inbox not cleared after reject") + } + if _, err := bob.StoreInbox(ctx, env2, "carol"); !errors.Is(err, node.ErrSenderMismatch) { + t.Fatalf("sender mismatch accepted: %v", err) + } + if _, err := bob.StoreInbox(ctx, []byte("junk"), "alice"); !errors.Is(err, exchange.ErrFormat) { + t.Fatalf("junk accepted: %v", err) + } +} diff --git a/host/internal/auth/auth.go b/host/internal/auth/auth.go index 8832b06..276d256 100644 --- a/host/internal/auth/auth.go +++ b/host/internal/auth/auth.go @@ -1 +1,115 @@ package auth + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "time" + + "github.com/Quant-Off/pairier/host/internal/exchange" +) + +var ( + ErrNoCertificate = errors.New("auth: peer presented no certificate") + ErrCertificate = errors.New("auth: peer certificate rejected") + ErrUnknownPeer = errors.New("auth: peer is not a known member") +) + +type Resolver interface { + MemberByTLSKey(ctx context.Context, spki []byte) (*exchange.Card, error) +} + +func leaf(raw [][]byte, now time.Time) (*x509.Certificate, error) { + if len(raw) == 0 { + return nil, ErrNoCertificate + } + c, err := x509.ParseCertificate(raw[0]) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrCertificate, err) + } + if now.Before(c.NotBefore) || now.After(c.NotAfter) { + return nil, fmt.Errorf("%w: outside validity period", ErrCertificate) + } + if c.PublicKeyAlgorithm != x509.ECDSA { + return nil, fmt.Errorf("%w: unsupported key algorithm", ErrCertificate) + } + return c, nil +} + +func Peer(ctx context.Context, r Resolver, cs *tls.ConnectionState, now time.Time) (*exchange.Card, error) { + if cs == nil || len(cs.PeerCertificates) == 0 { + return nil, ErrNoCertificate + } + c, err := leaf([][]byte{cs.PeerCertificates[0].Raw}, now) + if err != nil { + return nil, err + } + card, err := r.MemberByTLSKey(ctx, c.RawSubjectPublicKeyInfo) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrUnknownPeer, err) + } + if c.Subject.CommonName != card.MemberID { + return nil, fmt.Errorf("%w: subject does not match member", ErrCertificate) + } + return card, nil +} + +func ServerTLSConfig(cert tls.Certificate, r Resolver, now func() time.Time) *tls.Config { + return &tls.Config{ + MinVersion: tls.VersionTLS13, + Certificates: []tls.Certificate{cert}, + ClientAuth: tls.RequireAnyClientCert, + VerifyPeerCertificate: func(raw [][]byte, _ [][]*x509.Certificate) error { + c, err := leaf(raw, now()) + if err != nil { + return err + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + card, err := r.MemberByTLSKey(ctx, c.RawSubjectPublicKeyInfo) + if err != nil { + return fmt.Errorf("%w: %v", ErrUnknownPeer, err) + } + if c.Subject.CommonName != card.MemberID { + return fmt.Errorf("%w: subject does not match member", ErrCertificate) + } + return nil + }, + } +} + +func ClientTLSConfig(cert tls.Certificate, peer *exchange.Card, now func() time.Time) *tls.Config { + expected := append([]byte{}, peer.TLSPublicKey...) + memberID := peer.MemberID + return &tls.Config{ + MinVersion: tls.VersionTLS13, + Certificates: []tls.Certificate{cert}, + InsecureSkipVerify: true, + VerifyPeerCertificate: func(raw [][]byte, _ [][]*x509.Certificate) error { + c, err := leaf(raw, now()) + if err != nil { + return err + } + if !equalBytes(c.RawSubjectPublicKeyInfo, expected) { + return fmt.Errorf("%w: server key is not the pinned member key", ErrCertificate) + } + if c.Subject.CommonName != memberID { + return fmt.Errorf("%w: subject does not match member", ErrCertificate) + } + return nil + }, + } +} + +func equalBytes(a, b []byte) bool { + if len(a) != len(b) { + return false + } + var diff byte + for i := range a { + diff |= a[i] ^ b[i] + } + return diff == 0 +} diff --git a/host/internal/node/node.go b/host/internal/node/node.go index 76a10db..3439653 100644 --- a/host/internal/node/node.go +++ b/host/internal/node/node.go @@ -29,6 +29,7 @@ var ( ErrNotSymmetric = errors.New("node: only symmetric keys can be generated or exchanged") ErrSignatureAlg = errors.New("node: envelope signature algorithm differs from the member card") ErrUsageNotAllowed = errors.New("node: usage not allowed for this key kind") + ErrSenderMismatch = errors.New("node: envelope sender differs from the authenticated peer") ) const SymmetricUsage = wire.UsageEncrypt | wire.UsageDecrypt | wire.UsageWrap | wire.UsageUnwrap | wire.UsageDerive | wire.UsageExport @@ -426,43 +427,124 @@ func (n *Node) Receive(ctx context.Context, raw []byte) (*store.KeyRecord, error } func (n *Node) receive(ctx context.Context, raw []byte) (*store.KeyRecord, error) { - env, err := exchange.DecodeEnvelope(raw) + env, ectx, err := n.verifyEnvelope(ctx, raw) if err != nil { return nil, err } - peek, err := exchange.DecodeContext(env.Context) + imp, err := n.core.ImportKey(ctx, n.kem.Blob, env.Package, env.Context) if err != nil { return nil, err } + rec := &store.KeyRecord{ + KeyID: store.KeyIDString(imp.KeyID), + Alg: imp.Alg, + Usage: imp.Usage, + Label: imp.Label, + Blob: imp.Blob, + CreatedAt: n.opts.now().Unix(), + Origin: &store.Origin{Sender: ectx.Sender, SentAt: int64(ectx.CreatedAt)}, + } + if err := n.st.PutKey(rec); err != nil { + return nil, err + } + return rec, nil +} + +func (n *Node) verifyEnvelope(ctx context.Context, raw []byte) (*exchange.Envelope, *exchange.Context, error) { + env, err := exchange.DecodeEnvelope(raw) + if err != nil { + return nil, nil, err + } + peek, err := exchange.DecodeContext(env.Context) + if err != nil { + return nil, nil, err + } if peek.Recipient != n.info.MemberID { - return nil, ErrNotRecipient + return nil, nil, ErrNotRecipient } card, err := n.Member(ctx, peek.Sender) if err != nil { - return nil, err + return nil, nil, err } if env.SigAlg != card.IdentityAlg { - return nil, ErrSignatureAlg + return nil, nil, ErrSignatureAlg } ectx, err := env.Verify(ctx, n.core, card.IdentityPublicKey) if err != nil { + return nil, nil, err + } + return env, ectx, nil +} + +func (n *Node) StoreInbox(ctx context.Context, raw []byte, from string) (*store.InboxRecord, error) { + rec, err := n.storeInbox(ctx, raw, from) + fields := map[string]string{"from": from, "envelope": store.InboxID(raw)} + if err := n.audit("inbox.put", fields, err); err != nil { return nil, err } - imp, err := n.core.ImportKey(ctx, n.kem.Blob, env.Package, env.Context) + return rec, nil +} + +func (n *Node) storeInbox(ctx context.Context, raw []byte, from string) (*store.InboxRecord, error) { + _, ectx, err := n.verifyEnvelope(ctx, raw) if err != nil { return nil, err } - rec := &store.KeyRecord{ - KeyID: store.KeyIDString(imp.KeyID), - Alg: imp.Alg, - Usage: imp.Usage, - Label: imp.Label, - Blob: imp.Blob, - CreatedAt: n.opts.now().Unix(), - Origin: &store.Origin{Sender: ectx.Sender, SentAt: int64(ectx.CreatedAt)}, + if ectx.Sender != from { + return nil, ErrSenderMismatch } - if err := n.st.PutKey(rec); err != nil { + rec := &store.InboxRecord{ID: store.InboxID(raw), Envelope: raw, ReceivedAt: n.opts.now().Unix()} + if err := n.st.PutInbox(rec); err != nil { return nil, err } return rec, nil } + +type InboxItem struct { + ID string + Sender string + SentAt int64 + ReceivedAt int64 +} + +func (n *Node) Inbox() ([]InboxItem, error) { + recs, err := n.st.ListInbox() + if err != nil { + return nil, err + } + out := make([]InboxItem, 0, len(recs)) + for _, r := range recs { + item := InboxItem{ID: r.ID, ReceivedAt: r.ReceivedAt} + if env, err := exchange.DecodeEnvelope(r.Envelope); err == nil { + if c, err := exchange.DecodeContext(env.Context); err == nil { + item.Sender = c.Sender + item.SentAt = int64(c.CreatedAt) + } + } + out = append(out, item) + } + return out, nil +} + +func (n *Node) Accept(ctx context.Context, id string) (*store.KeyRecord, error) { + rec, err := n.st.GetInbox(id) + if err != nil { + return nil, err + } + key, err := n.Receive(ctx, rec.Envelope) + if err != nil { + return nil, err + } + if err := n.st.DeleteInbox(id); err != nil { + return nil, err + } + if err := n.audit("inbox.accept", map[string]string{"envelope": id, "key": key.KeyID}, nil); err != nil { + return nil, err + } + return key, nil +} + +func (n *Node) Reject(ctx context.Context, id string) error { + err := n.st.DeleteInbox(id) + return n.audit("inbox.reject", map[string]string{"envelope": id}, err) +} From 8a7e68c0462e0703c3f8adce13eeccba333cf0d5 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:17:14 +0900 Subject: [PATCH 16/48] =?UTF-8?q?kmsd=20=EB=8D=B0=EB=AA=AC=EA=B3=BC=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95,=20kmsctl=20=EC=98=A8=EB=9D=BC=EC=9D=B8=20?= =?UTF-8?q?=EB=AA=85=EB=A0=B9=20=EC=B6=94=EA=B0=80=20-=20kmsd=EB=8A=94=20?= =?UTF-8?q?=EB=85=B8=EB=93=9C=EB=A5=BC=20=EC=97=B4=EA=B3=A0=20mTLS=20inbox?= =?UTF-8?q?=20API=EB=A5=BC=20=EC=A0=9C=EA=B3=B5,=20SIGTERM=20=EC=8B=9C=20?= =?UTF-8?q?=EC=A0=95=EC=83=81=20=EC=A2=85=EB=A3=8C,=20--version=20-=20JSON?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95=20=ED=8C=8C=EC=9D=BC=20(=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=ED=95=84=EB=93=9C=20=EA=B1=B0=EB=B6=80,=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EA=B2=80=EC=82=AC)=EA=B3=BC=20=ED=94=8C?= =?UTF-8?q?=EB=9E=98=EA=B7=B8=20=EC=9A=B0=EC=84=A0=EC=88=9C=EC=9C=84=20-?= =?UTF-8?q?=20kmsctl=20key=20push,=20inbox=20list/accept/reject,=20peer=20?= =?UTF-8?q?health?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- host/cmd/kmsctl/main.go | 140 ++++++++++++++++++++++++ host/cmd/kmsd/main.go | 162 +++++++++++++++++++++++++++- host/cmd/kmsd/main_test.go | 115 ++++++++++++++++++++ host/internal/config/config.go | 81 ++++++++++++++ host/internal/config/config_test.go | 50 +++++++++ 5 files changed, 547 insertions(+), 1 deletion(-) create mode 100644 host/cmd/kmsd/main_test.go create mode 100644 host/internal/config/config_test.go diff --git a/host/cmd/kmsctl/main.go b/host/cmd/kmsctl/main.go index 6874360..120d5a5 100644 --- a/host/cmd/kmsctl/main.go +++ b/host/cmd/kmsctl/main.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "github.com/Quant-Off/pairier/host/internal/api" "github.com/Quant-Off/pairier/host/internal/node" "github.com/Quant-Off/pairier/host/internal/wire" ) @@ -30,6 +31,11 @@ commands: key info --id KEYID key send --id KEYID --to MEMBER [--usage decrypt] [--out FILE] key receive --in FILE + key push --id KEYID --to MEMBER --url https://HOST:PORT [--usage decrypt] + inbox list + inbox accept --id ENVELOPEID + inbox reject --id ENVELOPEID + peer health --member MEMBER --url https://HOST:PORT encrypt --id KEYID [--in FILE] [--out FILE] [--aad STRING] decrypt --id KEYID [--in FILE] [--out FILE] [--aad STRING] version @@ -152,8 +158,28 @@ func (c *cli) dispatch(args []string) (int, error) { return c.cmdKeySend(ctx, args[2:]) case "receive": return c.cmdKeyReceive(ctx, args[2:]) + case "push": + return c.cmdKeyPush(ctx, args[2:]) } return 2, fmt.Errorf("key: unknown subcommand %q", args[1]) + case "inbox": + if len(args) < 2 { + return 2, errors.New("inbox: subcommand required (list, accept, reject)") + } + switch args[1] { + case "list": + return c.cmdInboxList(ctx, args[2:]) + case "accept": + return c.cmdInboxAccept(ctx, args[2:]) + case "reject": + return c.cmdInboxReject(ctx, args[2:]) + } + return 2, fmt.Errorf("inbox: unknown subcommand %q", args[1]) + case "peer": + if len(args) < 2 || args[1] != "health" { + return 2, errors.New("peer: subcommand required (health)") + } + return c.cmdPeerHealth(ctx, args[2:]) case "encrypt": return c.cmdCrypt(ctx, args[1:], true) case "decrypt": @@ -398,6 +424,120 @@ func (c *cli) cmdKeyReceive(ctx context.Context, args []string) (int, error) { return 0, nil } +func (c *cli) cmdKeyPush(ctx context.Context, args []string) (int, error) { + fs := c.flags("key push") + id := fs.String("id", "", "key id") + to := fs.String("to", "", "recipient member id") + url := fs.String("url", "", "recipient kmsd base url") + usage := fs.String("usage", "decrypt", "usage granted to the recipient") + if err := fs.Parse(args); err != nil || fs.NArg() != 0 { + return 2, errors.New("key push: invalid arguments") + } + if *id == "" || *to == "" || *url == "" { + return 2, errors.New("key push: --id, --to and --url are required") + } + if !strings.HasPrefix(*url, "https://") { + return 2, errors.New("key push: --url must use https") + } + bits, err := parseUsage(*usage) + if err != nil { + return 2, err + } + n, err := c.open(ctx) + if err != nil { + return 1, err + } + defer n.Close() + env, err := n.Send(ctx, *id, *to, bits) + if err != nil { + return 1, err + } + envID, err := api.NewClient(n, nil).Push(ctx, strings.TrimRight(*url, "/"), *to, env) + if err != nil { + return 1, err + } + fmt.Fprintf(c.stdout, "pushed\t%s\tto\t%s\tenvelope\t%s\n", *id, *to, envID) + return 0, nil +} + +func (c *cli) cmdInboxList(ctx context.Context, args []string) (int, error) { + if len(args) != 0 { + return 2, errors.New("inbox list: no arguments expected") + } + n, err := c.open(ctx) + if err != nil { + return 1, err + } + defer n.Close() + items, err := n.Inbox() + if err != nil { + return 1, err + } + for _, it := range items { + fmt.Fprintf(c.stdout, "%s\t%s\t%s\t%s\n", it.ID, it.Sender, time.Unix(it.SentAt, 0).UTC().Format(time.RFC3339), time.Unix(it.ReceivedAt, 0).UTC().Format(time.RFC3339)) + } + return 0, nil +} + +func (c *cli) cmdInboxAccept(ctx context.Context, args []string) (int, error) { + fs := c.flags("inbox accept") + id := fs.String("id", "", "envelope id") + if err := fs.Parse(args); err != nil || fs.NArg() != 0 || *id == "" { + return 2, errors.New("inbox accept: --id is required") + } + n, err := c.open(ctx) + if err != nil { + return 1, err + } + defer n.Close() + rec, err := n.Accept(ctx, *id) + if err != nil { + return 1, err + } + fmt.Fprintf(c.stdout, "%s\t%s\t%s\t%s\tfrom %s\n", rec.KeyID, algName(rec.Alg), formatUsage(rec.Usage), rec.Label, rec.Origin.Sender) + return 0, nil +} + +func (c *cli) cmdInboxReject(ctx context.Context, args []string) (int, error) { + fs := c.flags("inbox reject") + id := fs.String("id", "", "envelope id") + if err := fs.Parse(args); err != nil || fs.NArg() != 0 || *id == "" { + return 2, errors.New("inbox reject: --id is required") + } + n, err := c.open(ctx) + if err != nil { + return 1, err + } + defer n.Close() + if err := n.Reject(ctx, *id); err != nil { + return 1, err + } + fmt.Fprintf(c.stdout, "rejected\t%s\n", *id) + return 0, nil +} + +func (c *cli) cmdPeerHealth(ctx context.Context, args []string) (int, error) { + fs := c.flags("peer health") + member := fs.String("member", "", "member id") + url := fs.String("url", "", "member kmsd base url") + if err := fs.Parse(args); err != nil || fs.NArg() != 0 || *member == "" || *url == "" { + return 2, errors.New("peer health: --member and --url are required") + } + if !strings.HasPrefix(*url, "https://") { + return 2, errors.New("peer health: --url must use https") + } + n, err := c.open(ctx) + if err != nil { + return 1, err + } + defer n.Close() + if err := api.NewClient(n, nil).Health(ctx, strings.TrimRight(*url, "/"), *member); err != nil { + return 1, err + } + fmt.Fprintf(c.stdout, "ok\t%s\n", *member) + return 0, nil +} + func (c *cli) cmdCrypt(ctx context.Context, args []string, encrypt bool) (int, error) { name := "decrypt" if encrypt { diff --git a/host/cmd/kmsd/main.go b/host/cmd/kmsd/main.go index 38dd16d..5a9a635 100644 --- a/host/cmd/kmsd/main.go +++ b/host/cmd/kmsd/main.go @@ -1,3 +1,163 @@ package main -func main() {} +import ( + "context" + "errors" + "flag" + "fmt" + "io" + "net" + "net/http" + "os" + "os/signal" + "path/filepath" + "runtime" + "syscall" + "time" + + "github.com/Quant-Off/pairier/host/internal/api" + "github.com/Quant-Off/pairier/host/internal/config" + "github.com/Quant-Off/pairier/host/internal/node" +) + +var version = "dev" + +const ( + defaultListen = "127.0.0.1:7443" + defaultTimeout = 10 * time.Second +) + +type settings struct { + node node.Options + listen string +} + +func main() { + os.Exit(run(os.Args[1:], os.Stdout, os.Stderr, nil)) +} + +func run(args []string, stdout, stderr io.Writer, ready func(addr string)) int { + fs := flag.NewFlagSet("kmsd", flag.ContinueOnError) + fs.SetOutput(stderr) + cfgPath := fs.String("config", "", "configuration file (json)") + dir := fs.String("dir", "", "node directory") + corePath := fs.String("core", "", "kms-core binary (default next to kmsd)") + listen := fs.String("listen", "", "listen address host:port") + allowUnlocked := fs.Bool("allow-unlocked-memory", runtime.GOOS == "darwin", "run the core without locked memory") + timeout := fs.Duration("timeout", defaultTimeout, "core call timeout") + showVersion := fs.Bool("version", false, "print version and exit") + if err := fs.Parse(args); err != nil { + return 2 + } + if *showVersion { + fmt.Fprintf(stdout, "kmsd %s\n", version) + return 0 + } + if fs.NArg() != 0 { + fmt.Fprintln(stderr, "kmsd: unexpected arguments") + return 2 + } + set := map[string]bool{} + fs.Visit(func(f *flag.Flag) { set[f.Name] = true }) + + s := settings{listen: defaultListen, node: node.Options{AllowUnlockedMemory: *allowUnlocked, Timeout: *timeout, Stderr: stderr}} + if *cfgPath != "" { + c, err := config.Load(*cfgPath) + if err != nil { + fmt.Fprintf(stderr, "kmsd: %v\n", err) + return 2 + } + s.node.Dir = c.Dir + s.node.CoreBinary = c.Core + if c.Listen != "" { + s.listen = c.Listen + } + if c.AllowUnlockedMemory != nil { + s.node.AllowUnlockedMemory = *c.AllowUnlockedMemory + } + s.node.Timeout = c.TimeoutDuration(*timeout) + } + if set["dir"] || s.node.Dir == "" { + s.node.Dir = *dir + } + if set["core"] || s.node.CoreBinary == "" { + s.node.CoreBinary = *corePath + } + if set["listen"] { + s.listen = *listen + } + if set["allow-unlocked-memory"] { + s.node.AllowUnlockedMemory = *allowUnlocked + } + if set["timeout"] { + s.node.Timeout = *timeout + } + if s.node.Dir == "" { + fmt.Fprintln(stderr, "kmsd: --dir or config dir is required") + return 2 + } + if s.node.CoreBinary == "" { + if exe, err := os.Executable(); err == nil { + s.node.CoreBinary = filepath.Join(filepath.Dir(exe), "kms-core") + } + } + if err := (&config.Config{Listen: s.listen}).Validate(); err != nil { + fmt.Fprintf(stderr, "kmsd: %v\n", err) + return 2 + } + + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + if err := serve(ctx, s, stderr, ready); err != nil { + fmt.Fprintf(stderr, "kmsd: %v\n", err) + return 1 + } + return 0 +} + +func serve(ctx context.Context, s settings, stderr io.Writer, ready func(addr string)) error { + n, err := node.Open(ctx, s.node) + if err != nil { + return err + } + defer func() { + if err := n.Close(); err != nil { + fmt.Fprintf(stderr, "kmsd: close: %v\n", err) + } + }() + srv := api.New(n, nil) + hs := &http.Server{ + Handler: srv.Handler(), + TLSConfig: srv.TLSConfig(), + ReadHeaderTimeout: 10 * time.Second, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 60 * time.Second, + MaxHeaderBytes: 16 << 10, + } + ln, err := net.Listen("tcp", s.listen) + if err != nil { + return err + } + fmt.Fprintf(stderr, "kmsd %s member %s listening on %s fips=%v pqc=%v\n", version, n.MemberID(), ln.Addr(), n.FIPS(), n.PQC()) + if ready != nil { + ready(ln.Addr().String()) + } + errc := make(chan error, 1) + go func() { errc <- hs.ServeTLS(ln, "", "") }() + select { + case err := <-errc: + if errors.Is(err, http.ErrServerClosed) { + return nil + } + return err + case <-ctx.Done(): + } + shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := hs.Shutdown(shutdownCtx); err != nil { + _ = hs.Close() + } + fmt.Fprintln(stderr, "kmsd: stopped") + return nil +} diff --git a/host/cmd/kmsd/main_test.go b/host/cmd/kmsd/main_test.go new file mode 100644 index 0000000..2451872 --- /dev/null +++ b/host/cmd/kmsd/main_test.go @@ -0,0 +1,115 @@ +package main + +import ( + "bytes" + "context" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/Quant-Off/pairier/host/internal/api" + "github.com/Quant-Off/pairier/host/internal/node" + "github.com/Quant-Off/pairier/host/internal/wire" +) + +func TestFlagsAndVersion(t *testing.T) { + var out, errb bytes.Buffer + if code := run([]string{"--version"}, &out, &errb, nil); code != 0 || !strings.HasPrefix(out.String(), "kmsd ") { + t.Fatalf("%d %q", code, out.String()) + } + if code := run(nil, &out, &errb, nil); code != 2 { + t.Fatalf("missing dir accepted: %d", code) + } + if code := run([]string{"--dir", t.TempDir(), "--listen", "nope"}, &out, &errb, nil); code != 2 { + t.Fatalf("bad listen accepted: %d", code) + } + cfg := filepath.Join(t.TempDir(), "c.json") + _ = os.WriteFile(cfg, []byte(`{"dir":"/nonexistent/x","listen":"127.0.0.1:0"}`), 0o600) + if code := run([]string{"--config", cfg}, &out, &errb, nil); code != 1 { + t.Fatalf("unopenable node should fail at runtime, got %d: %s", code, errb.String()) + } +} + +func TestServeAndShutdown(t *testing.T) { + bin := os.Getenv("KMS_CORE_BIN") + if bin == "" { + t.Skip("KMS_CORE_BIN not set") + } + ctx := context.Background() + mk := func(id string) (*node.Node, node.Options) { + o := node.Options{Dir: filepath.Join(t.TempDir(), id), CoreBinary: bin, AllowUnlockedMemory: true, Timeout: 5 * time.Second} + n, err := node.Init(ctx, o, id, false) + if err != nil { + t.Fatal(err) + } + return n, o + } + bob, bobOpts := mk("bob") + alice, _ := mk("alice") + defer alice.Close() + ca, rawA, _ := alice.Card(ctx) + cb, rawB, _ := bob.Card(ctx) + fpA, _ := ca.Fingerprint() + fpB, _ := cb.Fingerprint() + if _, err := bob.AddMember(ctx, rawA, fpA); err != nil { + t.Fatal(err) + } + if _, err := alice.AddMember(ctx, rawB, fpB); err != nil { + t.Fatal(err) + } + if err := bob.Close(); err != nil { + t.Fatal(err) + } + + sctx, cancel := context.WithCancel(ctx) + addr := make(chan string, 1) + done := make(chan error, 1) + var stderr bytes.Buffer + go func() { + done <- serve(sctx, settings{listen: "127.0.0.1:0", node: bobOpts}, &stderr, func(a string) { addr <- a }) + }() + var bobAddr string + select { + case bobAddr = <-addr: + case err := <-done: + t.Fatalf("serve exited early: %v\n%s", err, stderr.String()) + case <-time.After(10 * time.Second): + t.Fatal("server did not become ready") + } + k, err := alice.GenerateKey(ctx, "k", wire.UsageDecrypt|wire.UsageExport) + if err != nil { + t.Fatal(err) + } + env, err := alice.Send(ctx, k.KeyID, "bob", wire.UsageDecrypt) + if err != nil { + t.Fatal(err) + } + pushCtx, stopPush := context.WithTimeout(ctx, 10*time.Second) + defer stopPush() + if _, err := api.NewClient(alice, nil).Push(pushCtx, "https://"+bobAddr, "bob", env); err != nil { + t.Fatalf("push: %v\n%s", err, stderr.String()) + } + cancel() + select { + case err := <-done: + if err != nil { + t.Fatalf("serve: %v", err) + } + case <-time.After(15 * time.Second): + t.Fatal("server did not stop") + } + if !strings.Contains(stderr.String(), "stopped") { + t.Fatalf("stderr %q", stderr.String()) + } + reopened, err := node.Open(ctx, bobOpts) + if err != nil { + t.Fatal(err) + } + defer reopened.Close() + items, err := reopened.Inbox() + if err != nil || len(items) != 1 || items[0].Sender != "alice" { + t.Fatalf("inbox %+v %v", items, err) + } +} diff --git a/host/internal/config/config.go b/host/internal/config/config.go index d912156..af863ca 100644 --- a/host/internal/config/config.go +++ b/host/internal/config/config.go @@ -1 +1,82 @@ package config + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/fs" + "net" + "os" + "time" +) + +var ErrInvalid = errors.New("config: invalid configuration") + +type Config struct { + Dir string `json:"dir"` + Core string `json:"core"` + Listen string `json:"listen"` + AllowUnlockedMemory *bool `json:"allow_unlocked_memory,omitempty"` + Timeout string `json:"timeout,omitempty"` +} + +func Load(path string) (*Config, error) { + fi, err := os.Lstat(path) + if err != nil { + return nil, err + } + if fi.Mode()&fs.ModeSymlink != 0 || !fi.Mode().IsRegular() { + return nil, fmt.Errorf("%w: %s is not a regular file", ErrInvalid, path) + } + if fi.Mode().Perm()&0o022 != 0 { + return nil, fmt.Errorf("%w: %s is writable by group or others", ErrInvalid, path) + } + raw, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var c Config + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&c); err != nil { + return nil, fmt.Errorf("%w: %v", ErrInvalid, err) + } + if dec.More() { + return nil, fmt.Errorf("%w: trailing data", ErrInvalid) + } + if err := c.Validate(); err != nil { + return nil, err + } + return &c, nil +} + +func (c *Config) Validate() error { + if c.Listen != "" { + host, port, err := net.SplitHostPort(c.Listen) + if err != nil || port == "" { + return fmt.Errorf("%w: listen must be host:port", ErrInvalid) + } + if host != "" && net.ParseIP(host) == nil { + return fmt.Errorf("%w: listen host must be an ip address", ErrInvalid) + } + } + if c.Timeout != "" { + d, err := time.ParseDuration(c.Timeout) + if err != nil || d <= 0 { + return fmt.Errorf("%w: timeout must be a positive duration", ErrInvalid) + } + } + return nil +} + +func (c *Config) TimeoutDuration(def time.Duration) time.Duration { + if c.Timeout == "" { + return def + } + d, err := time.ParseDuration(c.Timeout) + if err != nil || d <= 0 { + return def + } + return d +} diff --git a/host/internal/config/config_test.go b/host/internal/config/config_test.go new file mode 100644 index 0000000..e2e3bff --- /dev/null +++ b/host/internal/config/config_test.go @@ -0,0 +1,50 @@ +package config + +import ( + "errors" + "os" + "path/filepath" + "testing" + "time" +) + +func write(t *testing.T, body string, mode os.FileMode) string { + t.Helper() + p := filepath.Join(t.TempDir(), "kmsd.json") + if err := os.WriteFile(p, []byte(body), mode); err != nil { + t.Fatal(err) + } + return p +} + +func TestLoadAndValidate(t *testing.T) { + c, err := Load(write(t, `{"dir":"/var/lib/kms","core":"/usr/bin/kms-core","listen":"127.0.0.1:7443","allow_unlocked_memory":false,"timeout":"20s"}`, 0o600)) + if err != nil { + t.Fatal(err) + } + if c.Dir != "/var/lib/kms" || c.Listen != "127.0.0.1:7443" || c.AllowUnlockedMemory == nil || *c.AllowUnlockedMemory || c.TimeoutDuration(time.Second) != 20*time.Second { + t.Fatalf("%+v", c) + } + for name, body := range map[string]string{ + "unknown field": `{"dir":"x","extra":1}`, + "bad listen": `{"listen":"nohost"}`, + "dns listen": `{"listen":"localhost:1"}`, + "bad timeout": `{"timeout":"-1s"}`, + "trailing": `{"dir":"x"}{}`, + } { + if _, err := Load(write(t, body, 0o600)); !errors.Is(err, ErrInvalid) { + t.Fatalf("%s accepted: %v", name, err) + } + } + loose := write(t, `{"dir":"x"}`, 0o600) + if err := os.Chmod(loose, 0o666); err != nil { + t.Fatal(err) + } + if _, err := Load(loose); !errors.Is(err, ErrInvalid) { + t.Fatalf("world writable config accepted: %v", err) + } + c, err = Load(write(t, `{"listen":":7443"}`, 0o644)) + if err != nil || c.TimeoutDuration(3*time.Second) != 3*time.Second { + t.Fatalf("%+v %v", c, err) + } +} From 8da3f25c030366d373f5eb81c9dd4f8c492f503e Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:18:49 +0900 Subject: [PATCH 17/48] =?UTF-8?q?=EC=BD=94=EC=96=B4=20--version=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=EC=99=80=20Go=20=EC=99=80=EC=9D=B4=EC=96=B4?= =?UTF-8?q?=20fuzz=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20-=20kms-core=20--versio?= =?UTF-8?q?n=EC=9D=80=20=EC=97=94=EC=A7=84=20=EB=B2=84=EC=A0=84,=20fips,?= =?UTF-8?q?=20pqc=20=EB=B9=8C=EB=93=9C=20=EC=97=AC=EB=B6=80=EB=A5=BC=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5=ED=95=98=EA=B3=A0=20=EC=84=9C=EB=B9=99=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=97=90=20=EB=B2=84=EC=A0=84=20=ED=8F=AC?= =?UTF-8?q?=ED=95=A8=20-=20=ED=97=A4=EB=8D=94=EC=99=80=20=EB=B3=B8?= =?UTF-8?q?=EB=AC=B8=20=EB=94=94=EC=BD=94=EB=8D=94=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20go=20fuzz=20=ED=83=80=EA=B2=9F=20(=EB=94=94?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=ED=9B=84=20=EC=9E=AC=EC=9D=B8=EC=BD=94?= =?UTF-8?q?=EB=94=A9=20=EC=9D=BC=EC=B9=98=20=EA=B2=80=EC=82=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/crates/kms-core/src/args.rs | 20 +++++++++++++- core/crates/kms-core/src/main.rs | 16 +++++++++-- host/internal/wire/fuzz_test.go | 46 ++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 host/internal/wire/fuzz_test.go diff --git a/core/crates/kms-core/src/args.rs b/core/crates/kms-core/src/args.rs index dcb18a1..9673008 100644 --- a/core/crates/kms-core/src/args.rs +++ b/core/crates/kms-core/src/args.rs @@ -26,9 +26,10 @@ pub(crate) struct Config { pub(crate) enum Action { Serve(Config), GenerateRootKey(PathBuf), + Version, } -const USAGE: &str = "usage: kms-core (--fd N | --socket PATH) (--root-key-file PATH | --ephemeral-root) [--pqc] [--allow-unlocked-memory] [--peer-uid UID]\n kms-core --generate-root-key PATH"; +const USAGE: &str = "usage: kms-core (--fd N | --socket PATH) (--root-key-file PATH | --ephemeral-root) [--pqc] [--allow-unlocked-memory] [--peer-uid UID]\n kms-core --generate-root-key PATH\n kms-core --version"; pub(crate) fn parse(argv: &[String]) -> Result { let mut transport: Option = None; @@ -37,6 +38,7 @@ pub(crate) fn parse(argv: &[String]) -> Result { let mut pqc = false; let mut allow_unlocked_memory = false; let mut peer_uid: Option = None; + let mut version = false; let mut it = argv.iter(); while let Some(flag) = it.next() { @@ -76,6 +78,7 @@ pub(crate) fn parse(argv: &[String]) -> Result { set_once(&mut generate, PathBuf::from(v), "--generate-root-key")?; } "--pqc" => pqc = true, + "--version" => version = true, "--allow-unlocked-memory" => allow_unlocked_memory = true, "--peer-uid" => { let v = value()?; @@ -86,6 +89,19 @@ pub(crate) fn parse(argv: &[String]) -> Result { } } + if version { + if transport.is_some() + || root.is_some() + || generate.is_some() + || pqc + || allow_unlocked_memory + || peer_uid.is_some() + { + return Err(format!("--version takes no other options\n{USAGE}")); + } + return Ok(Action::Version); + } + if let Some(path) = generate { if transport.is_some() || root.is_some() @@ -166,6 +182,8 @@ mod tests { Action::GenerateRootKey(PathBuf::from("/k")) ); assert!(parse(&args("--generate-root-key /k --pqc")).is_err()); + assert_eq!(parse(&args("--version")).unwrap(), Action::Version); + assert!(parse(&args("--version --fd 3")).is_err()); assert!(parse(&args("--fd 3")).is_err()); assert!(parse(&args("--ephemeral-root")).is_err()); assert!(parse(&args("--fd -1 --ephemeral-root")).is_err()); diff --git a/core/crates/kms-core/src/main.rs b/core/crates/kms-core/src/main.rs index 2a0ef10..37c5be5 100644 --- a/core/crates/kms-core/src/main.rs +++ b/core/crates/kms-core/src/main.rs @@ -1,5 +1,5 @@ #![forbid(unsafe_code)] -#![allow(clippy::print_stderr)] +#![allow(clippy::print_stderr, clippy::print_stdout)] mod args; mod rootfile; @@ -10,6 +10,8 @@ use std::process::ExitCode; use kms_engine::Engine; +const VERSION: &str = env!("CARGO_PKG_VERSION"); + use crate::args::{Action, Config, RootSource}; fn log(msg: &str) { @@ -36,6 +38,16 @@ fn run(argv: &[String]) -> Result<(), String> { Ok(()) } Action::Serve(cfg) => serve(&cfg), + Action::Version => { + println!( + "kms-core {VERSION} ({} {}, fips={}, pqc={})", + kms_engine::ENGINE_NAME, + kms_engine::ENGINE_VERSION, + Engine::fips_mode(), + cfg!(feature = "pqc") + ); + Ok(()) + } } } @@ -79,7 +91,7 @@ fn serve(cfg: &Config) -> Result<(), String> { let engine = Engine::new(root, cfg.pqc); log(&format!( - "serving peer pid {} (fips={}, pqc={})", + "{VERSION} serving peer pid {} (fips={}, pqc={})", cred.pid.map_or_else(|| "?".to_owned(), |p| p.to_string()), Engine::fips_mode(), engine.pqc_available() diff --git a/host/internal/wire/fuzz_test.go b/host/internal/wire/fuzz_test.go new file mode 100644 index 0000000..22df113 --- /dev/null +++ b/host/internal/wire/fuzz_test.go @@ -0,0 +1,46 @@ +package wire + +import ( + "bytes" + "testing" +) + +func FuzzDecodeHeader(f *testing.F) { + for _, h := range []Header{ + {Kind: KindRequest, Op: OpHello, ID: 1, Len: 4}, + {Kind: KindResponse, Status: StatusCryptoFailure, Op: OpAeadEncrypt, ID: 7}, + } { + b, _ := h.Encode() + f.Add(b[:]) + } + f.Fuzz(func(t *testing.T, b []byte) { + h, err := DecodeHeader(b) + if err != nil { + return + } + re, err := h.Encode() + if err != nil || !bytes.Equal(re[:], b) { + t.Fatalf("header %+v did not re-encode to its input", h) + } + }) +} + +func FuzzDecodeBody(f *testing.F) { + f.Add(uint16(OpHello), uint8(KindRequest), []byte{0, 0, 0, 1}) + f.Add(uint16(OpKeyGenerate), uint8(KindRequest), []byte{0, 1, 0, 0, 0, 3, 0, 0, 0, 1, 'x'}) + f.Add(uint16(OpKeyExport), uint8(KindResponse), []byte{0, 0, 0, 2, 1, 2}) + f.Add(uint16(OpVerify), uint8(KindResponse), []byte{1}) + f.Fuzz(func(t *testing.T, op uint16, kind uint8, body []byte) { + m := NewMessage(Op(op), Kind(kind)) + if m == nil { + return + } + if err := DecodeBody(m, body); err != nil { + return + } + re, err := EncodeBody(m) + if err != nil || !bytes.Equal(re, body) { + t.Fatalf("op 0x%04x kind %d: decoded body did not re-encode to its input", op, kind) + } + }) +} From 71b5257c68bd0bd94bfc51bde42102ffe43958f2 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:27:43 +0900 Subject: [PATCH 18/48] =?UTF-8?q?=EB=A6=B4=EB=A6=AC=EC=A6=88=20=EB=B9=8C?= =?UTF-8?q?=EB=93=9C=EC=99=80=20=EA=B3=B5=EA=B8=89=EB=A7=9D=20=EC=A0=90?= =?UTF-8?q?=EA=B2=80=20=EA=B5=AC=EC=84=B1=20-=20make=20dist=20=EC=9E=AC?= =?UTF-8?q?=ED=98=84=20=EA=B0=80=EB=8A=A5=20tarball=20(SOURCE=5FDATE=5FEPO?= =?UTF-8?q?CH,=20GNU=20tar=20=EC=A0=95=EA=B7=9C=ED=99=94,=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20remap)=EA=B3=BC=20SHA256SUMS=20-=20cargo-deny=20?= =?UTF-8?q?=EC=A0=95=EC=B1=85=20(=EB=9D=BC=EC=9D=B4=EC=84=A0=EC=8A=A4,=20?= =?UTF-8?q?=EC=86=8C=EC=8A=A4,=20=EC=99=80=EC=9D=BC=EB=93=9C=EC=B9=B4?= =?UTF-8?q?=EB=93=9C)=EA=B3=BC=20make=20audit=20-=20CI=20=EC=9B=8C?= =?UTF-8?q?=ED=81=AC=ED=94=8C=EB=A1=9C=20(Linux=20x86=5F64/arm64,=20macOS?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8,=20FIPS=20=EC=A0=95=EC=A0=81=20?= =?UTF-8?q?=EB=B9=8C=EB=93=9C=20=EA=B2=80=EC=A6=9D,=20=EA=B3=B5=EA=B8=89?= =?UTF-8?q?=EB=A7=9D=20=EC=A0=90=EA=B2=80),=20actions=EB=8A=94=20=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B=20SHA=EB=A1=9C=20=EA=B3=A0=EC=A0=95=20-=20race=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=97=90=EB=A7=8C=20cgo=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 158 +++++++++++++++++++++++++++++++++++++++ Makefile | 21 +++++- core/deny.toml | 32 ++++++++ scripts/dist.sh | 55 ++++++++++++++ 4 files changed, 262 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 core/deny.toml create mode 100755 scripts/dist.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3a49233 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,158 @@ +name: ci + +on: + push: + branches: ["**"] + tags: ["v*"] + pull_request: + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + GOTOOLCHAIN: local + GOPROXY: "off" + GOFLAGS: "-mod=vendor -trimpath" + CGO_ENABLED: "0" + +jobs: + core: + env: + CARGO_NET_OFFLINE: "true" + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, ubuntu-24.04-arm, macos-15] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - name: toolchain + run: cd core && rustup show active-toolchain + - name: lint + run: make lint-core + - name: test + run: make test-core + - name: feature matrix + run: | + cd core + cargo test --frozen -p kms-engine + cargo test --frozen -p kms-engine --features pqc + cargo test --frozen --no-default-features --features non-fips + + host: + env: + CARGO_NET_OFFLINE: "true" + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, ubuntu-24.04-arm, macos-15] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e + with: + go-version-file: host/go.mod + cache: false + - name: toolchain + run: cd core && rustup show active-toolchain + - name: lint + run: make lint-host + - name: test + run: make test-host + - name: fuzz smoke + run: | + cd host + go test -run='^$' -fuzz=FuzzDecodeHeader -fuzztime=20s ./internal/wire/ + go test -run='^$' -fuzz=FuzzDecodeBody -fuzztime=20s ./internal/wire/ + + fips: + env: + CARGO_NET_OFFLINE: "true" + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, ubuntu-24.04-arm] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e + with: + go-version-file: host/go.mod + cache: false + - name: toolchain + run: cd core && rustup show active-toolchain + - name: fips engine tests + run: | + cd core + cargo test --frozen --no-default-features --features fips + cargo test --frozen --no-default-features --features fips,pqc + - name: fips release build + run: | + make build-core FIPS=1 + out=$(core/target/release/kms-core --version) + echo "$out" + case "$out" in *"fips=true"*) ;; *) echo "fips mode not active"; exit 1;; esac + - name: static link check + run: | + if ldd core/target/release/kms-core | grep -q aws_lc; then + echo "fips module is dynamically linked"; exit 1 + fi + + supply-chain: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - name: toolchain + run: cd core && rustup show active-toolchain + - name: vendor integrity + env: + CARGO_NET_OFFLINE: "true" + run: | + cd core + cargo metadata --frozen --format-version 1 > /dev/null + for d in vendor/*/; do + test -f "$d/.cargo-checksum.json" || { echo "missing checksum in $d"; exit 1; } + done + - name: cargo deny + run: | + cargo install cargo-deny --version 0.19.8 --locked + cd core && cargo deny check licenses bans sources + - name: cargo audit + run: | + cargo install cargo-audit --version 0.22.2 --locked + cd core && cargo audit + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e + with: + go-version-file: host/go.mod + cache: false + - name: govulncheck + run: | + GOFLAGS= GOPROXY=https://proxy.golang.org go install golang.org/x/vuln/cmd/govulncheck@v1.8.0 + cd host && "$(go env GOPATH)/bin/govulncheck" ./... + + dist: + needs: [core, host, fips] + env: + CARGO_NET_OFFLINE: "true" + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, ubuntu-24.04-arm] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e + with: + go-version-file: host/go.mod + cache: false + - name: toolchain + run: cd core && rustup show active-toolchain + - name: dist (fips) + run: make dist FIPS=1 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: pairier-${{ matrix.os }} + path: | + dist/*.tar.gz + dist/SHA256SUMS diff --git a/Makefile b/Makefile index bcf7f3c..720598f 100644 --- a/Makefile +++ b/Makefile @@ -21,18 +21,22 @@ export CGO_ENABLED := 0 RUST_TOOLCHAIN := $(shell sed -n 's/^channel = "\(.*\)"/\1/p' $(CORE_DIR)/rust-toolchain.toml) VERSION := $(shell sed -n 's/^version = "\(.*\)"/\1/p' $(CORE_DIR)/Cargo.toml | head -1) GO_LDFLAGS := -s -w -X main.version=$(VERSION) +SOURCE_DATE_EPOCH ?= $(shell git log -1 --format=%ct 2>/dev/null || echo 0) +export SOURCE_DATE_EPOCH +DIST_DIR := dist +CORE_RELEASE := $(abspath $(CORE_DIR)/target/release/kms-core) CARGO_NET := $(CARGO) +$(RUST_TOOLCHAIN) CORE_MANIFEST := --manifest-path $(CORE_DIR)/Cargo.toml GO_NET := GOPROXY=https://proxy.golang.org,direct GOFLAGS=-trimpath $(GO) -.PHONY: all build build-core build-host test test-core test-host lint lint-core lint-host fmt vendor update clean +.PHONY: all build build-core build-host test test-core test-host lint lint-core lint-host fmt vendor update clean dist audit all: build build: build-core build-host build-core: - cd $(CORE_DIR) && $(CARGO) build --release --frozen -p kms-core $(CORE_FLAGS) + cd $(CORE_DIR) && RUSTFLAGS="--remap-path-prefix=$(abspath .)=/pairier $(RUSTFLAGS)" $(CARGO) build --release --frozen -p kms-core $(CORE_FLAGS) build-host: cd $(HOST_DIR) && $(GO) build -ldflags "$(GO_LDFLAGS)" -o bin/ ./cmd/... @@ -44,7 +48,7 @@ test-core: test-host: cd $(CORE_DIR) && $(CARGO) build --frozen -p kms-core --features pqc - cd $(HOST_DIR) && KMS_CORE_BIN=$(KMS_CORE_BIN) $(GO) test -race ./... + cd $(HOST_DIR) && CGO_ENABLED=1 KMS_CORE_BIN=$(KMS_CORE_BIN) $(GO) test -race ./... lint: lint-core lint-host @@ -68,6 +72,15 @@ update: $(CARGO_NET) update $(CORE_MANIFEST) cd $(HOST_DIR) && $(GO_NET) get -u ./... && $(GO_NET) mod tidy +dist: build + mkdir -p $(DIST_DIR) + ./scripts/dist.sh $(VERSION) $(CORE_RELEASE) $(HOST_DIR)/bin/kmsd $(HOST_DIR)/bin/kmsctl $(DIST_DIR) + +audit: + cd $(CORE_DIR) && $(CARGO) deny check licenses bans sources + cd $(CORE_DIR) && if [ -d "$$HOME/.cargo/advisory-db" ]; then $(CARGO) audit --no-fetch; else echo "advisory db missing, run: cargo audit fetch (online) or copy ~/.cargo/advisory-db"; fi + @command -v govulncheck >/dev/null && (cd $(HOST_DIR) && govulncheck ./...) || echo "govulncheck not installed, skipping" + clean: cd $(CORE_DIR) && $(CARGO) clean - rm -rf $(HOST_DIR)/bin + rm -rf $(HOST_DIR)/bin $(DIST_DIR) diff --git a/core/deny.toml b/core/deny.toml new file mode 100644 index 0000000..6805fcd --- /dev/null +++ b/core/deny.toml @@ -0,0 +1,32 @@ +[graph] +all-features = true + +[advisories] +db-path = "~/.cargo/advisory-db" +db-urls = ["https://github.com/rustsec/advisory-db"] +yanked = "deny" + +[licenses] +allow = [ + "MIT", + "Apache-2.0", + "ISC", + "BSD-3-Clause", + "Unicode-3.0", + "OpenSSL", + "Unlicense", + "CC0-1.0", + "MIT-0", +] +confidence-threshold = 0.8 +private = { ignore = true } + +[bans] +multiple-versions = "warn" +wildcards = "deny" +allow-wildcard-paths = true + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] diff --git a/scripts/dist.sh b/scripts/dist.sh new file mode 100755 index 0000000..df2a7a7 --- /dev/null +++ b/scripts/dist.sh @@ -0,0 +1,55 @@ +#!/bin/sh +set -eu + +version="$1" +core_bin="$2" +kmsd_bin="$3" +kmsctl_bin="$4" +out_dir="$5" +epoch="${SOURCE_DATE_EPOCH:-0}" + +os=$(uname -s | tr '[:upper:]' '[:lower:]') +arch=$(uname -m) +case "$arch" in + x86_64|amd64) arch=amd64 ;; + aarch64|arm64) arch=arm64 ;; +esac +name="pairier-${version}-${os}-${arch}" + +if tar --version 2>/dev/null | grep -q GNU; then + TAR=tar +elif command -v gtar >/dev/null 2>&1; then + TAR=gtar +else + echo "dist: GNU tar is required for reproducible archives (brew install gnu-tar)" >&2 + exit 1 +fi + +stage="$out_dir/$name" +rm -rf "$stage" +mkdir -p "$stage" +cp "$core_bin" "$stage/kms-core" +cp "$kmsd_bin" "$stage/kmsd" +cp "$kmsctl_bin" "$stage/kmsctl" +chmod 0755 "$stage"/* +"$stage/kms-core" --version > "$stage/VERSION" +"$stage/kmsd" --version >> "$stage/VERSION" +"$stage/kmsctl" version >> "$stage/VERSION" +chmod 0644 "$stage/VERSION" + +( + cd "$out_dir" + find "$name" -print0 | LC_ALL=C sort -z | \ + "$TAR" --null --no-recursion --files-from=- \ + --owner=0 --group=0 --numeric-owner \ + --mtime="@$epoch" --format=gnu \ + -cf - | gzip -n -9 > "$name.tar.gz" + rm -rf "$name" + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$name.tar.gz" > "$name.tar.gz.sha256" + else + shasum -a 256 "$name.tar.gz" > "$name.tar.gz.sha256" + fi + cat ./*.tar.gz.sha256 | LC_ALL=C sort > SHA256SUMS +) +echo "$out_dir/$name.tar.gz" From 267d8d69a38ee543aed9adb50454396d91651c68 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:27:43 +0900 Subject: [PATCH 19/48] =?UTF-8?q?=ED=98=B8=EC=8A=A4=ED=8A=B8=EC=97=90=20Go?= =?UTF-8?q?=20FIPS=20140-3=20=EB=AA=A8=EB=93=88=20=EC=A0=81=EC=9A=A9=20-?= =?UTF-8?q?=20=EA=B8=B0=EB=B3=B8=20=EB=B9=8C=EB=93=9C=EB=A5=BC=20GOFIPS140?= =?UTF-8?q?=3Dcertified=20(v1.0.0-c2097c7c)=EB=A1=9C=20=ED=95=98=EA=B3=A0?= =?UTF-8?q?=20GOFIPS140=3Doff=EB=A1=9C=20=ED=95=B4=EC=A0=9C=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=20-=20kmsd,=20kmsctl=20=EB=B2=84=EC=A0=84=20=EC=B6=9C?= =?UTF-8?q?=EB=A0=A5=EA=B3=BC=20=EA=B8=B0=EB=8F=99=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=97=90=20go=5Ffips=20=EC=83=81=ED=83=9C=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 2 ++ host/cmd/kmsctl/main.go | 7 ++++--- host/cmd/kmsd/main.go | 5 +++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 720598f..c992f0f 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,8 @@ export GOTOOLCHAIN := local export GOPROXY := off export GOFLAGS := -mod=vendor -trimpath export CGO_ENABLED := 0 +GOFIPS140 ?= certified +export GOFIPS140 RUST_TOOLCHAIN := $(shell sed -n 's/^channel = "\(.*\)"/\1/p' $(CORE_DIR)/rust-toolchain.toml) VERSION := $(shell sed -n 's/^version = "\(.*\)"/\1/p' $(CORE_DIR)/Cargo.toml | head -1) diff --git a/host/cmd/kmsctl/main.go b/host/cmd/kmsctl/main.go index 120d5a5..bc25e23 100644 --- a/host/cmd/kmsctl/main.go +++ b/host/cmd/kmsctl/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "crypto/fips140" "errors" "flag" "fmt" @@ -96,7 +97,7 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int { return 2 } if rest[0] == "version" { - fmt.Fprintf(stdout, "kmsctl %s\n", version) + fmt.Fprintf(stdout, "kmsctl %s (go %s, go_fips=%v)\n", version, runtime.Version(), fips140.Enabled()) return 0 } if *dir == "" { @@ -222,8 +223,8 @@ func (c *cli) cmdInit(ctx context.Context, args []string) (int, error) { if err != nil { return 1, err } - fmt.Fprintf(c.stdout, "member\t%s\nidentity\t%s\nkem\t%s\nfips\t%v\npqc\t%v\nfingerprint\t%s\n", - card.MemberID, algName(card.IdentityAlg), algName(card.KemAlg), n.FIPS(), n.PQC(), fp) + fmt.Fprintf(c.stdout, "member\t%s\nidentity\t%s\nkem\t%s\ncore_fips\t%v\ngo_fips\t%v\npqc\t%v\nfingerprint\t%s\n", + card.MemberID, algName(card.IdentityAlg), algName(card.KemAlg), n.FIPS(), fips140.Enabled(), n.PQC(), fp) return 0, nil } diff --git a/host/cmd/kmsd/main.go b/host/cmd/kmsd/main.go index 5a9a635..f78d953 100644 --- a/host/cmd/kmsd/main.go +++ b/host/cmd/kmsd/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "crypto/fips140" "errors" "flag" "fmt" @@ -50,7 +51,7 @@ func run(args []string, stdout, stderr io.Writer, ready func(addr string)) int { return 2 } if *showVersion { - fmt.Fprintf(stdout, "kmsd %s\n", version) + fmt.Fprintf(stdout, "kmsd %s (go %s, go_fips=%v)\n", version, runtime.Version(), fips140.Enabled()) return 0 } if fs.NArg() != 0 { @@ -139,7 +140,7 @@ func serve(ctx context.Context, s settings, stderr io.Writer, ready func(addr st if err != nil { return err } - fmt.Fprintf(stderr, "kmsd %s member %s listening on %s fips=%v pqc=%v\n", version, n.MemberID(), ln.Addr(), n.FIPS(), n.PQC()) + fmt.Fprintf(stderr, "kmsd %s member %s listening on %s core_fips=%v go_fips=%v pqc=%v\n", version, n.MemberID(), ln.Addr(), n.FIPS(), fips140.Enabled(), n.PQC()) if ready != nil { ready(ln.Addr().String()) } From 083ede9de9c9c41b19d3f08656262602c67cd207 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:31:05 +0900 Subject: [PATCH 20/48] =?UTF-8?q?KeyInspect=EA=B0=80=20=EB=B8=94=EB=A1=AD?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=9C=A0=EB=8F=84=ED=95=9C=20=EA=B3=B5?= =?UTF-8?q?=EA=B0=9C=ED=82=A4=20=EB=B0=98=ED=99=98=20-=20=EC=84=9C?= =?UTF-8?q?=EB=AA=85=20=ED=82=A4=EC=99=80=20KEM=20=ED=82=A4=EC=9D=98=20?= =?UTF-8?q?=EA=B3=B5=EA=B0=9C=ED=82=A4=EB=A5=BC=20=EC=BD=94=EC=96=B4?= =?UTF-8?q?=EA=B0=80=20=EB=B4=89=EC=9D=B8=EB=90=9C=20=EB=B9=84=EB=B0=80?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=A7=81=EC=A0=91=20=EA=B3=84=EC=82=B0=20?= =?UTF-8?q?(ML-KEM=EC=9D=80=20FIPS=20203=20dk=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=B6=94=EC=B6=9C)=20-=20=EB=85=B8?= =?UTF-8?q?=EB=93=9C=EB=8A=94=20=EC=A0=80=EC=9E=A5=EB=90=9C=20=EA=B3=B5?= =?UTF-8?q?=EA=B0=9C=ED=82=A4=EB=A5=BC=20=EC=BD=94=EC=96=B4=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=EC=99=80=20=EB=8C=80=EC=A1=B0=ED=95=B4=20KEM=20?= =?UTF-8?q?=EA=B3=B5=EA=B0=9C=ED=82=A4=20=EB=B0=94=EA=BF=94=EC=B9=98?= =?UTF-8?q?=EA=B8=B0=20=EC=B0=A8=EB=8B=A8=20-=20=EC=A0=81=ED=95=A9?= =?UTF-8?q?=EC=84=B1=20=EB=B2=A1=ED=84=B0=20=EC=9E=AC=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?(frame=2032,=20reject=2038)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- conformance/wire/gen_v1.py | 7 +++-- conformance/wire/v1.txt | 7 +++-- core/crates/kms-core/src/server.rs | 15 +++++++++ core/crates/kms-engine/src/kem.rs | 49 ++++++++++++++++++++++++++++++ core/crates/kms-engine/src/lib.rs | 11 +++++++ core/crates/kms-engine/src/sign.rs | 35 +++++++++++++++++++++ core/crates/kms-wire/src/msg.rs | 5 ++- host/internal/core/client.go | 11 ++++--- host/internal/core/client_test.go | 6 +++- host/internal/node/node.go | 3 +- host/internal/node/node_test.go | 20 ++++++++++++ host/internal/wire/msg.go | 15 ++++++--- 12 files changed, 167 insertions(+), 17 deletions(-) diff --git a/conformance/wire/gen_v1.py b/conformance/wire/gen_v1.py index 492160d..e8dab95 100644 --- a/conformance/wire/gen_v1.py +++ b/conformance/wire/gen_v1.py @@ -58,7 +58,8 @@ def frame(kind, op, id_, body, status=0): ("error_response_protocol", hdr(RSP, PROTOCOL, 0xFFFF, 0, 0)), ("ping_request_max_id", frame(REQ, PING, 0xFFFFFFFF, b"")), ("key_inspect_request", frame(REQ, KEYINSPECT, 14, b(blob))), - ("key_inspect_response", frame(RSP, KEYINSPECT, 14, u16(1) + u32(0x103) + b(kid) + s("tenant-a/db"))), + ("key_inspect_response", frame(RSP, KEYINSPECT, 14, u16(1) + u32(0x103) + b(kid) + s("tenant-a/db") + b(b""))), + ("key_inspect_response_asym", frame(RSP, KEYINSPECT, 18, u16(0x30) + u32(0x80) + b(kid2) + s("kem/bob") + b(kempk))), ("key_export_request", frame(REQ, KEYEXPORT, 15, b(blob) + u16(0x30) + b(kempk) + u32(2) + b(b"from=a;to=b"))), ("key_export_response", frame(RSP, KEYEXPORT, 15, b(pkg))), ("key_import_request", frame(REQ, KEYIMPORT, 16, b(blob) + b(pkg) + b(b"from=a;to=b"))), @@ -101,7 +102,9 @@ def frame(kind, op, id_, body, status=0): ("sign_request_blob_too_large", SIGN, REQ, u32(16385) + b"\x00" * 16385 + b(b"m")), ("unknown_op_body", 0x7777, REQ, b""), ("key_inspect_request_trailing", KEYINSPECT, REQ, b(blob) + b"\x00"), - ("key_inspect_response_label_too_long", KEYINSPECT, RSP, u16(1) + u32(1) + b(kid) + b(b"a" * 257)), + ("key_inspect_response_label_too_long", KEYINSPECT, RSP, u16(1) + u32(1) + b(kid) + b(b"a" * 257) + b(b"")), + ("key_inspect_response_missing_public_key", KEYINSPECT, RSP, u16(1) + u32(1) + b(kid) + s("x")), + ("key_inspect_response_public_key_too_large", KEYINSPECT, RSP, u16(0x30) + u32(0x80) + b(kid) + s("x") + u32(8193) + b"\x00" * 8193), ("key_export_request_context_too_long", KEYEXPORT, REQ, b(blob) + u16(0x30) + b(kempk) + u32(2) + b(b"\x00" * 1025)), ("key_export_response_package_too_large", KEYEXPORT, RSP, u32(8193) + b"\x00" * 8193), ("key_import_request_truncated", KEYIMPORT, REQ, b(blob) + u32(5) + b"ab"), diff --git a/conformance/wire/v1.txt b/conformance/wire/v1.txt index 3aee6eb..b57bd51 100644 --- a/conformance/wire/v1.txt +++ b/conformance/wire/v1.txt @@ -28,7 +28,8 @@ frame error_response_crypto 4b575031020600200000000400000000 frame error_response_protocol 4b5750310201ffff0000000000000000 frame ping_request_max_id 4b57503101000002ffffffff00000000 frame key_inspect_request 4b575031010000110000000e0000002c00000028a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7 -frame key_inspect_response 4b575031020000110000000e0000002900010000010300000010000102030405060708090a0b0c0d0e0f0000000b74656e616e742d612f6462 +frame key_inspect_response 4b575031020000110000000e0000002d00010000010300000010000102030405060708090a0b0c0d0e0f0000000b74656e616e742d612f646200000000 +frame key_inspect_response_asym 4b57503102000011000000120000006a00300000008000000010101112131415161718191a1b1c1d1e1f000000076b656d2f626f620000004104808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf frame key_export_request 4b575031010000300000000f0000008600000028a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c700300000004104808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf000000020000000b66726f6d3d613b746f3d62 frame key_export_response 4b575031020000300000000f0000007c00000078d0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f4041424344454647 frame key_import_request 4b5750310100003100000010000000b700000028a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c700000078d0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40414243444546470000000b66726f6d3d613b746f3d62 @@ -67,7 +68,9 @@ reject_body aead_encrypt_response_too_large 0x0020 rsp 0001004100000000000000000 reject_body sign_request_blob_too_large 0x0022 req 000040010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016d reject_body unknown_op_body 0x7777 req reject_body key_inspect_request_trailing 0x0011 req 00000028a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c700 -reject_body key_inspect_response_label_too_long 0x0011 rsp 00010000000100000010000102030405060708090a0b0c0d0e0f000001016161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161 +reject_body key_inspect_response_label_too_long 0x0011 rsp 00010000000100000010000102030405060708090a0b0c0d0e0f00000101616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616100000000 +reject_body key_inspect_response_missing_public_key 0x0011 rsp 00010000000100000010000102030405060708090a0b0c0d0e0f0000000178 +reject_body key_inspect_response_public_key_too_large 0x0011 rsp 00300000008000000010000102030405060708090a0b0c0d0e0f000000017800002001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 reject_body key_export_request_context_too_long 0x0030 req 00000028a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c700300000004104808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf00000002000004010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 reject_body key_export_response_package_too_large 0x0030 rsp 00002001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 reject_body key_import_request_truncated 0x0031 req 00000028a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7000000056162 diff --git a/core/crates/kms-core/src/server.rs b/core/crates/kms-core/src/server.rs index 4a6e616..62e7ce3 100644 --- a/core/crates/kms-core/src/server.rs +++ b/core/crates/kms-core/src/server.rs @@ -173,6 +173,7 @@ impl Server { usage: info.usage.bits(), key_id: info.key_id, label: &info.label, + public_key: &info.public_key, }) } @@ -567,6 +568,20 @@ mod tests { assert_eq!(info.usage, usage::ENCRYPT | usage::DECRYPT | usage::EXPORT); assert_eq!(info.key_id, key_id); assert_eq!(info.label, "a/shared"); + assert!(info.public_key.is_empty()); + let (h, body) = call( + &mut b, + 9, + &KeyInspectRequest { + key_blob: &kem_blob, + }, + ) + .unwrap(); + assert_eq!(h.status, Status::Ok); + assert_eq!( + KeyInspectResponse::decode_body(&body).unwrap().public_key, + kem_pk.as_slice() + ); let (h, _) = call(&mut b, 3, &KeyInspectRequest { key_blob: &blob }).unwrap(); assert_eq!(h.status, Status::InvalidKeyBlob); diff --git a/core/crates/kms-engine/src/kem.rs b/core/crates/kms-engine/src/kem.rs index a1a939f..b5a2ea4 100644 --- a/core/crates/kms-engine/src/kem.rs +++ b/core/crates/kms-engine/src/kem.rs @@ -27,6 +27,12 @@ const ML_KEM_768_CIPHERTEXT_LEN: usize = 1088; const ML_KEM_768_EK_OFFSET: usize = 1152; #[cfg(feature = "pqc")] const ML_KEM_768_EK_END: usize = ML_KEM_768_EK_OFFSET + ML_KEM_768_PUBLIC_LEN; +#[cfg(feature = "pqc")] +const ML_KEM_1024_SECRET_LEN: usize = 3168; +#[cfg(feature = "pqc")] +const ML_KEM_1024_EK_OFFSET: usize = 1536; +#[cfg(feature = "pqc")] +const ML_KEM_1024_EK_END: usize = ML_KEM_1024_EK_OFFSET + 1568; pub(crate) type Secret = Zeroizing>; pub(crate) type Generated = (Secret, Vec); @@ -52,6 +58,49 @@ pub(crate) fn generate(alg: Alg) -> Result { } } +pub(crate) fn public_key(alg: Alg, secret: &[u8]) -> Result, Error> { + match alg { + Alg::DhkemP256 => { + let sk = PrivateKey::from_private_key(&ECDH_P256, secret) + .map_err(|_| Error::InvalidKeyBlob)?; + p256_public(&sk) + } + #[cfg(feature = "pqc")] + Alg::MlKem768 => { + DecapsulationKey::new(&ML_KEM_768, secret).map_err(|_| Error::InvalidKeyBlob)?; + Ok(mlkem768_public(secret)?.to_vec()) + } + #[cfg(feature = "pqc")] + Alg::MlKem1024 => { + DecapsulationKey::new(&ML_KEM_1024, secret).map_err(|_| Error::InvalidKeyBlob)?; + if secret.len() != ML_KEM_1024_SECRET_LEN { + return Err(Error::InvalidKeyBlob); + } + secret + .get(ML_KEM_1024_EK_OFFSET..ML_KEM_1024_EK_END) + .map(<[u8]>::to_vec) + .ok_or(Error::InvalidKeyBlob) + } + #[cfg(feature = "pqc")] + Alg::HybridP256MlKem768 => { + let (dh_sk, ml_sk) = secret + .split_at_checked(P256_SECRET_LEN) + .ok_or(Error::InvalidKeyBlob)?; + let mut out = public_key(Alg::DhkemP256, dh_sk)?; + out.extend_from_slice(&public_key(Alg::MlKem768, ml_sk)?); + Ok(out) + } + #[cfg(not(feature = "pqc"))] + Alg::MlKem768 | Alg::MlKem1024 | Alg::HybridP256MlKem768 => Err(Error::CapabilityDisabled), + Alg::Aes256Gcm + | Alg::EcdsaP256Sha256 + | Alg::EcdsaP384Sha384 + | Alg::Ed25519 + | Alg::MlDsa65 + | Alg::MlDsa87 => Err(Error::InvalidArgument), + } +} + pub(crate) fn encapsulate(alg: Alg, public_key: &[u8]) -> Result { if Some(public_key.len()) != alg.public_key_len() { return Err(Error::InvalidArgument); diff --git a/core/crates/kms-engine/src/lib.rs b/core/crates/kms-engine/src/lib.rs index 0795c49..be0228c 100644 --- a/core/crates/kms-engine/src/lib.rs +++ b/core/crates/kms-engine/src/lib.rs @@ -40,6 +40,7 @@ pub struct KeyInfo { pub usage: Usage, pub key_id: [u8; KEY_ID_LEN], pub label: String, + pub public_key: Vec, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -127,11 +128,18 @@ impl Engine { pub fn inspect(&self, blob: &[u8]) -> Result { let k = blob::open(&self.root, blob)?; + self.check_enabled(k.alg)?; + let public_key = match k.alg.kind() { + AlgKind::Symmetric => Vec::new(), + AlgKind::Signature => sign::public_key(k.alg, &k.secret)?, + AlgKind::Kem => kem::public_key(k.alg, &k.secret)?, + }; Ok(KeyInfo { alg: k.alg, usage: k.usage, key_id: k.key_id, label: k.label, + public_key, }) } @@ -325,6 +333,7 @@ mod tests { assert!(k.public_key.is_empty()); let info = e.inspect(&k.blob).unwrap(); assert_eq!(info.alg, Alg::Aes256Gcm); + assert!(info.public_key.is_empty()); assert_eq!(info.usage, both); assert_eq!(info.key_id, k.key_id); assert_eq!(info.label, "tenant/db"); @@ -408,6 +417,7 @@ mod tests { let u = Usage::from_bits(usage::SIGN | usage::VERIFY).unwrap(); let k = e.generate_key(alg, u, "signer").unwrap(); assert_eq!(k.public_key.len(), alg.public_key_len().unwrap()); + assert_eq!(e.inspect(&k.blob).unwrap().public_key, k.public_key); let sig = e.sign(&k.blob, b"message").unwrap(); assert_eq!(sig.len(), alg.signature_len().unwrap()); assert!(e.verify(alg, &k.public_key, b"message", &sig).unwrap()); @@ -445,6 +455,7 @@ mod tests { let u = Usage::from_bits(usage::DECAPSULATE).unwrap(); let k = e.generate_key(alg, u, "kem").unwrap(); assert_eq!(k.public_key.len(), alg.public_key_len().unwrap()); + assert_eq!(e.inspect(&k.blob).unwrap().public_key, k.public_key); let (ct, ss1) = e.encapsulate(alg, &k.public_key).unwrap(); assert_eq!(ct.len(), alg.kem_ciphertext_len().unwrap()); let ss2 = e diff --git a/core/crates/kms-engine/src/sign.rs b/core/crates/kms-engine/src/sign.rs index b12f856..47bbda2 100644 --- a/core/crates/kms-engine/src/sign.rs +++ b/core/crates/kms-engine/src/sign.rs @@ -62,6 +62,41 @@ pub(crate) fn generate(alg: Alg) -> Result { } } +pub(crate) fn public_key(alg: Alg, pkcs8: &[u8]) -> Result, Error> { + match alg { + Alg::EcdsaP256Sha256 | Alg::EcdsaP384Sha384 => { + let sa = if alg == Alg::EcdsaP256Sha256 { + &ECDSA_P256_SHA256_FIXED_SIGNING + } else { + &ECDSA_P384_SHA384_FIXED_SIGNING + }; + let kp = EcdsaKeyPair::from_pkcs8(sa, pkcs8).map_err(|_| Error::InvalidKeyBlob)?; + Ok(kp.public_key().as_ref().to_vec()) + } + Alg::Ed25519 => { + let kp = Ed25519KeyPair::from_pkcs8(pkcs8).map_err(|_| Error::InvalidKeyBlob)?; + Ok(kp.public_key().as_ref().to_vec()) + } + #[cfg(feature = "pqc")] + Alg::MlDsa65 | Alg::MlDsa87 => { + let sa = if alg == Alg::MlDsa65 { + &ML_DSA_65_SIGNING + } else { + &ML_DSA_87_SIGNING + }; + let kp = PqdsaKeyPair::from_pkcs8(sa, pkcs8).map_err(|_| Error::InvalidKeyBlob)?; + Ok(kp.public_key().as_ref().to_vec()) + } + #[cfg(not(feature = "pqc"))] + Alg::MlDsa65 | Alg::MlDsa87 => Err(Error::CapabilityDisabled), + Alg::Aes256Gcm + | Alg::DhkemP256 + | Alg::MlKem768 + | Alg::MlKem1024 + | Alg::HybridP256MlKem768 => Err(Error::InvalidArgument), + } +} + pub(crate) fn sign(alg: Alg, pkcs8: &[u8], message: &[u8]) -> Result, Error> { match alg { Alg::EcdsaP256Sha256 | Alg::EcdsaP384Sha384 => { diff --git a/core/crates/kms-wire/src/msg.rs b/core/crates/kms-wire/src/msg.rs index faf7e5b..284d127 100644 --- a/core/crates/kms-wire/src/msg.rs +++ b/core/crates/kms-wire/src/msg.rs @@ -408,6 +408,7 @@ pub struct KeyInspectResponse<'a> { pub usage: u32, pub key_id: [u8; KEY_ID_LEN], pub label: &'a str, + pub public_key: &'a [u8], } impl<'a> Message<'a> for KeyInspectResponse<'a> { @@ -417,7 +418,8 @@ impl<'a> Message<'a> for KeyInspectResponse<'a> { w.write_u16(self.alg)?; w.write_u32(self.usage)?; w.write_bytes(&self.key_id)?; - w.write_str(self.label) + w.write_str(self.label)?; + w.write_bytes(self.public_key) } fn decode(r: &mut Reader<'a>) -> Result { @@ -426,6 +428,7 @@ impl<'a> Message<'a> for KeyInspectResponse<'a> { usage: r.read_u32()?, key_id: r.read_array::()?, label: r.read_str(MAX_LABEL)?, + public_key: r.read_bytes(MAX_PUBLIC_KEY)?, }) } } diff --git a/host/internal/core/client.go b/host/internal/core/client.go index 87350fe..4b2b2c9 100644 --- a/host/internal/core/client.go +++ b/host/internal/core/client.go @@ -37,10 +37,11 @@ type GeneratedKey struct { } type KeyInfo struct { - Alg uint16 - Usage uint32 - KeyID [wire.KeyIDLen]byte - Label string + Alg uint16 + Usage uint32 + KeyID [wire.KeyIDLen]byte + Label string + PublicKey []byte } type ImportedKey struct { @@ -152,7 +153,7 @@ func (c *Client) Inspect(ctx context.Context, blob []byte) (*KeyInfo, error) { if err := c.call(ctx, &wire.KeyInspectRequest{KeyBlob: blob}, &rsp); err != nil { return nil, err } - return &KeyInfo{Alg: rsp.Alg, Usage: rsp.Usage, KeyID: rsp.KeyID, Label: rsp.Label}, nil + return &KeyInfo{Alg: rsp.Alg, Usage: rsp.Usage, KeyID: rsp.KeyID, Label: rsp.Label, PublicKey: rsp.PublicKey}, nil } func (c *Client) ExportKey(ctx context.Context, blob []byte, kemAlg uint16, recipientPublicKey []byte, usage uint32, context []byte) ([]byte, error) { diff --git a/host/internal/core/client_test.go b/host/internal/core/client_test.go index f93c706..bd1aedf 100644 --- a/host/internal/core/client_test.go +++ b/host/internal/core/client_test.go @@ -403,9 +403,13 @@ func TestRealCoreExchange(t *testing.T) { t.Fatal(err) } info, err := a.Inspect(ctx, k.Blob) - if err != nil || info.KeyID != k.KeyID || info.Label != "a/shared" || info.Alg != wire.AlgAES256GCM { + if err != nil || info.KeyID != k.KeyID || info.Label != "a/shared" || info.Alg != wire.AlgAES256GCM || len(info.PublicKey) != 0 { t.Fatalf("inspect: %+v %v", info, err) } + kinfo, err := b.Inspect(ctx, kem.Blob) + if err != nil || !bytes.Equal(kinfo.PublicKey, kem.PublicKey) { + t.Fatalf("kem inspect: %v", err) + } _, err = b.Inspect(ctx, k.Blob) var se *StatusError if !errors.As(err, &se) || se.Status != wire.StatusInvalidKeyBlob { diff --git a/host/internal/node/node.go b/host/internal/node/node.go index 3439653..d1a3dba 100644 --- a/host/internal/node/node.go +++ b/host/internal/node/node.go @@ -239,7 +239,8 @@ func (n *Node) Key(ctx context.Context, id string) (*store.KeyRecord, error) { if err != nil { return nil, err } - if store.KeyIDString(info.KeyID) != rec.KeyID || info.Alg != rec.Alg || info.Usage != rec.Usage || info.Label != rec.Label { + if store.KeyIDString(info.KeyID) != rec.KeyID || info.Alg != rec.Alg || info.Usage != rec.Usage || info.Label != rec.Label || + !bytes.Equal(info.PublicKey, rec.PublicKey) { return nil, ErrMetadata } return rec, nil diff --git a/host/internal/node/node_test.go b/host/internal/node/node_test.go index 33706bb..9bfb270 100644 --- a/host/internal/node/node_test.go +++ b/host/internal/node/node_test.go @@ -219,6 +219,26 @@ func TestStoredMetadataIsVerifiedAgainstCore(t *testing.T) { if _, err := alice.Key(ctx, k.KeyID); !errors.Is(err, ErrMetadata) { t.Fatalf("metadata tamper not detected: %v", err) } + kemPath := filepath.Join(o.Dir, "keys", alice.info.KemKeyID+".json") + kraw, err := os.ReadFile(kemPath) + if err != nil { + t.Fatal(err) + } + var krec store.KeyRecord + if err := json.Unmarshal(kraw, &krec); err != nil { + t.Fatal(err) + } + krec.PublicKey[len(krec.PublicKey)-1] ^= 1 + kedit, _ := json.Marshal(&krec) + if err := os.WriteFile(kemPath, kedit, 0o600); err != nil { + t.Fatal(err) + } + if _, err := alice.Key(ctx, krec.KeyID); !errors.Is(err, ErrMetadata) { + t.Fatalf("swapped kem public key not detected: %v", err) + } + if _, err := Open(ctx, o); err == nil { + t.Fatal("node opened with a swapped kem public key") + } if _, err := alice.Encrypt(ctx, k.KeyID, []byte("x"), nil); !errors.Is(err, ErrMetadata) { t.Fatalf("tampered key usable: %v", err) } diff --git a/host/internal/wire/msg.go b/host/internal/wire/msg.go index 50943f9..0ca989c 100644 --- a/host/internal/wire/msg.go +++ b/host/internal/wire/msg.go @@ -371,10 +371,11 @@ func (m *KeyInspectRequest) Decode(r *Reader) (err error) { } type KeyInspectResponse struct { - Alg uint16 - Usage uint32 - KeyID [KeyIDLen]byte - Label string + Alg uint16 + Usage uint32 + KeyID [KeyIDLen]byte + Label string + PublicKey []byte } func (*KeyInspectResponse) Op() Op { return OpKeyInspect } @@ -384,6 +385,7 @@ func (m *KeyInspectResponse) Encode(w *Writer) { w.U32(m.Usage) w.Bytes(m.KeyID[:]) w.String(m.Label) + w.Bytes(m.PublicKey) } func (m *KeyInspectResponse) Decode(r *Reader) (err error) { @@ -398,7 +400,10 @@ func (m *KeyInspectResponse) Decode(r *Reader) (err error) { return err } copy(m.KeyID[:], id) - m.Label, err = r.String(MaxLabel) + if m.Label, err = r.String(MaxLabel); err != nil { + return err + } + m.PublicKey, err = r.Bytes(MaxPublicKey) return err } From e427a9f3414c6cf9f416eae9f94d98496fb71e5b Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:33:15 +0900 Subject: [PATCH 21/48] =?UTF-8?q?=EA=B0=90=EC=82=AC=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=20=EC=B2=B4=ED=81=AC=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=84=9C?= =?UTF-8?q?=EB=AA=85=20-=20=EB=85=B8=EB=93=9C=EB=A7=88=EB=8B=A4=20?= =?UTF-8?q?=EC=BD=94=EC=96=B4=20=EB=B4=89=EC=9D=B8=20=EA=B0=90=EC=82=AC=20?= =?UTF-8?q?=EC=84=9C=EB=AA=85=20=ED=82=A4=EB=A5=BC=20=EB=91=90=EA=B3=A0=20?= =?UTF-8?q?=ED=95=AD=EB=AA=A9=EB=A7=88=EB=8B=A4=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=B6=84=EB=A6=AC=EB=90=9C=20(seq,=20hash)=20?= =?UTF-8?q?=EC=B2=B4=ED=81=AC=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=84=9C?= =?UTF-8?q?=EB=AA=85=20-=20=EC=97=B4=20=EB=95=8C=20=EC=B2=B4=EC=9D=B8?= =?UTF-8?q?=EA=B3=BC=20=EC=B5=9C=EC=8B=A0=20=EC=B2=B4=ED=81=AC=ED=8F=AC?= =?UTF-8?q?=EC=9D=B8=ED=8A=B8=20=EC=84=9C=EB=AA=85=EC=9D=84=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=ED=95=B4=20=EC=BD=94=EC=96=B4=20=EC=97=86=EC=9D=B4=20?= =?UTF-8?q?=EC=9E=AC=EA=B3=84=EC=82=B0=ED=95=9C=20=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EA=B1=B0=EB=B6=80=20-=20=EB=85=B8=EB=93=9C=20=EB=A0=88?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EC=97=90=20=EA=B0=90=EC=82=AC=20=ED=82=A4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80,=20=ED=82=A4=20label=EA=B3=BC=20=EC=9A=A9?= =?UTF-8?q?=EB=8F=84=20=EB=8C=80=EC=A1=B0=20=EA=B0=95=ED=99=94,=20kmsctl?= =?UTF-8?q?=20audit=20verify=20=EC=A0=84=EC=B2=B4=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- host/cmd/kmsctl/main.go | 23 ++++ host/cmd/kmsctl/main_test.go | 5 +- host/internal/audit/audit.go | 173 +++++++++++++++++++++++------- host/internal/audit/audit_test.go | 129 ++++++++++++++++++++-- host/internal/node/node.go | 83 ++++++++++---- host/internal/node/node_test.go | 52 ++++++++- host/internal/store/store.go | 10 +- host/internal/store/store_test.go | 5 +- 8 files changed, 403 insertions(+), 77 deletions(-) diff --git a/host/cmd/kmsctl/main.go b/host/cmd/kmsctl/main.go index bc25e23..42a9c25 100644 --- a/host/cmd/kmsctl/main.go +++ b/host/cmd/kmsctl/main.go @@ -37,6 +37,7 @@ commands: inbox accept --id ENVELOPEID inbox reject --id ENVELOPEID peer health --member MEMBER --url https://HOST:PORT + audit verify encrypt --id KEYID [--in FILE] [--out FILE] [--aad STRING] decrypt --id KEYID [--in FILE] [--out FILE] [--aad STRING] version @@ -176,6 +177,11 @@ func (c *cli) dispatch(args []string) (int, error) { return c.cmdInboxReject(ctx, args[2:]) } return 2, fmt.Errorf("inbox: unknown subcommand %q", args[1]) + case "audit": + if len(args) < 2 || args[1] != "verify" { + return 2, errors.New("audit: subcommand required (verify)") + } + return c.cmdAuditVerify(ctx, args[2:]) case "peer": if len(args) < 2 || args[1] != "health" { return 2, errors.New("peer: subcommand required (health)") @@ -539,6 +545,23 @@ func (c *cli) cmdPeerHealth(ctx context.Context, args []string) (int, error) { return 0, nil } +func (c *cli) cmdAuditVerify(ctx context.Context, args []string) (int, error) { + if len(args) != 0 { + return 2, errors.New("audit verify: no arguments expected") + } + n, err := c.open(ctx) + if err != nil { + return 1, err + } + defer n.Close() + count, err := n.VerifyAudit() + if err != nil { + return 1, err + } + fmt.Fprintf(c.stdout, "verified\t%d\tentries\n", count) + return 0, nil +} + func (c *cli) cmdCrypt(ctx context.Context, args []string, encrypt bool) (int, error) { name := "decrypt" if encrypt { diff --git a/host/cmd/kmsctl/main_test.go b/host/cmd/kmsctl/main_test.go index da7a903..d19fc75 100644 --- a/host/cmd/kmsctl/main_test.go +++ b/host/cmd/kmsctl/main_test.go @@ -128,7 +128,10 @@ func TestEndToEndExchange(t *testing.T) { if !strings.Contains(info, "origin\talice") || !strings.Contains(info, "usage\tdecrypt\n") { t.Fatalf("info %q", info) } - if list := must(nil, "--dir", bob, "key", "list"); strings.Count(list, "\n") != 4 { + if list := must(nil, "--dir", bob, "key", "list"); strings.Count(list, "\n") != 5 { t.Fatalf("bob key list %q", list) } + if out := must(nil, "--dir", bob, "audit", "verify"); !strings.HasPrefix(out, "verified\t") { + t.Fatalf("audit verify %q", out) + } } diff --git a/host/internal/audit/audit.go b/host/internal/audit/audit.go index da55729..d61d318 100644 --- a/host/internal/audit/audit.go +++ b/host/internal/audit/audit.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "crypto/sha256" + "encoding/binary" "encoding/hex" "encoding/json" "errors" @@ -26,11 +27,56 @@ const ( ) var ( - ErrCorrupt = errors.New("audit: log chain is corrupt") - ErrInsecure = errors.New("audit: insecure log file") - ErrField = errors.New("audit: invalid event or field") + ErrCorrupt = errors.New("audit: log chain is corrupt") + ErrInsecure = errors.New("audit: insecure log file") + ErrField = errors.New("audit: invalid event or field") + ErrSignature = errors.New("audit: checkpoint signature is invalid") ) +const checkpointDomain = "pairier/audit/checkpoint/v1" + +type Signer interface { + SignCheckpoint(msg []byte) ([]byte, error) +} + +type Verifier interface { + VerifyCheckpoint(msg, sig []byte) (bool, error) +} + +func CheckpointMessage(seq uint64, hash string) ([]byte, error) { + h, err := hex.DecodeString(hash) + if err != nil || len(h) != hashLen { + return nil, ErrCorrupt + } + msg := make([]byte, 0, len(checkpointDomain)+1+8+hashLen) + msg = append(msg, checkpointDomain...) + msg = append(msg, 0) + msg = binary.BigEndian.AppendUint64(msg, seq) + return append(msg, h...), nil +} + +func verifyEntry(v Verifier, e *Entry) error { + if e.Sig == "" { + return fmt.Errorf("%w: entry %d is unsigned", ErrSignature, e.Seq) + } + sig, err := hex.DecodeString(e.Sig) + if err != nil { + return fmt.Errorf("%w: entry %d", ErrSignature, e.Seq) + } + msg, err := CheckpointMessage(e.Seq, e.Hash) + if err != nil { + return err + } + ok, err := v.VerifyCheckpoint(msg, sig) + if err != nil { + return fmt.Errorf("%w: entry %d: %v", ErrSignature, e.Seq, err) + } + if !ok { + return fmt.Errorf("%w: entry %d", ErrSignature, e.Seq) + } + return nil +} + type Entry struct { Seq uint64 `json:"seq"` Time int64 `json:"time"` @@ -38,21 +84,35 @@ type Entry struct { Fields map[string]string `json:"fields,omitempty"` Prev string `json:"prev"` Hash string `json:"hash"` + Sig string `json:"sig,omitempty"` } type Log struct { - mu sync.Mutex - f *os.File - seq uint64 - prev string - offset int64 - now func() time.Time + mu sync.Mutex + f *os.File + seq uint64 + prev string + head *Entry + offset int64 + now func() time.Time + signer Signer + verifier Verifier +} + +type Options struct { + Now func() time.Time + Signer Signer + Verifier Verifier } -func Open(path string, now func() time.Time) (*Log, error) { +func Open(path string, o Options) (*Log, error) { + now := o.Now if now == nil { now = time.Now } + if (o.Signer == nil) != (o.Verifier == nil) { + return nil, errors.New("audit: signer and verifier must be set together") + } f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, fileMode) if err != nil { return nil, err @@ -70,7 +130,7 @@ func Open(path string, now func() time.Time) (*Log, error) { _ = f.Close() return nil, fmt.Errorf("%w: %s is a symlink", ErrInsecure, path) } - l := &Log{f: f, prev: genesis, now: now} + l := &Log{f: f, prev: genesis, now: now, signer: o.Signer, verifier: o.Verifier} if err := l.lock(); err != nil { _ = f.Close() return nil, err @@ -108,14 +168,28 @@ func (l *Log) catchUp() error { return nil } section := io.NewSectionReader(l.f, l.offset, size-l.offset) - seq, prev, err := replayFrom(section, l.seq, l.prev) + seq, prev, head, err := replayFrom(section, l.seq, l.prev, l.verifier != nil) if err != nil { return err } + if l.verifier != nil && head != nil { + if err := verifyEntry(l.verifier, head); err != nil { + return err + } + } l.seq, l.prev, l.offset = seq, prev, size + if head != nil { + l.head = head + } return nil } +func (l *Log) Head() (uint64, string) { + l.mu.Lock() + defer l.mu.Unlock() + return l.seq, l.prev +} + func (l *Log) Close() error { l.mu.Lock() defer l.mu.Unlock() @@ -152,6 +226,17 @@ func (l *Log) Append(event string, fields map[string]string) error { return err } e.Hash = h + if l.signer != nil { + msg, err := CheckpointMessage(e.Seq, e.Hash) + if err != nil { + return err + } + sig, err := l.signer.SignCheckpoint(msg) + if err != nil { + return fmt.Errorf("audit: sign checkpoint: %w", err) + } + e.Sig = hex.EncodeToString(sig) + } line, err := json.Marshal(e) if err != nil { return err @@ -166,6 +251,7 @@ func (l *Log) Append(event string, fields map[string]string) error { } l.seq = e.Seq l.prev = e.Hash + l.head = e l.offset += int64(n) return nil } @@ -201,48 +287,55 @@ func digest(e *Entry) (string, error) { return hex.EncodeToString(h.Sum(nil)), nil } -func replay(r io.Reader) (uint64, string, error) { - return replayFrom(r, 0, genesis) -} - -func replayFrom(r io.Reader, seq uint64, prev string) (uint64, string, error) { +func replayFrom(r io.Reader, seq uint64, prev string, requireSig bool) (uint64, string, *Entry, error) { + var head *Entry sc := bufio.NewScanner(r) sc.Buffer(make([]byte, 0, maxLine), maxLine) for sc.Scan() { line := sc.Bytes() if len(bytes.TrimSpace(line)) == 0 { - return 0, "", fmt.Errorf("%w: blank line after entry %d", ErrCorrupt, seq) + return 0, "", nil, fmt.Errorf("%w: blank line after entry %d", ErrCorrupt, seq) } var e Entry dec := json.NewDecoder(bytes.NewReader(line)) dec.DisallowUnknownFields() if err := dec.Decode(&e); err != nil || dec.More() { - return 0, "", fmt.Errorf("%w: entry %d is not valid json", ErrCorrupt, seq+1) + return 0, "", nil, fmt.Errorf("%w: entry %d is not valid json", ErrCorrupt, seq+1) } if e.Seq != seq+1 || e.Prev != prev { - return 0, "", fmt.Errorf("%w: entry %d breaks the chain", ErrCorrupt, e.Seq) + return 0, "", nil, fmt.Errorf("%w: entry %d breaks the chain", ErrCorrupt, e.Seq) } h, err := digest(&e) if err != nil || h != e.Hash { - return 0, "", fmt.Errorf("%w: entry %d hash mismatch", ErrCorrupt, e.Seq) + return 0, "", nil, fmt.Errorf("%w: entry %d hash mismatch", ErrCorrupt, e.Seq) + } + if requireSig && e.Sig == "" { + return 0, "", nil, fmt.Errorf("%w: entry %d is unsigned", ErrSignature, e.Seq) } seq = e.Seq prev = e.Hash + entry := e + head = &entry } if err := sc.Err(); err != nil { - return 0, "", fmt.Errorf("%w: %v", ErrCorrupt, err) + return 0, "", nil, fmt.Errorf("%w: %v", ErrCorrupt, err) } - return seq, prev, nil + return seq, prev, head, nil } -func Verify(path string) (uint64, error) { - f, err := os.Open(path) +func Verify(path string, v Verifier) (uint64, error) { + entries, err := Read(path) if err != nil { return 0, err } - defer f.Close() - seq, _, err := replay(f) - return seq, err + if v != nil { + for i := range entries { + if err := verifyEntry(v, &entries[i]); err != nil { + return 0, err + } + } + } + return uint64(len(entries)), nil } func Read(path string) ([]Entry, error) { @@ -251,21 +344,23 @@ func Read(path string) ([]Entry, error) { return nil, err } defer f.Close() - if _, _, err := replay(f); err != nil { - return nil, err - } - if _, err := f.Seek(0, io.SeekStart); err != nil { - return nil, err - } var out []Entry + seq, prev := uint64(0), genesis sc := bufio.NewScanner(f) sc.Buffer(make([]byte, 0, maxLine), maxLine) for sc.Scan() { - var e Entry - if err := json.Unmarshal(sc.Bytes(), &e); err != nil { - return nil, ErrCorrupt + s, p, head, err := replayFrom(bytes.NewReader(append(sc.Bytes(), '\n')), seq, prev, false) + if err != nil { + return nil, err } - out = append(out, e) + if head == nil { + return nil, fmt.Errorf("%w: blank line after entry %d", ErrCorrupt, seq) + } + seq, prev = s, p + out = append(out, *head) + } + if err := sc.Err(); err != nil { + return nil, fmt.Errorf("%w: %v", ErrCorrupt, err) } - return out, sc.Err() + return out, nil } diff --git a/host/internal/audit/audit_test.go b/host/internal/audit/audit_test.go index 85aea1c..7c9144c 100644 --- a/host/internal/audit/audit_test.go +++ b/host/internal/audit/audit_test.go @@ -1,6 +1,10 @@ package audit import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" "errors" "os" "path/filepath" @@ -12,7 +16,7 @@ import ( func TestChainAppendVerifyAndTamper(t *testing.T) { path := filepath.Join(t.TempDir(), "audit.log") now := func() time.Time { return time.Unix(1_700_000_000, 0) } - l, err := Open(path, now) + l, err := Open(path, Options{Now: now}) if err != nil { t.Fatal(err) } @@ -35,11 +39,11 @@ func TestChainAppendVerifyAndTamper(t *testing.T) { if fi.Mode().Perm() != 0o600 { t.Fatalf("mode %v", fi.Mode()) } - n, err := Verify(path) + n, err := Verify(path, nil) if err != nil || n != 2 { t.Fatalf("verify %d %v", n, err) } - l2, err := Open(path, now) + l2, err := Open(path, Options{Now: now}) if err != nil { t.Fatal(err) } @@ -62,41 +66,41 @@ func TestChainAppendVerifyAndTamper(t *testing.T) { if err := os.WriteFile(path, []byte(tampered), 0o600); err != nil { t.Fatal(err) } - if _, err := Verify(path); !errors.Is(err, ErrCorrupt) { + if _, err := Verify(path, nil); !errors.Is(err, ErrCorrupt) { t.Fatalf("tamper not detected: %v", err) } - if _, err := Open(path, now); !errors.Is(err, ErrCorrupt) { + if _, err := Open(path, Options{Now: now}); !errors.Is(err, ErrCorrupt) { t.Fatalf("open on tampered log succeeded: %v", err) } truncated := strings.Join([]string{lines[0], lines[2]}, "\n") + "\n" if err := os.WriteFile(path, []byte(truncated), 0o600); err != nil { t.Fatal(err) } - if _, err := Verify(path); !errors.Is(err, ErrCorrupt) { + if _, err := Verify(path, nil); !errors.Is(err, ErrCorrupt) { t.Fatalf("deletion not detected: %v", err) } if err := os.WriteFile(path, []byte(lines[0]+"\n"+lines[1]+"\n"), 0o600); err != nil { t.Fatal(err) } - if _, err := Open(path, now); err != nil { + if _, err := Open(path, Options{Now: now}); err != nil { t.Fatalf("prefix of a valid chain rejected: %v", err) } if err := os.Chmod(path, 0o640); err != nil { t.Fatal(err) } - if _, err := Open(path, now); !errors.Is(err, ErrInsecure) { + if _, err := Open(path, Options{Now: now}); !errors.Is(err, ErrInsecure) { t.Fatalf("permissive mode accepted: %v", err) } } func TestConcurrentHandlesShareOneChain(t *testing.T) { path := filepath.Join(t.TempDir(), "audit.log") - a, err := Open(path, nil) + a, err := Open(path, Options{}) if err != nil { t.Fatal(err) } defer a.Close() - b, err := Open(path, nil) + b, err := Open(path, Options{}) if err != nil { t.Fatal(err) } @@ -112,7 +116,7 @@ func TestConcurrentHandlesShareOneChain(t *testing.T) { if a.Seq() != 9 || b.Seq() != 10 { t.Fatalf("seq %d %d", a.Seq(), b.Seq()) } - if n, err := Verify(path); err != nil || n != 10 { + if n, err := Verify(path, nil); err != nil || n != 10 { t.Fatalf("verify %d %v", n, err) } if err := os.Truncate(path, 10); err != nil { @@ -122,3 +126,106 @@ func TestConcurrentHandlesShareOneChain(t *testing.T) { t.Fatalf("shrunk log not detected: %v", err) } } + +type ecdsaCheckpoints struct { + key *ecdsa.PrivateKey + calls int +} + +func (k *ecdsaCheckpoints) SignCheckpoint(msg []byte) ([]byte, error) { + k.calls++ + sum := sha256.Sum256(msg) + return ecdsa.SignASN1(rand.Reader, k.key, sum[:]) +} + +func (k *ecdsaCheckpoints) VerifyCheckpoint(msg, sig []byte) (bool, error) { + sum := sha256.Sum256(msg) + return ecdsa.VerifyASN1(&k.key.PublicKey, sum[:], sig), nil +} + +func TestSignedCheckpoints(t *testing.T) { + path := filepath.Join(t.TempDir(), "audit.log") + key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + k := &ecdsaCheckpoints{key: key} + if _, err := Open(path, Options{Signer: k}); err == nil { + t.Fatal("signer without verifier accepted") + } + l, err := Open(path, Options{Signer: k, Verifier: k}) + if err != nil { + t.Fatal(err) + } + for _, ev := range []string{"a", "b", "c"} { + if err := l.Append(ev, map[string]string{"k": ev}); err != nil { + t.Fatal(err) + } + } + _ = l.Close() + if k.calls != 3 { + t.Fatalf("expected one signature per entry, got %d", k.calls) + } + if n, err := Verify(path, k); err != nil || n != 3 { + t.Fatalf("verify %d %v", n, err) + } + + raw, _ := os.ReadFile(path) + lines := strings.Split(strings.TrimSpace(string(raw)), "\n") + entries, _ := Read(path) + + forger, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + f := &ecdsaCheckpoints{key: forger} + if _, err := Verify(path, f); !errors.Is(err, ErrSignature) { + t.Fatalf("foreign key verified the log: %v", err) + } + if _, err := Open(path, Options{Signer: f, Verifier: f}); !errors.Is(err, ErrSignature) { + t.Fatalf("open with foreign verifier succeeded: %v", err) + } + + rewritten := filepath.Join(t.TempDir(), "audit.log") + rl, err := Open(rewritten, Options{Signer: f, Verifier: f}) + if err != nil { + t.Fatal(err) + } + for _, e := range entries { + if err := rl.Append(e.Event, e.Fields); err != nil { + t.Fatal(err) + } + } + _ = rl.Close() + if _, err := Verify(rewritten, nil); err != nil { + t.Fatalf("rewritten chain should be internally consistent: %v", err) + } + if _, err := Verify(rewritten, k); !errors.Is(err, ErrSignature) { + t.Fatalf("recomputed chain accepted without the signing key: %v", err) + } + + unsigned := filepath.Join(t.TempDir(), "audit.log") + ul, _ := Open(unsigned, Options{}) + _ = ul.Append("x", nil) + _ = ul.Close() + if _, err := Open(unsigned, Options{Signer: k, Verifier: k}); !errors.Is(err, ErrSignature) { + t.Fatalf("unsigned log accepted by a verifying open: %v", err) + } + + stripped := strings.Replace(lines[2], `,"sig":"`+entries[2].Sig+`"`, "", 1) + if stripped == lines[2] { + t.Fatal("strip did not apply") + } + if err := os.WriteFile(path, []byte(lines[0]+"\n"+lines[1]+"\n"+stripped+"\n"), 0o600); err != nil { + t.Fatal(err) + } + if _, err := Open(path, Options{Signer: k, Verifier: k}); !errors.Is(err, ErrSignature) { + t.Fatalf("stripped head signature accepted: %v", err) + } + + if err := os.WriteFile(path, []byte(lines[0]+"\n"+lines[1]+"\n"), 0o600); err != nil { + t.Fatal(err) + } + l2, err := Open(path, Options{Signer: k, Verifier: k}) + if err != nil { + t.Fatalf("truncated but signed prefix should open: %v", err) + } + if seq, _ := l2.Head(); seq != 2 { + t.Fatalf("head seq %d", seq) + } + _ = l2.Close() +} diff --git a/host/internal/node/node.go b/host/internal/node/node.go index d1a3dba..b6262a4 100644 --- a/host/internal/node/node.go +++ b/host/internal/node/node.go @@ -32,6 +32,8 @@ var ( ErrSenderMismatch = errors.New("node: envelope sender differs from the authenticated peer") ) +const auditTimeout = 10 * time.Second + const SymmetricUsage = wire.UsageEncrypt | wire.UsageDecrypt | wire.UsageWrap | wire.UsageUnwrap | wire.UsageDerive | wire.UsageExport type Options struct { @@ -51,9 +53,38 @@ type Node struct { info *store.Node identity *store.KeyRecord kem *store.KeyRecord + auditKey *store.KeyRecord tls tls.Certificate } +type checkpoints struct{ n *Node } + +func (c checkpoints) SignCheckpoint(msg []byte) ([]byte, error) { + ctx, cancel := context.WithTimeout(context.Background(), auditTimeout) + defer cancel() + return c.n.core.Sign(ctx, c.n.auditKey.Blob, msg) +} + +func (c checkpoints) VerifyCheckpoint(msg, sig []byte) (bool, error) { + ctx, cancel := context.WithTimeout(context.Background(), auditTimeout) + defer cancel() + return c.n.core.Verify(ctx, c.n.auditKey.Alg, c.n.auditKey.PublicKey, msg, sig) +} + +func (n *Node) openAudit() error { + cp := checkpoints{n: n} + log, err := audit.Open(n.st.AuditLogPath(), audit.Options{Now: n.opts.Now, Signer: cp, Verifier: cp}) + if err != nil { + return err + } + n.log = log + return nil +} + +func (n *Node) VerifyAudit() (uint64, error) { + return audit.Verify(n.st.AuditLogPath(), checkpoints{n: n}) +} + func (n *Node) audit(event string, fields map[string]string, err error) error { if err != nil { if fields == nil { @@ -106,12 +137,7 @@ func Init(ctx context.Context, o Options, memberID string, pqc bool) (*Node, err if err != nil { return nil, err } - log, err := audit.Open(st.AuditLogPath(), o.Now) - if err != nil { - _ = c.Close() - return nil, err - } - n := &Node{opts: o, st: st, core: c, log: log} + n := &Node{opts: o, st: st, core: c} if err := n.createIdentity(ctx, memberID, pqc); err != nil { _ = n.Close() return nil, err @@ -120,7 +146,11 @@ func Init(ctx context.Context, o Options, memberID string, pqc bool) (*Node, err _ = n.Close() return nil, err } - if err := n.audit("node.init", map[string]string{"member": memberID, "identity": n.identity.KeyID, "kem": n.kem.KeyID, "pqc": fmt.Sprint(pqc), "fips": fmt.Sprint(c.FIPS())}, nil); err != nil { + if err := n.openAudit(); err != nil { + _ = n.Close() + return nil, err + } + if err := n.audit("node.init", map[string]string{"member": memberID, "identity": n.identity.KeyID, "kem": n.kem.KeyID, "audit_key": n.auditKey.KeyID, "pqc": fmt.Sprint(pqc), "fips": fmt.Sprint(c.FIPS())}, nil); err != nil { _ = n.Close() return nil, err } @@ -156,6 +186,10 @@ func (n *Node) createIdentity(ctx context.Context, memberID string, pqc bool) er if err != nil { return err } + ak, err := n.core.GenerateKey(ctx, sigAlg, wire.UsageSign, "audit/"+memberID) + if err != nil { + return err + } n.identity = &store.KeyRecord{KeyID: store.KeyIDString(id.KeyID), Alg: sigAlg, Usage: wire.UsageSign, Label: "identity/" + memberID, Blob: id.Blob, PublicKey: id.PublicKey, CreatedAt: now} n.kem = &store.KeyRecord{KeyID: store.KeyIDString(kem.KeyID), Alg: kemAlg, Usage: wire.UsageDecapsulate, Label: "kem/" + memberID, Blob: kem.Blob, PublicKey: kem.PublicKey, CreatedAt: now} if err := n.st.PutKey(n.identity); err != nil { @@ -164,7 +198,11 @@ func (n *Node) createIdentity(ctx context.Context, memberID string, pqc bool) er if err := n.st.PutKey(n.kem); err != nil { return err } - n.info = &store.Node{MemberID: memberID, IdentityKeyID: n.identity.KeyID, KemKeyID: n.kem.KeyID, PQC: pqc, CreatedAt: now} + n.auditKey = &store.KeyRecord{KeyID: store.KeyIDString(ak.KeyID), Alg: sigAlg, Usage: wire.UsageSign, Label: "audit/" + memberID, Blob: ak.Blob, PublicKey: ak.PublicKey, CreatedAt: now} + if err := n.st.PutKey(n.auditKey); err != nil { + return err + } + n.info = &store.Node{MemberID: memberID, IdentityKeyID: n.identity.KeyID, KemKeyID: n.kem.KeyID, AuditKeyID: n.auditKey.KeyID, PQC: pqc, CreatedAt: now} return n.st.PutNode(n.info) } @@ -181,12 +219,7 @@ func Open(ctx context.Context, o Options) (*Node, error) { if err != nil { return nil, err } - log, err := audit.Open(st.AuditLogPath(), o.Now) - if err != nil { - _ = c.Close() - return nil, err - } - n := &Node{opts: o, st: st, core: c, log: log, info: info} + n := &Node{opts: o, st: st, core: c, info: info} if info.PQC && !c.PQC() { _ = n.Close() return nil, ErrPQCUnavailable @@ -199,9 +232,14 @@ func Open(ctx context.Context, o Options) (*Node, error) { _ = n.Close() return nil, fmt.Errorf("node: kem key: %w", err) } - if n.identity.Usage != wire.UsageSign || !exchange.IsSignatureAlg(n.identity.Alg) || - n.kem.Usage != wire.UsageDecapsulate || !exchange.IsKemAlg(n.kem.Alg) || - len(n.identity.PublicKey) == 0 || len(n.kem.PublicKey) == 0 { + if n.auditKey, err = n.Key(ctx, info.AuditKeyID); err != nil { + _ = n.Close() + return nil, fmt.Errorf("node: audit key: %w", err) + } + if n.identity.Usage != wire.UsageSign || !exchange.IsSignatureAlg(n.identity.Alg) || n.identity.Label != "identity/"+info.MemberID || + n.kem.Usage != wire.UsageDecapsulate || !exchange.IsKemAlg(n.kem.Alg) || n.kem.Label != "kem/"+info.MemberID || + n.auditKey.Usage != wire.UsageSign || !exchange.IsSignatureAlg(n.auditKey.Alg) || n.auditKey.Label != "audit/"+info.MemberID || + len(n.identity.PublicKey) == 0 || len(n.kem.PublicKey) == 0 || len(n.auditKey.PublicKey) == 0 { _ = n.Close() return nil, ErrMetadata } @@ -209,6 +247,10 @@ func Open(ctx context.Context, o Options) (*Node, error) { _ = n.Close() return nil, err } + if err := n.openAudit(); err != nil { + _ = n.Close() + return nil, err + } if err := n.audit("node.open", map[string]string{"member": info.MemberID, "fips": fmt.Sprint(c.FIPS()), "pqc": fmt.Sprint(c.PQC())}, nil); err != nil { _ = n.Close() return nil, err @@ -217,9 +259,12 @@ func Open(ctx context.Context, o Options) (*Node, error) { } func (n *Node) Close() error { + var lerr error + if n.log != nil { + lerr = n.log.Close() + } cerr := n.core.Close() - lerr := n.log.Close() - return errors.Join(cerr, lerr) + return errors.Join(lerr, cerr) } func (n *Node) AuditLogPath() string { return n.st.AuditLogPath() } diff --git a/host/internal/node/node_test.go b/host/internal/node/node_test.go index 9bfb270..8816368 100644 --- a/host/internal/node/node_test.go +++ b/host/internal/node/node_test.go @@ -153,7 +153,7 @@ func TestExchangeBetweenNodes(t *testing.T) { } defer reopened.Close() keys, err := reopened.Keys() - if err != nil || len(keys) != 4 { + if err != nil || len(keys) != 5 { t.Fatalf("keys after reopen %d %v", len(keys), err) } if _, err := reopened.Key(ctx, k.KeyID); err != nil { @@ -190,9 +190,53 @@ func TestExchangeBetweenNodes(t *testing.T) { if !failed { t.Fatal("failed send to unknown member not audited") } - if n, err := audit.Verify(bob.AuditLogPath()); err != nil || n == 0 { + if n, err := bob.VerifyAudit(); err != nil || n == 0 { t.Fatalf("bob audit %d %v", n, err) } + if _, err := audit.Verify(bob.AuditLogPath(), checkpoints{n: reopened}); !errors.Is(err, audit.ErrSignature) { + t.Fatalf("alice's audit key verified bob's log: %v", err) + } +} + +func TestAuditLogCannotBeRewrittenWithoutTheCore(t *testing.T) { + ctx := context.Background() + alice, o := initNode(t, "alice", false) + if _, err := alice.GenerateKey(ctx, "k", wire.UsageEncrypt); err != nil { + t.Fatal(err) + } + if n, err := alice.VerifyAudit(); err != nil || n != 2 { + t.Fatalf("verify %d %v", n, err) + } + if err := alice.Close(); err != nil { + t.Fatal(err) + } + path := filepath.Join(o.Dir, "audit.log") + entries, err := audit.Read(path) + if err != nil { + t.Fatal(err) + } + forged := filepath.Join(t.TempDir(), "forged.log") + fl, err := audit.Open(forged, audit.Options{}) + if err != nil { + t.Fatal(err) + } + for _, e := range entries { + f := e.Fields + if e.Event == "key.generate" { + f = map[string]string{"key": "0000", "label": "k", "usage": "1"} + } + if err := fl.Append(e.Event, f); err != nil { + t.Fatal(err) + } + } + _ = fl.Close() + raw, _ := os.ReadFile(forged) + if err := os.WriteFile(path, raw, 0o600); err != nil { + t.Fatal(err) + } + if _, err := Open(ctx, o); !errors.Is(err, audit.ErrSignature) { + t.Fatalf("node opened with a rewritten audit log: %v", err) + } } func TestStoredMetadataIsVerifiedAgainstCore(t *testing.T) { @@ -334,7 +378,7 @@ func TestTLSMaterialAndPeerLookup(t *testing.T) { t.Fatal("certificate subject") } keys, _ := reopened.Keys() - if len(keys) != 3 { - t.Fatalf("expected identity, kem and tls-wrap keys, got %d", len(keys)) + if len(keys) != 4 { + t.Fatalf("expected identity, kem, audit and tls-wrap keys, got %d", len(keys)) } } diff --git a/host/internal/store/store.go b/host/internal/store/store.go index 6efd1c8..96fa194 100644 --- a/host/internal/store/store.go +++ b/host/internal/store/store.go @@ -74,10 +74,16 @@ type Node struct { MemberID string `json:"member_id"` IdentityKeyID string `json:"identity_key_id"` KemKeyID string `json:"kem_key_id"` + AuditKeyID string `json:"audit_key_id"` PQC bool `json:"pqc"` CreatedAt int64 `json:"created_at"` } +func (n *Node) valid() bool { + return ValidMemberID(n.MemberID) && ValidKeyID(n.IdentityKeyID) && ValidKeyID(n.KemKeyID) && ValidKeyID(n.AuditKeyID) && + n.IdentityKeyID != n.KemKeyID && n.IdentityKeyID != n.AuditKeyID && n.KemKeyID != n.AuditKeyID +} + type TLSRecord struct { Cert []byte `json:"cert"` WrapKeyID string `json:"wrap_key_id"` @@ -248,7 +254,7 @@ func (s *Store) ListMembers() ([]*MemberRecord, error) { } func (s *Store) PutNode(n *Node) error { - if !ValidMemberID(n.MemberID) || !ValidKeyID(n.IdentityKeyID) || !ValidKeyID(n.KemKeyID) { + if !n.valid() { return ErrBadID } return s.writeNew(nodeFile, n) @@ -262,7 +268,7 @@ func (s *Store) GetNode() (*Node, error) { } return nil, err } - if !ValidMemberID(n.MemberID) || !ValidKeyID(n.IdentityKeyID) || !ValidKeyID(n.KemKeyID) { + if !n.valid() { return nil, ErrCorrupt } return &n, nil diff --git a/host/internal/store/store_test.go b/host/internal/store/store_test.go index dea1a95..bf5420d 100644 --- a/host/internal/store/store_test.go +++ b/host/internal/store/store_test.go @@ -146,7 +146,10 @@ func TestMembersNodeAndInbox(t *testing.T) { if err != nil || len(list) != 2 || list[0].MemberID != "alice" || list[1].Fingerprint != "fp" { t.Fatalf("members: %+v %v", list, err) } - n := &Node{MemberID: "alice", IdentityKeyID: KeyIDString([16]byte{1}), KemKeyID: KeyIDString([16]byte{2}), PQC: true, CreatedAt: 3} + if err := s.PutNode(&Node{MemberID: "alice", IdentityKeyID: KeyIDString([16]byte{1}), KemKeyID: KeyIDString([16]byte{1}), AuditKeyID: KeyIDString([16]byte{3})}); !errors.Is(err, ErrBadID) { + t.Fatalf("shared key ids accepted: %v", err) + } + n := &Node{MemberID: "alice", IdentityKeyID: KeyIDString([16]byte{1}), KemKeyID: KeyIDString([16]byte{2}), AuditKeyID: KeyIDString([16]byte{3}), PQC: true, CreatedAt: 3} if err := s.PutNode(n); err != nil { t.Fatal(err) } From febeea51263da7d7f5340559611af78cbe7edf73 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:33:59 +0900 Subject: [PATCH 22/48] =?UTF-8?q?inbox=20=EC=98=A4=EB=A5=98=20=EB=B6=84?= =?UTF-8?q?=EB=A5=98=EC=99=80=20=EC=A0=80=EC=9E=A5=EC=86=8C=20=EC=9E=84?= =?UTF-8?q?=EC=8B=9C=20=ED=8C=8C=EC=9D=BC=20=EC=B2=98=EB=A6=AC=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20-=20=EC=9D=B8=EC=A6=9D=EB=90=9C=20=ED=94=BC?= =?UTF-8?q?=EC=96=B4=EC=99=80=20=EB=B4=89=ED=88=AC=EC=9D=98=20=EC=86=A1?= =?UTF-8?q?=EC=8B=A0=EC=9E=90=EB=A5=BC=20=EC=84=9C=EB=AA=85=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=A0=84=EC=97=90=20=EB=8C=80=EC=A1=B0=ED=95=98?= =?UTF-8?q?=EA=B3=A0=20=EB=AF=B8=EB=93=B1=EB=A1=9D=20=EC=86=A1=EC=8B=A0?= =?UTF-8?q?=EC=9E=90=EB=8A=94=20400=EC=9C=BC=EB=A1=9C=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=20-=20=EC=A0=80=EC=9E=A5=EC=86=8C=20=EC=9E=84=EC=8B=9C=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=9D=B4=EB=A6=84=EC=9D=84=20=EB=AC=B4?= =?UTF-8?q?=EC=9E=91=EC=9C=84=EB=A1=9C=20=EB=A7=8C=EB=93=A4=EC=96=B4=20?= =?UTF-8?q?=EC=A4=91=EB=8B=A8=20=ED=9B=84=20=EB=82=A8=EC=9D=80=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EC=9D=B4=20=EC=93=B0=EA=B8=B0=EB=A5=BC=20=EB=A7=89?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?-=20kmsctl=20=ED=8C=8C=EC=9D=BC=20=EC=9E=85=EB=A0=A5=EC=97=90?= =?UTF-8?q?=EB=8F=84=201=20MiB=20=EC=83=81=ED=95=9C=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- host/cmd/kmsctl/main.go | 21 ++++++++++++++--- host/internal/api/api.go | 2 +- host/internal/api/api_test.go | 38 +++++++++++++++++++++++++++++++ host/internal/node/node.go | 11 +++++++++ host/internal/store/store.go | 11 ++++++--- host/internal/store/store_test.go | 16 +++++++++++-- 6 files changed, 90 insertions(+), 9 deletions(-) diff --git a/host/cmd/kmsctl/main.go b/host/cmd/kmsctl/main.go index 42a9c25..503498c 100644 --- a/host/cmd/kmsctl/main.go +++ b/host/cmd/kmsctl/main.go @@ -599,11 +599,26 @@ func (c *cli) cmdCrypt(ctx context.Context, args []string, encrypt bool) (int, e return 0, nil } +const maxInput = 1 << 20 + func (c *cli) readIn(path string) ([]byte, error) { - if path == "" { - return io.ReadAll(io.LimitReader(c.stdin, 1<<20)) + var r io.Reader = c.stdin + if path != "" { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + r = f + } + b, err := io.ReadAll(io.LimitReader(r, maxInput+1)) + if err != nil { + return nil, err + } + if len(b) > maxInput { + return nil, fmt.Errorf("input exceeds %d bytes", maxInput) } - return os.ReadFile(path) + return b, nil } func (c *cli) writeOut(path string, data []byte) error { diff --git a/host/internal/api/api.go b/host/internal/api/api.go index 7bbda7a..4663e86 100644 --- a/host/internal/api/api.go +++ b/host/internal/api/api.go @@ -120,7 +120,7 @@ func (s *Server) inbox(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusCreated, map[string]string{"id": rec.ID}) case errors.Is(err, store.ErrExists): fail(w, http.StatusConflict, "duplicate envelope") - case errors.Is(err, node.ErrNotRecipient), errors.Is(err, node.ErrSenderMismatch), + case errors.Is(err, node.ErrNotRecipient), errors.Is(err, node.ErrSenderMismatch), errors.Is(err, node.ErrUnknownMember), errors.Is(err, exchange.ErrFormat), errors.Is(err, exchange.ErrSignature), errors.Is(err, exchange.ErrBadMemberID), errors.Is(err, exchange.ErrAlgorithm), errors.Is(err, node.ErrSignatureAlg): diff --git a/host/internal/api/api_test.go b/host/internal/api/api_test.go index cc635ea..2e18d45 100644 --- a/host/internal/api/api_test.go +++ b/host/internal/api/api_test.go @@ -149,7 +149,45 @@ func TestPushInboxAndAccept(t *testing.T) { if _, err := bob.StoreInbox(ctx, env2, "carol"); !errors.Is(err, node.ErrSenderMismatch) { t.Fatalf("sender mismatch accepted: %v", err) } + trust(t, carol, bob) + envFromCarol, err := carol.Send(ctx, mustKey(t, carol), "bob", wire.UsageDecrypt) + if err != nil { + t.Fatal(err) + } + if _, err := NewClient(alice, nil).Push(ctx, bobURL, "bob", envFromCarol); err == nil || !strings.Contains(err.Error(), "400") { + t.Fatalf("relayed envelope from another sender accepted: %v", err) + } + dave := initNode(t, "dave") + trust(t, dave, carol) + envFromDave, err := dave.Send(ctx, mustKey(t, dave), "carol", wire.UsageDecrypt) + if err != nil { + t.Fatal(err) + } + if _, err := bob.StoreInbox(ctx, envFromDave, "dave"); !errors.Is(err, node.ErrNotRecipient) { + t.Fatalf("envelope for another recipient accepted: %v", err) + } + cb, rawB, _ := bob.Card(ctx) + fpB, _ := cb.Fingerprint() + if _, err := dave.AddMember(ctx, rawB, fpB); err != nil { + t.Fatal(err) + } + envToBob, err := dave.Send(ctx, mustKey(t, dave), "bob", wire.UsageDecrypt) + if err != nil { + t.Fatal(err) + } + if _, err := bob.StoreInbox(ctx, envToBob, "dave"); !errors.Is(err, node.ErrUnknownMember) { + t.Fatalf("envelope from an unknown member accepted: %v", err) + } if _, err := bob.StoreInbox(ctx, []byte("junk"), "alice"); !errors.Is(err, exchange.ErrFormat) { t.Fatalf("junk accepted: %v", err) } } + +func mustKey(t *testing.T, n *node.Node) string { + t.Helper() + k, err := n.GenerateKey(context.Background(), "relay", wire.UsageDecrypt|wire.UsageExport) + if err != nil { + t.Fatal(err) + } + return k.KeyID +} diff --git a/host/internal/node/node.go b/host/internal/node/node.go index b6262a4..ff74127 100644 --- a/host/internal/node/node.go +++ b/host/internal/node/node.go @@ -532,6 +532,17 @@ func (n *Node) StoreInbox(ctx context.Context, raw []byte, from string) (*store. } func (n *Node) storeInbox(ctx context.Context, raw []byte, from string) (*store.InboxRecord, error) { + env, err := exchange.DecodeEnvelope(raw) + if err != nil { + return nil, err + } + claimed, err := exchange.DecodeContext(env.Context) + if err != nil { + return nil, err + } + if claimed.Sender != from { + return nil, ErrSenderMismatch + } _, ectx, err := n.verifyEnvelope(ctx, raw) if err != nil { return nil, err diff --git a/host/internal/store/store.go b/host/internal/store/store.go index 96fa194..39fb8e1 100644 --- a/host/internal/store/store.go +++ b/host/internal/store/store.go @@ -360,7 +360,7 @@ func (s *Store) listIDs(sub, ext string, valid func(string) bool) ([]string, err ids := make([]string, 0, len(entries)) for _, e := range entries { name := e.Name() - if e.Type()&fs.ModeType != 0 || !strings.HasSuffix(name, ext) { + if e.Type()&fs.ModeType != 0 || !strings.HasSuffix(name, ext) || strings.HasPrefix(name, ".") { continue } id := strings.TrimSuffix(name, ext) @@ -413,11 +413,16 @@ func (s *Store) writeNew(rel string, v any) error { if err != nil { return err } - tmp := path + ".tmp" - f, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_EXCL, fileMode) + f, err := os.CreateTemp(filepath.Dir(path), "."+filepath.Base(path)+".*.tmp") if err != nil { return err } + tmp := f.Name() + if err := f.Chmod(fileMode); err != nil { + _ = f.Close() + _ = os.Remove(tmp) + return err + } if _, err := f.Write(data); err != nil { _ = f.Close() _ = os.Remove(tmp) diff --git a/host/internal/store/store_test.go b/host/internal/store/store_test.go index bf5420d..9850c48 100644 --- a/host/internal/store/store_test.go +++ b/host/internal/store/store_test.go @@ -191,7 +191,19 @@ func TestMembersNodeAndInbox(t *testing.T) { if err := s.DeleteInbox(rec.ID); !errors.Is(err, ErrNotFound) { t.Fatalf("expected ErrNotFound, got %v", err) } - if _, err := os.Lstat(filepath.Join(s.Dir(), "inbox", rec.ID+".json.tmp")); !errors.Is(err, os.ErrNotExist) { - t.Fatal("temp file left behind") + leftovers, _ := filepath.Glob(filepath.Join(s.Dir(), "*", ".*.tmp")) + if len(leftovers) != 0 { + t.Fatalf("temp files left behind: %v", leftovers) + } + stale := filepath.Join(s.Dir(), "inbox", "."+InboxID([]byte("next"))+".json.123.tmp") + if err := os.WriteFile(stale, []byte("partial"), 0o600); err != nil { + t.Fatal(err) + } + next := &InboxRecord{ID: InboxID([]byte("next")), Envelope: []byte("next")} + if err := s.PutInbox(next); err != nil { + t.Fatalf("stale temp file blocked a write: %v", err) + } + if list, err := s.ListInbox(); err != nil || len(list) != 1 { + t.Fatalf("stale temp file confused listing: %v %v", list, err) } } From a3d9d411fdb40e75f61d1acfa15810c735b1b500 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:35:43 +0900 Subject: [PATCH 23/48] =?UTF-8?q?=EA=B3=B5=EA=B8=89=EB=A7=9D=20=EC=A0=90?= =?UTF-8?q?=EA=B2=80=EC=9D=84=20cargo-deny=EB=A1=9C=20=ED=86=B5=EC=9D=BC?= =?UTF-8?q?=20-=20advisories=EA=B9=8C=EC=A7=80=20cargo-deny=20=ED=95=9C=20?= =?UTF-8?q?=EB=8F=84=EA=B5=AC=EB=A1=9C=20=EC=A0=90=EA=B2=80=ED=95=B4=20adv?= =?UTF-8?q?isory=20DB=20=EA=B2=BD=EB=A1=9C=20=EC=B6=A9=EB=8F=8C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20-=20make=20audit=EC=9D=80=20=EA=B8=B0?= =?UTF-8?q?=EC=A1=B4=20DB=EB=A1=9C=20=EC=98=A4=ED=94=84=EB=9D=BC=EC=9D=B8?= =?UTF-8?q?=20=EC=A0=90=EA=B2=80,=20CI=EB=8A=94=20=EC=B5=9C=EC=8B=A0=20DB?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=90=EA=B2=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 6 +----- Makefile | 3 +-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a49233..91e7f32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -117,11 +117,7 @@ jobs: - name: cargo deny run: | cargo install cargo-deny --version 0.19.8 --locked - cd core && cargo deny check licenses bans sources - - name: cargo audit - run: | - cargo install cargo-audit --version 0.22.2 --locked - cd core && cargo audit + cd core && cargo deny check advisories licenses bans sources - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e with: go-version-file: host/go.mod diff --git a/Makefile b/Makefile index c992f0f..406cbc7 100644 --- a/Makefile +++ b/Makefile @@ -79,8 +79,7 @@ dist: build ./scripts/dist.sh $(VERSION) $(CORE_RELEASE) $(HOST_DIR)/bin/kmsd $(HOST_DIR)/bin/kmsctl $(DIST_DIR) audit: - cd $(CORE_DIR) && $(CARGO) deny check licenses bans sources - cd $(CORE_DIR) && if [ -d "$$HOME/.cargo/advisory-db" ]; then $(CARGO) audit --no-fetch; else echo "advisory db missing, run: cargo audit fetch (online) or copy ~/.cargo/advisory-db"; fi + cd $(CORE_DIR) && $(CARGO) deny check --disable-fetch advisories licenses bans sources @command -v govulncheck >/dev/null && (cd $(HOST_DIR) && govulncheck ./...) || echo "govulncheck not installed, skipping" clean: From 4e766d4003068da581248670189fb87f0b9b784a Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:37:36 +0900 Subject: [PATCH 24/48] =?UTF-8?q?=EB=B0=B0=ED=8F=AC=EB=B3=B8=EC=97=90=20Cy?= =?UTF-8?q?cloneDX=20SBOM=20=ED=8F=AC=ED=95=A8=20-=20cargo=20metadata?= =?UTF-8?q?=EC=99=80=20Cargo=20lock=20=ED=8C=8C=EC=9D=BC=20=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=EC=84=AC,=20Go=20=EB=B9=8C=EB=93=9C=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EB=A1=9C=20=EA=B2=B0=EC=A0=95=EC=A0=81=20SBOM=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20(=ED=91=9C=EC=A4=80=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B8=8C=EB=9F=AC=EB=A6=AC=EB=A7=8C=20=EC=82=AC=EC=9A=A9)=20-?= =?UTF-8?q?=20=EB=9F=B0=ED=83=80=EC=9E=84=20=EC=9D=98=EC=A1=B4=EC=84=B1?= =?UTF-8?q?=EA=B3=BC=20=EB=B9=8C=EB=93=9C=20=EC=A0=84=EC=9A=A9=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EA=B5=AC=EB=B6=84,=20Go=20FIPS=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=EA=B3=BC=20=EB=B9=8C=EB=93=9C=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EA=B8=B0=EB=A1=9D=20-=20tarball=20=EB=82=B4?= =?UTF-8?q?=EB=B6=80=EC=99=80=20=EB=B3=84=EB=8F=84=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B0=B0=ED=8F=AC=ED=95=98=EA=B3=A0=20SHA256SUMS?= =?UTF-8?q?=EC=97=90=20=ED=8F=AC=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 1 + Makefile | 6 +- scripts/dist.sh | 13 ++-- scripts/sbom.py | 149 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 164 insertions(+), 5 deletions(-) create mode 100644 scripts/sbom.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 91e7f32..deecfc6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -151,4 +151,5 @@ jobs: name: pairier-${{ matrix.os }} path: | dist/*.tar.gz + dist/*.cdx.json dist/SHA256SUMS diff --git a/Makefile b/Makefile index 406cbc7..afdfcc0 100644 --- a/Makefile +++ b/Makefile @@ -76,7 +76,11 @@ update: dist: build mkdir -p $(DIST_DIR) - ./scripts/dist.sh $(VERSION) $(CORE_RELEASE) $(HOST_DIR)/bin/kmsd $(HOST_DIR)/bin/kmsctl $(DIST_DIR) + python3 -I scripts/sbom.py --version $(VERSION) --cargo "$(CARGO)" --core-flags "$(CORE_FLAGS)" \ + --go-binary $(HOST_DIR)/bin/kmsd --go-binary $(HOST_DIR)/bin/kmsctl \ + --epoch $(SOURCE_DATE_EPOCH) --out $(DIST_DIR)/sbom.cdx.json + ./scripts/dist.sh $(VERSION) $(CORE_RELEASE) $(HOST_DIR)/bin/kmsd $(HOST_DIR)/bin/kmsctl $(DIST_DIR) $(DIST_DIR)/sbom.cdx.json + rm -f $(DIST_DIR)/sbom.cdx.json audit: cd $(CORE_DIR) && $(CARGO) deny check --disable-fetch advisories licenses bans sources diff --git a/scripts/dist.sh b/scripts/dist.sh index df2a7a7..973f783 100755 --- a/scripts/dist.sh +++ b/scripts/dist.sh @@ -6,6 +6,7 @@ core_bin="$2" kmsd_bin="$3" kmsctl_bin="$4" out_dir="$5" +sbom="$6" epoch="${SOURCE_DATE_EPOCH:-0}" os=$(uname -s | tr '[:upper:]' '[:lower:]') @@ -35,7 +36,8 @@ chmod 0755 "$stage"/* "$stage/kms-core" --version > "$stage/VERSION" "$stage/kmsd" --version >> "$stage/VERSION" "$stage/kmsctl" version >> "$stage/VERSION" -chmod 0644 "$stage/VERSION" +cp "$sbom" "$stage/sbom.cdx.json" +chmod 0644 "$stage/VERSION" "$stage/sbom.cdx.json" ( cd "$out_dir" @@ -44,12 +46,15 @@ chmod 0644 "$stage/VERSION" --owner=0 --group=0 --numeric-owner \ --mtime="@$epoch" --format=gnu \ -cf - | gzip -n -9 > "$name.tar.gz" + cp "$name/sbom.cdx.json" "$name.cdx.json" rm -rf "$name" if command -v sha256sum >/dev/null 2>&1; then - sha256sum "$name.tar.gz" > "$name.tar.gz.sha256" + sum="sha256sum" else - shasum -a 256 "$name.tar.gz" > "$name.tar.gz.sha256" + sum="shasum -a 256" fi - cat ./*.tar.gz.sha256 | LC_ALL=C sort > SHA256SUMS + $sum "$name.tar.gz" > "$name.tar.gz.sha256" + $sum "$name.cdx.json" > "$name.cdx.json.sha256" + cat ./*.sha256 | LC_ALL=C sort > SHA256SUMS ) echo "$out_dir/$name.tar.gz" diff --git a/scripts/sbom.py b/scripts/sbom.py new file mode 100644 index 0000000..dd639a7 --- /dev/null +++ b/scripts/sbom.py @@ -0,0 +1,149 @@ +import argparse +import datetime +import hashlib +import json +import pathlib +import subprocess +import sys +import tomllib +import uuid + +CRATES_IO = "registry+https://github.com/rust-lang/crates.io-index" + + +def run(cmd, cwd=None): + return subprocess.run(cmd, cwd=cwd, check=True, capture_output=True, text=True).stdout + + +def host_triple(): + for line in run(["rustc", "-vV"], cwd="core").splitlines(): + if line.startswith("host: "): + return line.split(" ", 1)[1] + raise SystemExit("sbom: cannot determine host triple") + + +def cargo_components(cargo, flags): + meta = json.loads(run([cargo, "metadata", "--frozen", "--format-version", "1", + "--manifest-path", "core/crates/kms-core/Cargo.toml", + "--filter-platform", host_triple(), *flags])) + lock = tomllib.loads(pathlib.Path("core/Cargo.lock").read_text()) + checksums = {(p["name"], p["version"]): p.get("checksum") for p in lock["package"]} + packages = {p["id"]: p for p in meta["packages"]} + nodes = {n["id"]: n for n in meta["resolve"]["nodes"]} + root = next(pid for pid in meta["workspace_members"] if packages[pid]["name"] == "kms-core") + + def walk(start, kinds): + seen, stack = set(), [start] + while stack: + pid = stack.pop() + if pid in seen: + continue + seen.add(pid) + for d in nodes[pid]["deps"]: + if any(k["kind"] in kinds for k in d["dep_kinds"]): + stack.append(d["pkg"]) + return seen + + runtime = walk(root, {None}) + build = set() + for pid in runtime: + for d in nodes[pid]["deps"]: + if any(k["kind"] == "build" for k in d["dep_kinds"]): + build |= walk(d["pkg"], {None, "build"}) + build -= runtime + + def ref(pid): + p = packages[pid] + if p["source"] == CRATES_IO: + return f"pkg:cargo/{p['name']}@{p['version']}" + return f"pkg:generic/pairier/{p['name']}@{p['version']}" + + components, deps = [], [] + for pid in sorted(runtime | build, key=ref): + p = packages[pid] + c = {"type": "application" if pid == root else "library", "bom-ref": ref(pid), "name": p["name"], "version": p["version"]} + if p["source"] == CRATES_IO: + c["purl"] = ref(pid) + sha = checksums.get((p["name"], p["version"])) + if sha: + c["hashes"] = [{"alg": "SHA-256", "content": sha}] + if p.get("license"): + c["licenses"] = [{"expression": p["license"]}] + if pid in build: + c["scope"] = "excluded" + c["properties"] = [{"name": "pairier:role", "value": "build-only"}] + components.append(c) + kinds = {None} if pid in runtime else {None, "build"} + on = sorted({ref(d["pkg"]) for d in nodes[pid]["deps"] if any(k["kind"] in kinds or k["kind"] == "build" for k in d["dep_kinds"]) and d["pkg"] in runtime | build}) + deps.append({"ref": ref(pid), "dependsOn": on}) + return ref(root), components, deps + + +def go_components(binaries, version): + components, deps, refs = [], [], [] + go_version, fips = None, None + for b in binaries: + info = run(["go", "version", "-m", b]) + lines = info.splitlines() + go_version = lines[0].split(": ", 1)[1].strip() + name = pathlib.Path(b).name + r = f"pkg:golang/github.com/Quant-Off/pairier/host/cmd/{name}@{version}" + props = [] + for line in lines[1:]: + parts = line.strip().split("\t") + if parts[0] == "build" and len(parts) > 1 and "=" in parts[1]: + k, v = parts[1].split("=", 1) + if k in ("GOFIPS140", "DefaultGODEBUG", "CGO_ENABLED", "GOOS", "GOARCH", "vcs.revision"): + props.append({"name": f"go:build:{k}", "value": v}) + if k == "GOFIPS140": + fips = v + if parts[0] == "dep": + raise SystemExit(f"sbom: unexpected go dependency in {b}: {line}") + components.append({"type": "application", "bom-ref": r, "name": name, "version": version, "purl": r, "properties": sorted(props, key=lambda p: p["name"])}) + refs.append(r) + std = f"pkg:golang/stdlib@{go_version.removeprefix('go')}" + components.append({"type": "library", "bom-ref": std, "name": "go-stdlib", "version": go_version.removeprefix("go"), "purl": std, "licenses": [{"expression": "BSD-3-Clause"}]}) + on = [std] + if fips and fips != "off": + fr = f"pkg:generic/go/crypto-internal-fips140@{fips}" + components.append({"type": "library", "bom-ref": fr, "name": "go-cryptographic-module", "version": fips, "licenses": [{"expression": "BSD-3-Clause"}], + "properties": [{"name": "fips:standard", "value": "FIPS 140-3"}]}) + on.append(fr) + for r in refs: + deps.append({"ref": r, "dependsOn": sorted(on)}) + return refs, components, deps + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--version", required=True) + ap.add_argument("--cargo", default="cargo") + ap.add_argument("--core-flags", default="") + ap.add_argument("--go-binary", action="append", default=[]) + ap.add_argument("--epoch", type=int, default=0) + ap.add_argument("--out", required=True) + a = ap.parse_args() + + core_ref, cc, cd = cargo_components(a.cargo, a.core_flags.split()) + go_refs, gc, gd = go_components(a.go_binary, a.version) + top = f"pkg:generic/pairier@{a.version}" + bom = { + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "version": 1, + "metadata": { + "timestamp": datetime.datetime.fromtimestamp(a.epoch, datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + "component": {"type": "application", "bom-ref": top, "name": "pairier", "version": a.version}, + "properties": [{"name": "pairier:core-flags", "value": a.core_flags.strip()}], + }, + "components": sorted(cc + gc, key=lambda c: c["bom-ref"]), + "dependencies": sorted([{"ref": top, "dependsOn": sorted([core_ref, *go_refs])}] + cd + gd, key=lambda d: d["ref"]), + } + body = json.dumps(bom, sort_keys=True, separators=(",", ":")).encode() + bom["serialNumber"] = "urn:uuid:" + str(uuid.uuid5(uuid.NAMESPACE_URL, hashlib.sha256(body).hexdigest())) + pathlib.Path(a.out).write_text(json.dumps(bom, sort_keys=True, indent=2) + "\n") + print(a.out) + + +if __name__ == "__main__": + sys.exit(main()) From 943f6256a493610e17a05441070e7dbfb30456cc Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:39:00 +0900 Subject: [PATCH 25/48] =?UTF-8?q?SBOM=20=EC=83=9D=EC=84=B1=20=EC=8B=9C=20c?= =?UTF-8?q?argo=EB=A5=BC=20core=20=EB=94=94=EB=A0=89=ED=84=B0=EB=A6=AC?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=8B=A4=ED=96=89=20-=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=20=EB=94=94=EB=A0=89=ED=84=B0=EB=A6=AC=20=EA=B8=B0?= =?UTF-8?q?=EC=A4=80=20=EC=84=A4=EC=A0=95=20=ED=83=90=EC=83=89=20=EB=95=8C?= =?UTF-8?q?=EB=AC=B8=EC=97=90=20=EB=A3=A8=ED=8A=B8=EC=97=90=EC=84=9C?= =?UTF-8?q?=EB=8A=94=20vendor=20=EB=8C=80=EC=B2=B4=EA=B0=80=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=EB=90=98=EC=A7=80=20=EC=95=8A=EC=95=84=20=EC=98=A4?= =?UTF-8?q?=ED=94=84=EB=9D=BC=EC=9D=B8=EC=97=90=EC=84=9C=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=ED=95=98=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/sbom.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/sbom.py b/scripts/sbom.py index dd639a7..6b6dc0a 100644 --- a/scripts/sbom.py +++ b/scripts/sbom.py @@ -23,9 +23,9 @@ def host_triple(): def cargo_components(cargo, flags): - meta = json.loads(run([cargo, "metadata", "--frozen", "--format-version", "1", - "--manifest-path", "core/crates/kms-core/Cargo.toml", - "--filter-platform", host_triple(), *flags])) + meta = json.loads(run([cargo, "metadata", "--frozen", "--offline", "--format-version", "1", + "--manifest-path", "crates/kms-core/Cargo.toml", + "--filter-platform", host_triple(), *flags], cwd="core")) lock = tomllib.loads(pathlib.Path("core/Cargo.lock").read_text()) checksums = {(p["name"], p["version"]): p.get("checksum") for p in lock["package"]} packages = {p["id"]: p for p in meta["packages"]} From 429ec046813b08c961337abfecbe1a69a3bb97ad Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:39:32 +0900 Subject: [PATCH 26/48] =?UTF-8?q?Rust=20=EC=99=80=EC=9D=B4=EC=96=B4=20?= =?UTF-8?q?=EB=94=94=EC=BD=94=EB=8D=94=20=EB=AC=B4=EC=9E=91=EC=9C=84=20?= =?UTF-8?q?=EB=B3=80=EC=9D=B4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20-=20?= =?UTF-8?q?=EC=A0=81=ED=95=A9=EC=84=B1=20=ED=94=84=EB=A0=88=EC=9E=84?= =?UTF-8?q?=EC=9D=84=204=EB=A7=8C=20=ED=9A=8C=20=EB=B3=80=EC=9D=B4?= =?UTF-8?q?=ED=95=B4=20=EB=94=94=EC=BD=94=EB=8D=94=EA=B0=80=20=ED=8C=A8?= =?UTF-8?q?=EB=8B=89=20=EC=97=86=EC=9D=B4=20=EA=B1=B0=EB=B6=80=ED=95=98?= =?UTF-8?q?=EA=B1=B0=EB=82=98=20=EC=A0=95=EA=B7=9C=20=EC=9D=B8=EC=BD=94?= =?UTF-8?q?=EB=94=A9=EB=A7=8C=20=EC=88=98=EC=9A=A9=ED=95=98=EB=8A=94?= =?UTF-8?q?=EC=A7=80=20=EA=B2=80=EC=82=AC=20-=20=EC=99=B8=EB=B6=80=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=97=86=EC=9D=B4=20=EA=B2=B0?= =?UTF-8?q?=EC=A0=95=EC=A0=81=20=EC=8B=9C=EB=93=9C=EB=A1=9C=20=EB=8F=99?= =?UTF-8?q?=EC=9E=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/crates/kms-wire/tests/mutation.rs | 139 +++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 core/crates/kms-wire/tests/mutation.rs diff --git a/core/crates/kms-wire/tests/mutation.rs b/core/crates/kms-wire/tests/mutation.rs new file mode 100644 index 0000000..95ed4db --- /dev/null +++ b/core/crates/kms-wire/tests/mutation.rs @@ -0,0 +1,139 @@ +#![allow( + clippy::unwrap_used, + clippy::expect_used, + clippy::panic, + clippy::indexing_slicing, + clippy::arithmetic_side_effects, + clippy::cast_possible_truncation, + clippy::as_conversions +)] + +use kms_wire::msg::{ + AeadDecryptRequest, AeadDecryptResponse, AeadEncryptRequest, AeadEncryptResponse, HelloRequest, + HelloResponse, KeyExportRequest, KeyExportResponse, KeyGenerateRequest, KeyGenerateResponse, + KeyImportRequest, KeyImportResponse, KeyInspectRequest, KeyInspectResponse, Message, + PingRequest, PingResponse, RandomRequest, RandomResponse, SignRequest, SignResponse, + VerifyRequest, VerifyResponse, +}; +use kms_wire::{Error, HEADER_LEN, Header, Kind, Op}; + +const VECTORS: &str = include_str!("../../../../conformance/wire/v1.txt"); + +struct Rng(u64); + +impl Rng { + fn next(&mut self) -> u64 { + self.0 ^= self.0 << 13; + self.0 ^= self.0 >> 7; + self.0 ^= self.0 << 17; + self.0 + } + + fn below(&mut self, n: usize) -> usize { + (self.next() % n as u64) as usize + } +} + +fn unhex(s: &str) -> Vec { + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()) + .collect() +} + +fn roundtrip(op: u16, kind: Kind, body: &[u8]) -> Result, Error> { + let op = Op::try_from(op)?; + match (op, kind) { + (Op::Hello, Kind::Request) => HelloRequest::decode_body(body)?.encode_body(), + (Op::Hello, Kind::Response) => HelloResponse::decode_body(body)?.encode_body(), + (Op::Ping, Kind::Request) => PingRequest::decode_body(body)?.encode_body(), + (Op::Ping, Kind::Response) => PingResponse::decode_body(body)?.encode_body(), + (Op::KeyGenerate, Kind::Request) => KeyGenerateRequest::decode_body(body)?.encode_body(), + (Op::KeyGenerate, Kind::Response) => KeyGenerateResponse::decode_body(body)?.encode_body(), + (Op::KeyInspect, Kind::Request) => KeyInspectRequest::decode_body(body)?.encode_body(), + (Op::KeyInspect, Kind::Response) => KeyInspectResponse::decode_body(body)?.encode_body(), + (Op::KeyExport, Kind::Request) => KeyExportRequest::decode_body(body)?.encode_body(), + (Op::KeyExport, Kind::Response) => KeyExportResponse::decode_body(body)?.encode_body(), + (Op::KeyImport, Kind::Request) => KeyImportRequest::decode_body(body)?.encode_body(), + (Op::KeyImport, Kind::Response) => KeyImportResponse::decode_body(body)?.encode_body(), + (Op::AeadEncrypt, Kind::Request) => AeadEncryptRequest::decode_body(body)?.encode_body(), + (Op::AeadEncrypt, Kind::Response) => AeadEncryptResponse::decode_body(body)?.encode_body(), + (Op::AeadDecrypt, Kind::Request) => AeadDecryptRequest::decode_body(body)?.encode_body(), + (Op::AeadDecrypt, Kind::Response) => AeadDecryptResponse::decode_body(body)?.encode_body(), + (Op::Sign, Kind::Request) => SignRequest::decode_body(body)?.encode_body(), + (Op::Sign, Kind::Response) => SignResponse::decode_body(body)?.encode_body(), + (Op::Verify, Kind::Request) => VerifyRequest::decode_body(body)?.encode_body(), + (Op::Verify, Kind::Response) => VerifyResponse::decode_body(body)?.encode_body(), + (Op::Random, Kind::Request) => RandomRequest::decode_body(body)?.encode_body(), + (Op::Random, Kind::Response) => RandomResponse::decode_body(body)?.encode_body(), + } +} + +fn mutate(rng: &mut Rng, input: &[u8]) -> Vec { + let mut b = input.to_vec(); + for _ in 0..=rng.below(4) { + match rng.below(6) { + 0 if !b.is_empty() => { + let i = rng.below(b.len()); + b[i] ^= 1 << rng.below(8); + } + 1 if !b.is_empty() => { + let i = rng.below(b.len()); + b[i] = rng.next() as u8; + } + 2 if !b.is_empty() => { + let n = rng.below(b.len()); + b.truncate(n); + } + 3 => { + let i = rng.below(b.len() + 1); + b.insert(i, rng.next() as u8); + } + 4 if b.len() >= 4 => { + let i = rng.below(b.len() - 3); + let v = [0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0]; + let o = rng.below(5); + b[i..i + 4].copy_from_slice(&v[o..o + 4]); + } + _ => b.extend((0..rng.below(8)).map(|_| rng.next() as u8)), + } + } + b +} + +#[test] +fn mutated_frames_never_panic_and_accepted_bodies_are_canonical() { + let frames: Vec> = VECTORS + .lines() + .filter_map(|l| l.strip_prefix("frame ")) + .map(|l| unhex(l.split_whitespace().nth(1).unwrap())) + .collect(); + assert!(frames.len() >= 30); + let mut rng = Rng(0x9e37_79b9_7f4a_7c15); + let mut accepted = 0usize; + let mut rejected = 0usize; + for round in 0..40_000 { + let base = &frames[round % frames.len()]; + let (h, body) = base.split_at(HEADER_LEN); + let mut hb: [u8; HEADER_LEN] = h.try_into().unwrap(); + if rng.below(8) == 0 { + let i = rng.below(HEADER_LEN); + hb[i] ^= 1 << rng.below(8); + } + let Ok(hdr) = Header::decode(&hb) else { + rejected += 1; + continue; + }; + assert_eq!(hdr.encode(), hb, "accepted header is not canonical"); + let mutated = mutate(&mut rng, body); + match roundtrip(hdr.op, hdr.kind, &mutated) { + Ok(re) => { + assert_eq!(re, mutated, "round {round}: accepted body is not canonical"); + accepted += 1; + } + Err(_) => rejected += 1, + } + } + assert!(accepted > 1_000, "accepted={accepted}"); + assert!(rejected > 1_000, "rejected={rejected}"); +} From 8d9acfba03254d643be32459e1b7cdded3f92653 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:41:09 +0900 Subject: [PATCH 27/48] =?UTF-8?q?CI=20core=20=EC=9E=A1=EC=97=90=20Go=20?= =?UTF-8?q?=EC=84=A4=EC=B9=98=20=EC=B6=94=EA=B0=80=20-=20all-features=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EA=B0=80=20aws-lc-fips-sys=EB=A5=BC?= =?UTF-8?q?=20=EB=B9=8C=EB=93=9C=ED=95=98=EB=AF=80=EB=A1=9C=20=EA=B3=A0?= =?UTF-8?q?=EC=A0=95=EB=90=9C=20setup-go=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index deecfc6..ba09a91 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,10 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e + with: + go-version-file: host/go.mod + cache: false - name: toolchain run: cd core && rustup show active-toolchain - name: lint From 5de56a099a3fb446d435dcd692a7fc8b6690744c Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:44:07 +0900 Subject: [PATCH 28/48] =?UTF-8?q?=ED=95=9C=EA=B5=AD=EC=96=B4=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EB=B2=95=20=EB=AC=B8=EC=84=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?-=20=EB=B9=8C=EB=93=9C=EC=99=80=20=EC=84=A4=EC=B9=98,=20?= =?UTF-8?q?=EB=85=B8=EB=93=9C=20=EC=B4=88=EA=B8=B0=ED=99=94=EB=B6=80?= =?UTF-8?q?=ED=84=B0=20=EC=98=A4=ED=94=84=EB=9D=BC=EC=9D=B8=EA=B3=BC=20?= =?UTF-8?q?=EC=98=A8=EB=9D=BC=EC=9D=B8=20=ED=82=A4=20=EA=B5=90=ED=99=98?= =?UTF-8?q?=EA=B9=8C=EC=A7=80=20=EB=B9=A0=EB=A5=B8=20=EC=8B=9C=EC=9E=91=20?= =?UTF-8?q?-=20kmsctl,=20kmsd,=20kms-core=20=EC=98=B5=EC=85=98=20=EC=B0=B8?= =?UTF-8?q?=EC=A1=B0=EC=99=80=20=ED=82=A4=20=EC=9A=A9=EB=8F=84,=20?= =?UTF-8?q?=EB=94=94=EB=A0=89=ED=84=B0=EB=A6=AC=20=EA=B5=AC=EC=A1=B0,=20?= =?UTF-8?q?=EB=B3=B4=EC=95=88=20=EC=9A=B4=EC=98=81=20=EC=A3=BC=EC=9D=98=20?= =?UTF-8?q?=EC=82=AC=ED=95=AD,=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- USAGE_KR.md | 335 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 USAGE_KR.md diff --git a/USAGE_KR.md b/USAGE_KR.md new file mode 100644 index 0000000..683c0b4 --- /dev/null +++ b/USAGE_KR.md @@ -0,0 +1,335 @@ +# pairier 사용법 + +pairier는 구성원 간에 대칭 키를 안전하게 생성하고 교환하기 위한 KMS입니다. 구성원마다 자기 노드 디렉터리를 두고, 키 재료는 항상 각자의 보안 코어(`kms-core`) 안에서만 평문으로 존재합니다. 다른 구성원에게 키를 보낼 때는 수신자의 KEM 공개키로 봉인하고 송신자의 신원 키로 서명한 봉투(envelope)를 전달합니다. + +## 구성 요소 + +| 바이너리 | 역할 | +| --- | --- | +| `kms-core` | Rust 보안 코어. 루트 키와 모든 암호 연산을 담당하며 호스트가 자식 프로세스로 기동합니다. | +| `kmsctl` | 구성원이 쓰는 명령행 도구. 노드 초기화, 구성원 등록, 키 생성과 교환, 암복호화를 수행합니다. | +| `kmsd` | 다른 구성원이 보낸 봉투를 mTLS로 받는 데몬. 온라인 전달을 쓸 때만 필요합니다. | + +`kmsctl`과 `kmsd`는 실행될 때마다 `kms-core`를 자식 프로세스로 띄우고, 상속한 소켓 하나로만 통신합니다. 파일 시스템에 소켓이 노출되지 않습니다. + +## 빌드 + +요구 사항은 다음과 같습니다. + +- Rust `1.95.0` (`core/rust-toolchain.toml`로 고정), Go `1.27.2` +- FIPS 코어 빌드: `cmake`, `perl`, Go. 정적 링크는 Linux x86_64/aarch64에서만 지원 +- 배포본 생성: GNU tar (macOS는 `brew install gnu-tar`), `python3` + +모든 빌드는 벤더링된 의존성만 사용하며 네트워크 없이 동작합니다. + +```bash +$ make build # 코어(release, pqc 포함) + kmsd, kmsctl +$ make build-core FIPS=1 # AWS-LC FIPS 140-3 모듈로 코어 빌드 (Linux) +$ make test # Rust 전체 피처 테스트 + Go race 테스트 (실제 코어 사용) +$ make lint +$ make audit # cargo-deny로 취약점, 라이선스, 소스 점검 (로컬 DB 사용) +$ make dist FIPS=1 # 재현 가능한 배포 tarball, SBOM, SHA256SUMS를 dist/에 생성 +``` + +Go 바이너리는 기본적으로 Go 인증 암호 모듈(`GOFIPS140=certified`)로 빌드됩니다. 해제하려면 `GOFIPS140=off make build-host`를 사용합니다. 현재 모드는 버전 출력에서 확인할 수 있습니다. + +```bash +$ kms-core --version +kms-core 1.0.0 (kms-engine 1.0.0, fips=true, pqc=true) +$ kmsctl version +kmsctl 1.0.0 (go go1.27.2, go_fips=true) +``` + +## 설치 + +배포 tarball에는 `kms-core`, `kmsd`, `kmsctl`, `VERSION`, `sbom.cdx.json`이 들어 있습니다. 풀기 전에 체크섬을 확인합니다. + +```bash +$ sha256sum -c SHA256SUMS +$ tar -xzf pairier-1.0.0-linux-amd64.tar.gz +``` + +`kmsctl`과 `kmsd`는 같은 디렉터리에 있는 `kms-core`를 기본으로 사용합니다. 다른 위치에 두었다면 `--core` 플래그나 `KMS_CORE` 환경 변수로 지정합니다. 노드 디렉터리는 `--dir` 플래그나 `KMS_DIR` 환경 변수로 지정합니다. + +## 빠른 시작 + +alice가 키를 만들어 bob에게 복호화 권한만 넘기는 흐름입니다. 각 명령은 해당 구성원의 컴퓨터에서 실행합니다. + +### 1. 노드 초기화 + +```bash +alice$ kmsctl --dir ~/.pairier init --id alice --pqc +member alice +identity ml-dsa-65 +kem hybrid-p256-ml-kem-768 +core_fips false +go_fips true +pqc true +fingerprint 664e-c20d-cced-8ac3-5422-d278-d9ad-107b-d225-0aea-1e45-ff50-cce1-c28e-0319-ed0f +``` + +초기화는 루트 키, 신원 서명 키, KEM 키, 감사 서명 키, TLS 키를 만듭니다. `--pqc`를 주면 ML-DSA-65 신원 키와 P-256 + ML-KEM-768 하이브리드 KEM을, 생략하면 ECDSA P-256과 DHKEM(P-256)을 사용합니다. + +> 한 그룹 안에서는 모든 구성원이 같은 모드를 쓰는 것을 권장합니다. pqc 노드는 일반 카드를 등록할 수 있지만, pqc 없이 초기화한 노드는 pqc 카드를 검증할 수 없어 등록을 거부합니다. + +### 2. 신원 카드 교환과 지문 확인 + +```bash +alice$ kmsctl --dir ~/.pairier card --out alice.card +member alice +fingerprint 664e-c20d-...-ed0f +``` + +카드 파일은 이메일이나 USB 등 어떤 경로로 전달해도 됩니다. 단, **지문은 반드시 카드와 다른 채널**(대면, 전화, 서명된 메시지 등)로 확인해야 합니다. 지문이 신뢰의 유일한 근거이며, CA나 TOFU(최초 접속 신뢰)는 쓰지 않습니다. + +### 3. 구성원 등록 + +```bash +alice$ kmsctl --dir ~/.pairier member add --card bob.card --fingerprint b567-baee-...-3d65 +added bob b567-baee-...-3d65 + +bob$ kmsctl --dir ~/.pairier member add --card alice.card --fingerprint 664e-c20d-...-ed0f +``` + +지문 비교는 대소문자, 공백, `-`, `:`를 무시합니다. 카드의 자기 서명이 맞지 않거나 지문이 다르면 등록이 거부됩니다. 키를 주고받으려면 양쪽 모두 상대를 등록해야 합니다. + +### 4. 키 생성 + +```bash +alice$ kmsctl --dir ~/.pairier key gen --label project/db +af8477cc1076f219bfb047d846629eb8 aes-256-gcm encrypt,decrypt,export project/db +``` + +생성되는 키는 AES-256-GCM 대칭 키입니다. `--usage`를 생략하면 `encrypt,decrypt,export`입니다. 다른 구성원에게 보낼 키에는 반드시 `export`가 있어야 합니다. + +### 5-a. 오프라인 전달 (파일) + +```bash +alice$ kmsctl --dir ~/.pairier key send --id af8477cc1076f219bfb047d846629eb8 --to bob --out db.kxp +sent af8477cc1076f219bfb047d846629eb8 to bob usage decrypt + +bob$ kmsctl --dir ~/.pairier key receive --in db.kxp +af8477cc1076f219bfb047d846629eb8 aes-256-gcm decrypt project/db from alice +``` + +`--usage`로 수신자에게 줄 권한을 정하며 기본값은 `decrypt`입니다. 원래 키가 가진 용도의 부분집합만 줄 수 있습니다. 봉투는 bob에게만 열리고, bob 쪽에서는 서명, 수신자, 컨텍스트를 모두 검증한 뒤에만 가져옵니다. 같은 키를 두 번 가져올 수는 없습니다. + +### 5-b. 온라인 전달 (kmsd) + +수신자가 `kmsd`를 실행해 두면 송신자가 직접 밀어 넣을 수 있습니다. + +```bash +bob$ kmsd --dir ~/.pairier --listen 0.0.0.0:7443 +kmsd 1.0.0 member bob listening on 0.0.0.0:7443 core_fips=false go_fips=true pqc=true + +alice$ kmsctl --dir ~/.pairier peer health --member bob --url https://bob.example:7443 +ok bob +alice$ kmsctl --dir ~/.pairier key push --id af8477cc... --to bob --url https://bob.example:7443 +pushed af8477cc... to bob envelope 854a37b90d476c1070be8c5449403071 +``` + +받은 봉투는 바로 키가 되지 않고 inbox에 보관됩니다. 수신자가 확인 후 수락하거나 거절합니다. + +```bash +bob$ kmsctl --dir ~/.pairier inbox list +854a37b90d476c1070be8c5449403071 alice 2026-10-14T09:42:00Z 2026-10-14T09:42:00Z +bob$ kmsctl --dir ~/.pairier inbox accept --id 854a37b90d476c1070be8c5449403071 +bob$ kmsctl --dir ~/.pairier inbox reject --id +``` + +연결은 TLS 1.3 상호 인증이며, 양쪽 모두 상대 카드에 들어 있는 TLS 공개키와 일치하는지 확인합니다. 등록되지 않은 구성원은 TLS 단계에서 거부됩니다. + +### 6. 암복호화 + +```bash +alice$ printf 'db password' | kmsctl --dir ~/.pairier encrypt --id af8477cc... --aad project/db --out secret.bin +bob$ kmsctl --dir ~/.pairier decrypt --id af8477cc... --aad project/db --in secret.bin +db password +``` + +암호문은 바이너리이며 평문보다 48바이트 깁니다(salt 32 + 태그 16). `--aad`는 암호화와 복호화에서 같아야 합니다. bob은 `decrypt` 권한만 받았으므로 `encrypt`를 시도하면 `usage denied`로 거부됩니다. + +### 7. 감사 로그 검증 + +```bash +bob$ kmsctl --dir ~/.pairier audit verify +verified 13 entries +``` + +## kmsctl 명령 참조 + +### 전역 옵션 + +| 옵션 | 기본값 | 설명 | +| --- | --- | --- | +| `--dir DIR` | `$KMS_DIR` | 노드 디렉터리 | +| `--core PATH` | `$KMS_CORE`, 없으면 kmsctl 옆의 `kms-core` | 코어 바이너리 | +| `--allow-unlocked-memory` | macOS `true`, 그 외 `false` | 메모리 잠금 없이 코어 실행 | +| `--timeout DUR` | `10s` | 코어 호출 제한 시간 | + +전역 옵션은 명령보다 앞에 둡니다. 예를 들어 `kmsctl --dir X key list`처럼 씁니다. + +### 명령 + +| 명령 | 설명 | +| --- | --- | +| `init --id MEMBER [--pqc]` | 빈 디렉터리에 노드를 만듭니다. | +| `card [--out FILE]` | 자기 서명된 신원 카드를 출력합니다. 지문은 stderr에 표시됩니다. | +| `member add --card FILE --fingerprint FP` | 지문을 확인한 뒤 구성원을 등록합니다. | +| `member list` | 등록된 구성원과 알고리즘, 지문을 표시합니다. | +| `key gen --label LABEL [--usage LIST]` | 대칭 키를 생성합니다. | +| `key list` | 키 목록을 표시합니다. 마지막 열은 `local` 또는 `from <송신자>`입니다. | +| `key info --id KEYID` | 키 상세와 출처를 표시합니다. | +| `key send --id KEYID --to MEMBER [--usage LIST] [--out FILE]` | 봉투 파일을 만듭니다. | +| `key receive [--in FILE]` | 봉투를 검증하고 키를 가져옵니다. | +| `key push --id KEYID --to MEMBER --url URL [--usage LIST]` | 수신자의 kmsd로 봉투를 보냅니다. | +| `inbox list`, `inbox accept --id ID`, `inbox reject --id ID` | kmsd가 받은 봉투를 처리합니다. | +| `peer health --member MEMBER --url URL` | 상대 kmsd와 상호 인증이 되는지 확인합니다. | +| `encrypt --id KEYID [--in FILE] [--out FILE] [--aad STR]` | 암호화합니다. 입력이 없으면 stdin을 씁니다. | +| `decrypt --id KEYID [--in FILE] [--out FILE] [--aad STR]` | 복호화합니다. | +| `audit verify` | 감사 로그 체인과 모든 체크포인트 서명을 검증합니다. | +| `version` | 버전과 Go FIPS 상태를 출력합니다. | + +`--out`으로 지정한 파일이 이미 있으면 덮어쓰지 않고 실패하며, 새 파일은 `0600` 권한으로 만들어집니다. 종료 코드는 성공 `0`, 실행 실패 `1`, 인자 오류 `2`입니다. + +## 키 용도 + +| 용도 | 의미 | +| --- | --- | +| `encrypt`, `decrypt` | AEAD 암호화, 복호화 | +| `export` | 다른 구성원에게 보낼 수 있음 | +| `wrap`, `unwrap`, `derive` | 예약된 대칭 키 용도 | +| `sign`, `verify`, `decapsulate` | 신원, 감사, KEM 키 전용 (내부에서 사용) | + +- `key gen`으로는 대칭 키 용도(`encrypt`, `decrypt`, `wrap`, `unwrap`, `derive`, `export`)만 지정할 수 있습니다. +- 보낼 때 주는 용도는 원래 키 용도의 부분집합이어야 합니다. 권한 상승은 코어가 거부합니다. +- 수신자에게 `export`를 주면 수신자가 그 키를 다시 다른 구성원에게 보낼 수 있습니다. 재공유를 막으려면 `export`를 주지 마십시오. + +## kmsd 운영 + +### 플래그와 설정 파일 + +| 플래그 | 설정 파일 필드 | 기본값 | +| --- | --- | --- | +| `--dir` | `dir` | 없음 (필수) | +| `--core` | `core` | kmsd 옆의 `kms-core` | +| `--listen` | `listen` | `127.0.0.1:7443` | +| `--allow-unlocked-memory` | `allow_unlocked_memory` | macOS `true`, 그 외 `false` | +| `--timeout` | `timeout` | `10s` | +| `--config FILE` | | | +| `--version` | | | + +```json +{ + "dir": "/var/lib/pairier/bob", + "core": "/opt/pairier/kms-core", + "listen": "0.0.0.0:7443", + "allow_unlocked_memory": false, + "timeout": "10s" +} +``` + +- 우선순위는 명시한 플래그 -> 설정 파일 -> 기본값 순입니다. +- 설정 파일은 일반 파일이어야 하고 그룹이나 다른 사용자가 쓸 수 없어야 합니다. 모르는 필드가 있으면 기동하지 않습니다. +- `listen`의 호스트는 IP 주소만 허용합니다. +- `kmsd`는 `kmsctl init`으로 이미 초기화한 노드만 엽니다. +- `SIGINT`나 `SIGTERM`을 받으면 진행 중인 요청을 마무리하고 종료합니다. +- `kmsd`가 실행 중이어도 같은 노드에서 `kmsctl`을 쓸 수 있습니다. 각자 별도의 코어를 띄우며, 저장소와 감사 로그는 동시 접근에 안전합니다. + +### HTTP API + +모든 엔드포인트는 등록된 구성원의 클라이언트 인증서를 요구합니다. + +| 메서드와 경로 | 설명 | +| --- | --- | +| `GET /v1/health` | 구성원 id, FIPS, PQC 상태 | +| `GET /v1/card` | 이 노드의 신원 카드 | +| `POST /v1/inbox` | 봉투 제출 (본문은 봉투 바이트). 성공 시 `201`과 봉투 id | + +봉투의 송신자가 인증된 피어와 다르거나, 수신자가 이 노드가 아니거나, 서명이 맞지 않으면 `400`으로 거부합니다. 같은 봉투를 다시 보내면 `409`입니다. + +### systemd 예시 + +```ini +[Service] +User=pairier +ExecStart=/opt/pairier/kmsd --config /etc/pairier/kmsd.json +LimitMEMLOCK=infinity +LimitCORE=0 +NoNewPrivileges=yes +ProtectSystem=strict +ReadWritePaths=/var/lib/pairier +PrivateTmp=yes +``` + +Linux에서 코어는 `mlockall`로 모든 메모리를 잠급니다. memlock 한도가 작으면 코어가 기동을 거부하므로 `LimitMEMLOCK=infinity`가 필요합니다. + +## kms-core 단독 실행 + +보통은 `kmsctl`과 `kmsd`가 코어를 직접 띄웁니다. 코어를 별도 사용자로 분리해 운영하려면 소켓 경로 모드를 씁니다. + +```bash +$ kms-core --generate-root-key /path/root.key +$ kms-core --socket /run/pairier/core.sock --root-key-file /path/root.key --pqc --peer-uid 1001 +``` + +| 옵션 | 설명 | +| --- | --- | +| `--fd N` | 상속받은 소켓 사용 (호스트가 기동할 때 `3`) | +| `--socket PATH` | 소켓 경로에서 연결 하나만 받은 뒤 경로를 삭제 | +| `--root-key-file PATH` | 32바이트 일반 파일만 허용. 현재 사용자 소유, 그룹과 기타 권한 없음, 심볼릭 링크와 추가 하드 링크 없음 | +| `--ephemeral-root` | 메모리에만 있는 임시 루트 키 (재시작하면 블롭을 열 수 없음) | +| `--pqc` | PQC 알고리즘 활성화 (pqc 피처로 빌드한 경우) | +| `--allow-unlocked-memory` | `mlockall`을 쓸 수 없는 환경에서 계속 진행 | +| `--peer-uid UID` | 연결한 피어의 uid 검증 (기본은 자신의 uid) | + +현재 `kmsctl`과 `kmsd`는 노드 디렉터리의 루트 키로 코어를 직접 기동하는 방식만 지원합니다. + +## 노드 디렉터리 + +```text +/ + root.key 루트 키 (0400). 모든 키 블롭을 여는 유일한 비밀 + node.json 구성원 id와 신원, KEM, 감사 키 id + keys/ 키 레코드. 키 재료는 루트 키로 봉인된 블롭으로만 저장 + members/ 등록된 구성원 카드와 지문 + inbox/ kmsd가 받은 봉투 + tls.json TLS 인증서와 코어로 암호화한 TLS 개인키 + audit.log 해시 체인 감사 로그 (항목마다 감사 키 서명) +``` + +- 디렉터리는 `0700`이고 현재 사용자 소유여야 하며, 파일에는 그룹과 기타 권한이 없어야 합니다. 심볼릭 링크는 열지 않습니다. +- 저장된 메타데이터와 공개키는 사용할 때마다 코어가 블롭에서 다시 계산한 값과 대조합니다. +- 백업에는 `root.key`가 포함되므로 백업 매체도 루트 키와 같은 수준으로 보호해야 합니다. + +## 보안 운영 주의 사항 + +- **지문은 별도 채널로 확인합니다.** 카드와 지문을 같은 경로로 받으면 중간자 공격을 막을 수 없습니다. +- **감사 로그의 한계를 이해해야 합니다.** 각 항목은 코어의 감사 키로 서명되므로, 코어를 쓸 수 없는 공격자(디스크 탈취, 변조된 백업 등)는 로그를 위조하거나 재계산할 수 없습니다. 반면 노드 사용자 권한으로 코어를 기동할 수 있으면 유효한 서명을 만들 수 있습니다. 또 로그 끝부분을 잘라 내는 것은 외부 기준점이 없으면 탐지할 수 없습니다. +- **kmsd는 TLS 개인키를 메모리에 둡니다.** 키 교환 대상인 대칭 키와 신원, KEM, 감사 키는 코어 밖으로 나오지 않지만, 전송용 TLS 키는 예외입니다. +- **macOS는 개발용입니다.** `mlockall`이 없어 메모리 잠금 없이 실행되고, FIPS 코어도 정적 링크되지 않습니다. 운영은 Linux에서 하십시오. +- **PQC 모드 감사 로그는 빠르게 커집니다.** ML-DSA 서명 때문에 항목 하나가 약 7KB입니다. 암복호화도 한 번마다 항목이 추가됩니다. + +## 한도 + +| 항목 | 한도 | +| --- | --- | +| 구성원 id | 영문 소문자, 숫자로 시작하고 소문자, 숫자, `.`, `_`, `-`로 최대 64자 | +| 키 label | 256바이트 | +| 암호화 평문, AAD | 각각 64KiB | +| kmsctl 입력 파일과 stdin | 1MiB | +| 봉투 컨텍스트 | 1KiB | + +## 문제 해결 + +| 메시지 | 원인과 조치 | +| --- | --- | +| `mlockall: Cannot allocate memory ... (pass --allow-unlocked-memory to proceed)` | memlock 한도가 작습니다. `ulimit -l unlimited` 또는 `LimitMEMLOCK=infinity`로 늘립니다. | +| `node: fingerprint does not match the card` | 지문이 다릅니다. 카드를 다시 받고 별도 채널로 지문을 재확인합니다. | +| `core: op 0x0023 failed: capability disabled` | pqc 없이 초기화한 노드에 pqc 카드를 등록하려 했습니다. | +| `core: op 0x0030 failed: usage denied` | 키에 `export`가 없거나, 원래 용도보다 넓은 용도를 주려 했습니다. | +| `node: envelope is addressed to another member` | 다른 구성원에게 보낸 봉투입니다. | +| `node: unknown member` | 송신자 또는 수신자가 등록되지 않았습니다. | +| `store: object already exists` | 이미 가져온 키이거나 이미 받은 봉투입니다. | +| `node: stored metadata does not match the core` | 노드 디렉터리의 레코드가 변조되었습니다. 백업과 감사 로그를 확인합니다. | +| `audit: checkpoint signature is invalid` | 감사 로그가 코어 없이 수정되었습니다. | From ada67406d71f7e425928d573bc10ab6a4bcc7b6f Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:52:39 +0900 Subject: [PATCH 29/48] =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=90?= =?UTF-8?q?=EC=9B=90=20=EC=B6=94=EC=A0=81=20=ED=95=B4=EC=A0=9C=20-=20?= =?UTF-8?q?=EC=8B=A0=EC=9B=90=20=EC=B9=B4=EB=93=9C=EC=9D=98=20=EC=9E=98?= =?UTF-8?q?=EB=AA=BB=EB=90=9C=20=EB=B0=B0=ED=8F=AC=EB=A5=BC=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=9C=20*.card=20?= =?UTF-8?q?=EC=B6=94=EC=A0=81=20=ED=95=B4=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 9ae50fd..c3517ed 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,7 @@ *.log *.pem *.key +*.card .DS_Store .idea/ +l-tests/ From d331e0095e8f864c12b70749e591ba1c98b3a2b1 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:05:30 +0900 Subject: [PATCH 30/48] =?UTF-8?q?=EC=BD=94=EC=96=B4=20=EA=B2=BD=EB=A1=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=A0=88=EB=8C=80=20=EA=B2=BD=EB=A1=9C=EB=A1=9C=20?= =?UTF-8?q?=ED=95=B4=EC=84=9D=20-=20=EC=BD=94=EC=96=B4=EB=A5=BC=20?= =?UTF-8?q?=EB=A3=A8=ED=8A=B8=20=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=8B=A4=ED=96=89=ED=95=98=EB=AF=80?= =?UTF-8?q?=EB=A1=9C=20=EC=83=81=EB=8C=80=20=EA=B2=BD=EB=A1=9C=EA=B0=80=20?= =?UTF-8?q?=ED=94=8C=EB=9E=98=EA=B7=B8=EC=99=80=20=ED=99=98=EA=B2=BD=20?= =?UTF-8?q?=EB=B3=80=EC=88=98=20=EB=AA=A8=EB=91=90=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=ED=95=98=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20-=20=EB=85=B8=EB=93=9C=20=EB=94=94?= =?UTF-8?q?=EB=A0=89=ED=86=A0=EB=A6=AC=20=EC=83=9D=EC=84=B1=20=EC=A0=84?= =?UTF-8?q?=EC=97=90=20=EC=BD=94=EC=96=B4=20=EC=8B=A4=ED=96=89=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EC=9D=84=20=EA=B2=80=EC=82=AC=ED=95=B4=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=ED=95=9C=20init=EC=9D=B4=20=EB=B9=88=20=EB=85=B8?= =?UTF-8?q?=EB=93=9C=EB=A5=BC=20=EB=82=A8=EA=B8=B0=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- host/internal/node/node.go | 28 +++++++++++++++++++-- host/internal/node/node_test.go | 43 +++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/host/internal/node/node.go b/host/internal/node/node.go index ff74127..a9eaa32 100644 --- a/host/internal/node/node.go +++ b/host/internal/node/node.go @@ -7,7 +7,9 @@ import ( "errors" "fmt" "io" + "os" "os/exec" + "path/filepath" "strings" "time" @@ -119,12 +121,31 @@ func (o *Options) spawn(ctx context.Context, rootKey string, pqc bool) (*core.Cl }) } +func (o *Options) resolveCore() error { + if o.CoreBinary == "" { + return errors.New("node: core binary path is required") + } + abs, err := filepath.Abs(o.CoreBinary) + if err != nil { + return fmt.Errorf("node: core binary: %w", err) + } + fi, err := os.Stat(abs) + if err != nil { + return fmt.Errorf("node: core binary: %w", err) + } + if !fi.Mode().IsRegular() || fi.Mode().Perm()&0o111 == 0 { + return fmt.Errorf("node: core binary %s is not an executable file", abs) + } + o.CoreBinary = abs + return nil +} + func Init(ctx context.Context, o Options, memberID string, pqc bool) (*Node, error) { if !exchange.ValidMemberID(memberID) { return nil, ErrBadMemberID } - if o.CoreBinary == "" { - return nil, errors.New("node: core binary path is required") + if err := o.resolveCore(); err != nil { + return nil, err } st, err := store.Init(o.Dir) if err != nil { @@ -207,6 +228,9 @@ func (n *Node) createIdentity(ctx context.Context, memberID string, pqc bool) er } func Open(ctx context.Context, o Options) (*Node, error) { + if err := o.resolveCore(); err != nil { + return nil, err + } st, err := store.Open(o.Dir) if err != nil { return nil, err diff --git a/host/internal/node/node_test.go b/host/internal/node/node_test.go index 8816368..319bdda 100644 --- a/host/internal/node/node_test.go +++ b/host/internal/node/node_test.go @@ -38,6 +38,49 @@ func initNode(t *testing.T, id string, pqc bool) (*Node, Options) { return n, o } +func TestInitChecksCoreBeforeCreatingDir(t *testing.T) { + base := t.TempDir() + dir := filepath.Join(base, "node") + notExec := filepath.Join(base, "kms-core") + if err := os.WriteFile(notExec, []byte("x"), 0o600); err != nil { + t.Fatal(err) + } + for _, bin := range []string{"", filepath.Join(base, "missing"), notExec, base} { + o := Options{Dir: dir, CoreBinary: bin, AllowUnlockedMemory: true, Timeout: time.Second} + if _, err := Init(context.Background(), o, "alice", false); err == nil { + t.Fatalf("core %q accepted", bin) + } + if _, err := os.Stat(dir); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("core %q: node dir created: %v", bin, err) + } + } +} + +func TestRelativeCoreBinary(t *testing.T) { + o := opts(t, filepath.Join(t.TempDir(), "alice")) + abs, err := filepath.Abs(o.CoreBinary) + if err != nil { + t.Fatal(err) + } + wd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + if o.CoreBinary, err = filepath.Rel(wd, abs); err != nil { + t.Fatal(err) + } + n, err := Init(context.Background(), o, "alice", false) + if err != nil { + t.Fatalf("relative core %q: %v", o.CoreBinary, err) + } + _ = n.Close() + n, err = Open(context.Background(), o) + if err != nil { + t.Fatalf("relative core %q: %v", o.CoreBinary, err) + } + _ = n.Close() +} + func pair(t *testing.T, a, b *Node) { t.Helper() ctx := context.Background() From 936b8e91554f0124c89bde2f963acc6cbf584852 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:05:30 +0900 Subject: [PATCH 31/48] =?UTF-8?q?kmsctl=20=EA=B3=B5=ED=86=B5=20=EC=98=B5?= =?UTF-8?q?=EC=85=98=EC=9D=84=20=EB=AA=85=EB=A0=B9=20=EB=92=A4=EC=97=90?= =?UTF-8?q?=EC=84=9C=EB=8F=84=20=ED=97=88=EC=9A=A9=20-=20--dir,=20--core,?= =?UTF-8?q?=20--allow-unlocked-memory,=20--timeout=EC=9D=84=20init=20?= =?UTF-8?q?=EB=93=B1=20=ED=95=98=EC=9C=84=20=EB=AA=85=EB=A0=B9=20=EB=92=A4?= =?UTF-8?q?=EC=97=90=20=EC=8D=A8=EB=8F=84=20=EC=A0=81=EC=9A=A9=20-=20?= =?UTF-8?q?=EC=9D=B8=EC=9E=90=EA=B0=80=20=EC=97=86=EB=8D=98=20=EB=AA=85?= =?UTF-8?q?=EB=A0=B9=EB=8F=84=20=EA=B0=99=EC=9D=80=20=ED=94=8C=EB=9E=98?= =?UTF-8?q?=EA=B7=B8=20=ED=8C=8C=EC=8B=B1=EC=9D=84=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- host/cmd/kmsctl/main.go | 95 ++++++++++++++++++++++++------------ host/cmd/kmsctl/main_test.go | 77 +++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 32 deletions(-) diff --git a/host/cmd/kmsctl/main.go b/host/cmd/kmsctl/main.go index 503498c..303f747 100644 --- a/host/cmd/kmsctl/main.go +++ b/host/cmd/kmsctl/main.go @@ -71,23 +71,32 @@ var algNames = map[uint16]string{ } type cli struct { - stdin io.Reader - stdout io.Writer - stderr io.Writer - opts node.Options + stdin io.Reader + stdout io.Writer + stderr io.Writer + dir string + core string + allowUnlocked bool + timeout time.Duration } +var errNoDir = errors.New("--dir or $KMS_DIR is required") + func main() { os.Exit(run(os.Args[1:], os.Stdin, os.Stdout, os.Stderr)) } func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int { - fs := flag.NewFlagSet("kmsctl", flag.ContinueOnError) - fs.SetOutput(stderr) - dir := fs.String("dir", os.Getenv("KMS_DIR"), "node directory (default $KMS_DIR)") - corePath := fs.String("core", os.Getenv("KMS_CORE"), "kms-core binary (default $KMS_CORE or next to kmsctl)") - allowUnlocked := fs.Bool("allow-unlocked-memory", runtime.GOOS == "darwin", "run the core without locked memory") - timeout := fs.Duration("timeout", 10*time.Second, "core call timeout") + c := &cli{ + stdin: stdin, + stdout: stdout, + stderr: stderr, + dir: os.Getenv("KMS_DIR"), + core: os.Getenv("KMS_CORE"), + allowUnlocked: runtime.GOOS == "darwin", + timeout: 10 * time.Second, + } + fs := c.flags("kmsctl") fs.Usage = func() { fmt.Fprint(stderr, usageText) } if err := fs.Parse(args); err != nil { return 2 @@ -101,25 +110,12 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int { fmt.Fprintf(stdout, "kmsctl %s (go %s, go_fips=%v)\n", version, runtime.Version(), fips140.Enabled()) return 0 } - if *dir == "" { - fmt.Fprintln(stderr, "kmsctl: --dir or $KMS_DIR is required") - return 2 - } - if *corePath == "" { - if exe, err := os.Executable(); err == nil { - *corePath = filepath.Join(filepath.Dir(exe), "kms-core") - } - } - c := &cli{stdin: stdin, stdout: stdout, stderr: stderr, opts: node.Options{ - Dir: *dir, - CoreBinary: *corePath, - AllowUnlockedMemory: *allowUnlocked, - Timeout: *timeout, - Stderr: stderr, - }} code, err := c.dispatch(rest) if err != nil { fmt.Fprintf(stderr, "kmsctl: %v\n", err) + if errors.Is(err, errNoDir) { + code = 2 + } if code == 0 { code = 1 } @@ -199,11 +195,38 @@ func (c *cli) dispatch(args []string) (int, error) { func (c *cli) flags(name string) *flag.FlagSet { fs := flag.NewFlagSet(name, flag.ContinueOnError) fs.SetOutput(c.stderr) + fs.StringVar(&c.dir, "dir", c.dir, "node directory (default $KMS_DIR)") + fs.StringVar(&c.core, "core", c.core, "kms-core binary (default $KMS_CORE or next to kmsctl)") + fs.BoolVar(&c.allowUnlocked, "allow-unlocked-memory", c.allowUnlocked, "run the core without locked memory") + fs.DurationVar(&c.timeout, "timeout", c.timeout, "core call timeout") return fs } +func (c *cli) options() (node.Options, error) { + if c.dir == "" { + return node.Options{}, errNoDir + } + core := c.core + if core == "" { + if exe, err := os.Executable(); err == nil { + core = filepath.Join(filepath.Dir(exe), "kms-core") + } + } + return node.Options{ + Dir: c.dir, + CoreBinary: core, + AllowUnlockedMemory: c.allowUnlocked, + Timeout: c.timeout, + Stderr: c.stderr, + }, nil +} + func (c *cli) open(ctx context.Context) (*node.Node, error) { - return node.Open(ctx, c.opts) + opts, err := c.options() + if err != nil { + return nil, err + } + return node.Open(ctx, opts) } func (c *cli) cmdInit(ctx context.Context, args []string) (int, error) { @@ -216,7 +239,11 @@ func (c *cli) cmdInit(ctx context.Context, args []string) (int, error) { if *id == "" { return 2, errors.New("init: --id is required") } - n, err := node.Init(ctx, c.opts, *id, *pqc) + opts, err := c.options() + if err != nil { + return 2, err + } + n, err := node.Init(ctx, opts, *id, *pqc) if err != nil { return 1, err } @@ -285,7 +312,8 @@ func (c *cli) cmdMemberAdd(ctx context.Context, args []string) (int, error) { } func (c *cli) cmdMemberList(ctx context.Context, args []string) (int, error) { - if len(args) != 0 { + fs := c.flags("member list") + if err := fs.Parse(args); err != nil || fs.NArg() != 0 { return 2, errors.New("member list: no arguments expected") } n, err := c.open(ctx) @@ -332,7 +360,8 @@ func (c *cli) cmdKeyGen(ctx context.Context, args []string) (int, error) { } func (c *cli) cmdKeyList(ctx context.Context, args []string) (int, error) { - if len(args) != 0 { + fs := c.flags("key list") + if err := fs.Parse(args); err != nil || fs.NArg() != 0 { return 2, errors.New("key list: no arguments expected") } n, err := c.open(ctx) @@ -468,7 +497,8 @@ func (c *cli) cmdKeyPush(ctx context.Context, args []string) (int, error) { } func (c *cli) cmdInboxList(ctx context.Context, args []string) (int, error) { - if len(args) != 0 { + fs := c.flags("inbox list") + if err := fs.Parse(args); err != nil || fs.NArg() != 0 { return 2, errors.New("inbox list: no arguments expected") } n, err := c.open(ctx) @@ -546,7 +576,8 @@ func (c *cli) cmdPeerHealth(ctx context.Context, args []string) (int, error) { } func (c *cli) cmdAuditVerify(ctx context.Context, args []string) (int, error) { - if len(args) != 0 { + fs := c.flags("audit verify") + if err := fs.Parse(args); err != nil || fs.NArg() != 0 { return 2, errors.New("audit verify: no arguments expected") } n, err := c.open(ctx) diff --git a/host/cmd/kmsctl/main_test.go b/host/cmd/kmsctl/main_test.go index d19fc75..56382f0 100644 --- a/host/cmd/kmsctl/main_test.go +++ b/host/cmd/kmsctl/main_test.go @@ -2,11 +2,13 @@ package main import ( "bytes" + "io" "os" "path/filepath" "strings" "sync" "testing" + "time" ) type syncBuffer struct { @@ -53,6 +55,65 @@ func TestUsageAndVersion(t *testing.T) { } } +func TestGlobalFlagsAfterCommand(t *testing.T) { + t.Setenv("KMS_DIR", "") + t.Setenv("KMS_CORE", "") + missing := filepath.Join(t.TempDir(), "kms-core") + globals := []string{"--core", missing, "--allow-unlocked-memory", "--timeout", "1s"} + for _, cmd := range [][]string{ + {"init", "--id", "alice"}, + {"card"}, + {"member", "list"}, + {"key", "gen", "--label", "x"}, + {"key", "list"}, + {"key", "info", "--id", "k"}, + {"inbox", "list"}, + {"inbox", "accept", "--id", "e"}, + {"inbox", "reject", "--id", "e"}, + {"peer", "health", "--member", "bob", "--url", "https://127.0.0.1:1"}, + {"audit", "verify"}, + } { + var out, errb bytes.Buffer + code := run(append(append([]string{}, cmd...), globals...), nil, &out, &errb) + if code != 2 || !strings.Contains(errb.String(), errNoDir.Error()) { + t.Fatalf("%v: %d %q", cmd, code, errb.String()) + } + } + dir := filepath.Join(t.TempDir(), "node") + var out, errb bytes.Buffer + code := run([]string{"init", "--id", "alice", "--dir", dir, "--core", missing}, nil, &out, &errb) + if code != 1 || !strings.Contains(errb.String(), missing) { + t.Fatalf("init ignored --core: %d %q", code, errb.String()) + } + if _, err := os.Stat(dir); !os.IsNotExist(err) { + t.Fatalf("node dir created with a missing core: %v", err) + } +} + +func TestGlobalFlagsKeepTopLevelValues(t *testing.T) { + c := &cli{stderr: io.Discard, allowUnlocked: true, timeout: time.Second} + top := c.flags("kmsctl") + if err := top.Parse([]string{"--allow-unlocked-memory=false", "--core", "/a", "init", "--id", "x"}); err != nil { + t.Fatal(err) + } + sub := c.flags("init") + sub.String("id", "", "") + if err := sub.Parse(top.Args()[1:]); err != nil { + t.Fatal(err) + } + if c.allowUnlocked || c.core != "/a" || c.timeout != time.Second { + t.Fatalf("top level values reset: %v %q %v", c.allowUnlocked, c.core, c.timeout) + } + sub = c.flags("init") + sub.String("id", "", "") + if err := sub.Parse([]string{"--id", "x", "--core", "/b", "--dir", "/d", "--timeout", "3s"}); err != nil { + t.Fatal(err) + } + if c.allowUnlocked || c.core != "/b" || c.dir != "/d" || c.timeout != 3*time.Second { + t.Fatalf("command level values: %v %q %q %v", c.allowUnlocked, c.core, c.dir, c.timeout) + } +} + func TestEndToEndExchange(t *testing.T) { bin := os.Getenv("KMS_CORE_BIN") if bin == "" { @@ -134,4 +195,20 @@ func TestEndToEndExchange(t *testing.T) { if out := must(nil, "--dir", bob, "audit", "verify"); !strings.HasPrefix(out, "verified\t") { t.Fatalf("audit verify %q", out) } + abs, err := filepath.Abs(bin) + if err != nil { + t.Fatal(err) + } + wd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + rel, err := filepath.Rel(wd, abs) + if err != nil { + t.Fatal(err) + } + var out, errb syncBuffer + if code := run([]string{"audit", "verify", "--dir", bob, "--core", rel, "--allow-unlocked-memory"}, nil, &out, &errb); code != 0 || !strings.HasPrefix(out.String(), "verified\t") { + t.Fatalf("global flags after command with relative core: %d %s%s", code, out.String(), errb.String()) + } } From a8b2c915d4d869e9f72d00ca63db5b97b177b00d Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:36:53 +0900 Subject: [PATCH 32/48] =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20=EB=B0=8F=20=EC=88=98=EC=A0=95=20-=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EB=B2=95=20=EC=A0=95=EB=A6=AC=ED=95=9C=20USAGE=20=ED=95=9C/?= =?UTF-8?q?=EC=98=81=20=EB=AC=B8=EC=84=9C=20=EC=B6=94=EA=B0=80=20-=20USAGE?= =?UTF-8?q?=20=EB=AC=B8=EC=84=9C=20=EB=A7=81=ED=81=AC=EB=90=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20README=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 20 +-- README_KR.md | 20 +-- USAGE.md | 358 +++++++++++++++++++++++++++++++++++++++++++++++++++ USAGE_KR.md | 251 ++++++++++++++++++++---------------- 4 files changed, 499 insertions(+), 150 deletions(-) create mode 100644 USAGE.md diff --git a/README.md b/README.md index 6cd7e12..3ca29f5 100644 --- a/README.md +++ b/README.md @@ -37,22 +37,6 @@ The following features are also included. - Strict protocol: fixed 16-byte header, length-prefixed fields, 1 MiB body limit, and rejection of trailing bytes. `HELLO` must come first, and any framing violation terminates the session. - Air-gapped build: Rust dependencies are vendored in `core/vendor` and built with `--frozen`. Go builds with `GOPROXY=off` and `-mod=vendor` and has no external dependencies. -## Build +## Build and run -Requirements are Rust `1.95.0` (pinned by `rust-toolchain.toml`) and Go `1.27.2`. A FIPS build additionally needs `cmake` and `perl`, and is statically linked only on Linux x86_64/aarch64. - -```bash -$ make build # core (release, with pqc) + host -$ make build-core FIPS=1 -$ make test # Rust tests with all features + Go tests (using the real core binary) -$ make lint -``` - -## Running the core - -```bash -$ kms-core --generate-root-key /path/root.key -$ kms-core --socket /run/kms/core.sock --root-key-file /path/root.key --pqc -``` - -When the host launches the core directly, it uses the inherited socket via `--fd 3`. On platforms without `mlockall`, such as macOS, `--allow-unlocked-memory` is required. +For details, see [USAGE.md](USAGE.md). diff --git a/README_KR.md b/README_KR.md index d800953..60514a5 100644 --- a/README_KR.md +++ b/README_KR.md @@ -37,22 +37,6 @@ - 엄격한 프로토콜: 고정 16바이트 헤더와 길이 접두 필드, 1MiB 본문 상한, 후행 바이트를 거부합니다. `HELLO` 선행을 강제하며 프레이밍(framing) 위반 시 세션을 끊어냅니다. - 폐쇄 빌드: Rust 의존성은 `core/vendor`에 벤더링되고 `--frozen`으로 빌드합니다. Go는 `GOPROXY=off`, `-mod=vendor`로 빌드하며 외부 의존성이 없습니다. -## 빌드 +## 빌드 및 실행 -요구 사항은 Rust `1.95.0`(`rust-toolchain.toml`로 고정)과 Go `1.27.2`입니다. FIPS 빌드는 `cmake`, `perl`이 추가로 필요하며 Linux x86_64/aarch64에서만 정적 링크됩니다. - -```bash -$ make build # 코어(release, pqc 포함) + 호스트 -$ make build-core FIPS=1 -$ make test # Rust 전체 feature 테스트 + Go 테스트(실제 코어 바이너리 사용) -$ make lint -``` - -## 코어 실행 - -```bash -$ kms-core --generate-root-key /path/root.key -$ kms-core --socket /run/kms/core.sock --root-key-file /path/root.key --pqc -``` - -호스트가 직접 기동할 때는 `--fd 3`으로 상속 소켓을 사용합니다. macOS처럼 `mlockall`이 없는 환경에서는 `--allow-unlocked-memory`가 필요합니다. +이 내용에 관해서는 [USAGE.md](USAGE_KR.md)를 확인할 수 있습니다. diff --git a/USAGE.md b/USAGE.md new file mode 100644 index 0000000..3227ab7 --- /dev/null +++ b/USAGE.md @@ -0,0 +1,358 @@ +# pairier usage + +
+ +[한국어](USAGE_KR.md) | English + +
+ +pairier is a KMS for securely generating and exchanging symmetric keys between members. Each member keeps their own node directory, and key material exists in plaintext only inside each member's own security core (`kms-core`). To send a key to another member, the sender seals it to the recipient's KEM public key and delivers it in an envelope signed with the sender's identity key. + +> [!WARNING] +> pairier has not been released yet. Even after development is complete, verifying the safety of its features may take additional time. Please use pairier for development or research purposes only. + +## Components + +| Binary | Role | +|------------|--------------------------------------------------------------------------------------------------------------------------------------| +| `kms-core` | The Rust security core. It owns the root key and all cryptographic operations, and the host launches it as a child process. | +| `kmsctl` | The command-line tool members use. It initializes nodes, registers members, generates and exchanges keys, and encrypts and decrypts. | +| `kmsd` | A daemon that receives envelopes from other members over mTLS. It is needed only for online delivery. | + +Every time `kmsctl` and `kmsd` run, they start `kms-core` as a child process and talk to it only over a single inherited socket, so **no socket is exposed on the filesystem.** + +## Build + +Requirements: + +- Rust `1.95.0` (pinned by `core/rust-toolchain.toml`), Go `1.27.2` +- FIPS core build: `cmake`, `perl`, Go (static linking is supported only on Linux x86_64/aarch64) +- Distribution build: GNU tar (`brew install gnu-tar` on macOS), `python3` + +All builds use only vendored dependencies and work without network access. + +```bash +$ make build # core (release, with pqc) + kmsd, kmsctl +$ make build-core FIPS=1 # build the core with the AWS-LC FIPS 140-3 module (Linux) +$ make test # Rust tests with all features + Go race tests (using the real core) +$ make lint +$ make audit # check vulnerabilities, licenses and sources with cargo-deny (local DB) +$ make dist FIPS=1 # write a reproducible tarball, SBOM and SHA256SUMS to dist/ +``` + +Go binaries are built with the Go Cryptographic Module (`GOFIPS140=certified`) by default. To turn it off, run `GOFIPS140=off make build-host`. The current mode appears in the **version output**. + +```bash +$ kms-core --version +kms-core 1.0.0 (kms-engine 1.0.0, fips=true, pqc=true) + +$ kmsctl version +kmsctl 1.0.0 (go go1.27.2, go_fips=true) +``` + +## Installation + +The distribution tarball contains `kms-core`, `kmsd`, `kmsctl`, `VERSION` and `sbom.cdx.json`. Verify the checksums before extracting it. + +```bash +$ sha256sum -c SHA256SUMS +$ tar -xzf pairier-1.0.0-linux-amd64.tar.gz +``` + +> [!TIP] +> `kmsctl` and `kmsd` use the `kms-core` in their own directory by default. If it is located elsewhere, you can set it with the `--core` flag or the `KMS_CORE` environment variable. The node directory can be set with the `--dir` flag or the `KMS_DIR` environment variable. + +## Quick start + +The following example walks through `alice` creating a key and handing `bob` decrypt permission only. Run each command locally on the corresponding member's machine. + +### 1. Initialize a node + +```bash +alice$ export KMS_CORE=$PWD/core/target/release/kms-core +alice$ kmsctl --dir ~/.pairier init --id alice --pqc +member alice +identity ml-dsa-65 +kem hybrid-p256-ml-kem-768 +core_fips false +go_fips true +pqc true +fingerprint 664e-c20d-cced-8ac3-5422-d278-d9ad-107b-d225-0aea-1e45-ff50-cce1-c28e-0319-ed0f +``` + +Initialization creates the root key, the identity signing key, the KEM key, the audit signing key and the TLS key. With the `--pqc` flag, the node uses an ML-DSA-65 identity key and a P-256 + ML-KEM-768 hybrid KEM. Without it, the node uses ECDSA P-256 and DHKEM(P-256). + +> [!TIP] +> All members of a group should use the same mode. A pqc node can register regular cards, but a node initialized without pqc cannot verify pqc cards and rejects them. + +### 2. Exchange identity cards and verify fingerprints + +```bash +alice$ kmsctl --dir ~/.pairier card --out alice.card +member alice +fingerprint 664e-c20d-...-ed0f +``` + +The card file can be delivered by any route, such as email or USB. However, **the fingerprint must be verified over a channel separate from the card** (in person, by phone, through a signed message, and so on). The fingerprint is the sole basis of trust, and using a CA or TOFU (trust on first use) is not recommended. + +### 3. Register members + +```bash +alice$ kmsctl --dir ~/.pairier member add --card bob.card --fingerprint b567-baee-...-3d65 +added bob b567-baee-...-3d65 + +bob$ kmsctl --dir ~/.pairier member add --card alice.card --fingerprint 664e-c20d-...-ed0f +``` + +Fingerprint comparison ignores case, whitespace, `-` and `:`. Registration is rejected if the card's self-signature does not verify or the fingerprint differs. Both sides must register each other before they can exchange keys. + +### 4. Generate a key + +```bash +alice$ kmsctl --dir ~/.pairier key gen --label project/db +af8477cc1076f219bfb047d846629eb8 aes-256-gcm encrypt,decrypt,export project/db +``` + +The generated key is an AES-256-GCM symmetric key. If the `--usage` flag is omitted, the default `encrypt,decrypt,export` applies. A key that will be sent to another member must include `export`. + +### 5-a. Offline delivery (file) + +```bash +alice$ kmsctl --dir ~/.pairier key send --id af8477cc1076f219bfb047d846629eb8 --to bob --out db.kxp +sent af8477cc1076f219bfb047d846629eb8 to bob usage decrypt + +bob$ kmsctl --dir ~/.pairier key receive --in db.kxp +af8477cc1076f219bfb047d846629eb8 aes-256-gcm decrypt project/db from alice +``` + +`--usage` sets the permissions granted to the recipient and defaults to `decrypt`. Only a subset of the original key's usage can be granted. The envelope opens only for `bob`, and `bob` imports the key only after verifying the signature, the recipient and the context. The same key cannot be imported twice. + +### 5-b. Online delivery (kmsd) + +If the recipient keeps `kmsd` running, the sender can push the envelope directly. + +```bash +bob$ kmsd --dir ~/.pairier --listen 0.0.0.0:7443 +kmsd 1.0.0 member bob listening on 0.0.0.0:7443 core_fips=false go_fips=true pqc=true + +alice$ kmsctl --dir ~/.pairier peer health --member bob --url https://bob.example:7443 +ok bob +alice$ kmsctl --dir ~/.pairier key push --id af8477cc... --to bob --url https://bob.example:7443 +pushed af8477cc... to bob envelope 854a37b90d476c1070be8c5449403071 +``` + +A received envelope does not become a key right away. It is held in the inbox, and the recipient reviews it and then accepts or rejects it. + +```bash +bob$ kmsctl --dir ~/.pairier inbox list +854a37b90d476c1070be8c5449403071 alice 2026-10-14T09:42:00Z 2026-10-14T09:42:00Z +bob$ kmsctl --dir ~/.pairier inbox accept --id 854a37b90d476c1070be8c5449403071 +bob$ kmsctl --dir ~/.pairier inbox reject --id +``` + +The connection uses TLS 1.3 mutual authentication, and each side checks that the peer matches the TLS public key in the peer's card. Unregistered members are rejected during the TLS handshake. + +### 6. Encrypt and decrypt + +```bash +alice$ printf 'db password' | kmsctl --dir ~/.pairier encrypt --id af8477cc... --aad project/db --out secret.bin +bob$ kmsctl --dir ~/.pairier decrypt --id af8477cc... --aad project/db --in secret.bin +db password +``` + +The ciphertext is binary and exactly 48 bytes longer than the plaintext (salt 32 + tag 16). The `--aad` value must be identical for encryption and decryption. Because `bob` was granted only `decrypt`, an `encrypt` attempt is rejected with `usage denied`. + +### 7. Verify the audit log + +```bash +bob$ kmsctl --dir ~/.pairier audit verify +verified 13 entries +``` + +## kmsctl command reference + +### Global options + +| Option | Default | Description | +|---------------------------|--------------------------------------------------|------------------------------------| +| `--dir DIR` | `$KMS_DIR` | Node directory | +| `--core PATH` | `$KMS_CORE`, otherwise `kms-core` next to kmsctl | Core binary | +| `--allow-unlocked-memory` | `true` on macOS, `false` elsewhere | Run the core without locked memory | +| `--timeout DUR` | `10s` | Core call timeout | + +Global options must come before the command, for example `kmsctl --dir X key list`. + +### Commands + +| Command | Description | +|---------------------------------------------------------------|---------------------------------------------------------------------------| +| `init --id MEMBER [--pqc]` | Creates a node in an empty directory. | +| `card [--out FILE]` | Prints the self-signed identity card. The fingerprint is shown on stderr. | +| `member add --card FILE --fingerprint FP` | Registers a member after checking the fingerprint. | +| `member list` | Shows registered members with their algorithms and fingerprints. | +| `key gen --label LABEL [--usage LIST]` | Generates a symmetric key. | +| `key list` | Lists keys. The last column is `local` or `from `. | +| `key info --id KEYID` | Shows key details and origin. | +| `key send --id KEYID --to MEMBER [--usage LIST] [--out FILE]` | Creates an envelope file. | +| `key receive [--in FILE]` | Verifies an envelope and imports the key. | +| `key push --id KEYID --to MEMBER --url URL [--usage LIST]` | Sends an envelope to the recipient's kmsd. | +| `inbox list`, `inbox accept --id ID`, `inbox reject --id ID` | Handles envelopes received by kmsd. | +| `peer health --member MEMBER --url URL` | Checks that mutual authentication with the peer's kmsd works. | +| `encrypt --id KEYID [--in FILE] [--out FILE] [--aad STR]` | Encrypts. Reads stdin when no input is given. | +| `decrypt --id KEYID [--in FILE] [--out FILE] [--aad STR]` | Decrypts. | +| `audit verify` | Verifies the audit log chain and every checkpoint signature. | +| `version` | Prints the version and the Go FIPS status. | + +If the file given with `--out` already exists, the command fails instead of overwriting it, and new files are created with `0600` permissions. Exit codes are as follows. + +- Success `0` +- Execution failure `1` +- Argument error `2` + +## Key usage + +| Usage | Meaning | +|---------------------------------|-----------------------------------------------------| +| `encrypt`, `decrypt` | AEAD encryption and decryption | +| `export` | Can be sent to other members | +| `wrap`, `unwrap`, `derive` | Reserved symmetric key usages | +| `sign`, `verify`, `decapsulate` | Identity, audit and KEM keys only (used internally) | + +- `key gen` accepts only symmetric key usages (`encrypt`, `decrypt`, `wrap`, `unwrap`, `derive`, `export`). +- The usage granted when sending must be a subset of the original key's usage. The core rejects privilege escalation. +- Granting `export` to a recipient lets them send the key on to other members. To prevent re-sharing, do not grant `export`. + +## Operating kmsd + +### Flags and configuration file + +| Flag | Config field | Default | +|---------------------------|-------------------------|------------------------------------| +| `--dir` | `dir` | None (required) | +| `--core` | `core` | `kms-core` next to kmsd | +| `--listen` | `listen` | `127.0.0.1:7443` | +| `--allow-unlocked-memory` | `allow_unlocked_memory` | `true` on macOS, `false` elsewhere | +| `--timeout` | `timeout` | `10s` | +| `--config FILE` | | | +| `--version` | | | + +```json +{ + "dir": "/var/lib/pairier/bob", + "core": "/opt/pairier/kms-core", + "listen": "0.0.0.0:7443", + "allow_unlocked_memory": false, + "timeout": "10s" +} +``` + +- Precedence is explicit flags -> configuration file -> defaults. +- The configuration file must be a regular file that the group and other users cannot access. kmsd refuses to start if the file contains unknown fields. +- The `listen` host must be an IP address. +- `kmsd` opens only nodes already initialized with `kmsctl init`. +- On `SIGINT` or `SIGTERM`, kmsd finishes in-flight requests and exits. +- `kmsctl` can be used on the same node while `kmsd` is running. Each starts its own core, and the store and audit log are safe for concurrent access. + +### HTTP API + +Every endpoint requires a client certificate of a registered member. + +| Method and path | Description | +|------------------|----------------------------------------------------------------------------------------------------| +| `GET /v1/health` | Member id, FIPS and PQC status | +| `GET /v1/card` | This node's identity card | +| `POST /v1/inbox` | Submits an envelope (the body is the envelope bytes). Returns `201` and the envelope id on success | + +An envelope is rejected with `400` if its sender differs from the authenticated peer, its recipient is not this node, or its signature does not verify. Submitting the same envelope again returns `409`. + +### systemd example + +```ini +[Service] +User=pairier +ExecStart=/opt/pairier/kmsd --config /etc/pairier/kmsd.json +LimitMEMLOCK=infinity +LimitCORE=0 +NoNewPrivileges=yes +ProtectSystem=strict +ReadWritePaths=/var/lib/pairier +PrivateTmp=yes +``` + +On Linux, the core locks all of its memory with the `mlockall` system call. If the memlock limit is too small the core refuses to start, so `LimitMEMLOCK=infinity` is required. + +## Running kms-core standalone + +Normally `kmsctl` and `kmsd` launch the core themselves. To run the core separately under its own user, use socket path mode as follows. + +```bash +$ kms-core --generate-root-key /path/root.key +$ kms-core --socket /run/pairier/core.sock --root-key-file /path/root.key --pqc --peer-uid 1001 +``` + +| Option | Description | +|---------------------------|----------------------------------------------------------------------------------------------------------------------------------------------| +| `--fd N` | Use an inherited socket (`3` when the host launches the core) | +| `--socket PATH` | Accept a single connection on the socket path, then remove the path | +| `--root-key-file PATH` | Accepts only a 32-byte regular file owned by the current user, with no group or other permissions, no symbolic links and no extra hard links | +| `--ephemeral-root` | Temporary root key held only in memory (blobs cannot be opened after a restart) | +| `--pqc` | Enable PQC algorithms (when built with the pqc feature) | +| `--allow-unlocked-memory` | Continue where `mlockall` is unavailable | +| `--peer-uid UID` | Verify the uid of the connecting peer (defaults to the core's own uid) | + +> [!NOTE] +> Currently `kmsctl` and `kmsd` support only launching the core directly with the root key in the node directory. + +## Node directory + +```text +/ + root.key root key (0400) - the only secret that opens every key blob + node.json member id and the identity, KEM and audit key ids + keys/ key records - key material is stored only as blobs sealed under the root key + members/ registered member cards and fingerprints + inbox/ envelopes received by kmsd + tls.json TLS certificate and the TLS private key encrypted by the core + audit.log hash-chained audit log (each entry signed with the audit key) +``` + +- The directory must be `0700` and owned by the current user, and files must have no group or other permissions. Symbolic links are never opened. +- On every use, stored metadata and public keys are checked against values the core recomputes from the blob. +- Backups include `root.key`, so **backup media must be protected at the same level as the root key**. + +## Security operations notes + +- **Verify fingerprints over a separate channel.** If the card and the fingerprint arrive by the same route, a man-in-the-middle attack cannot be prevented. +- **Understand the limits of the audit log.** Each entry is signed with the core's audit key, so an attacker who cannot use the core (a stolen disk, a tampered backup, and so on) cannot forge or recompute the log. However, anyone who can start the core with the node user's permissions can produce valid signatures. Truncating the end of the log also cannot be detected without an external reference point. +- **kmsd keeps its TLS private key in memory.** The symmetric keys being exchanged and the identity, KEM and audit keys never leave the core, but the transport TLS key is an exception. +- **Use macOS for development only.** Without `mlockall`, the core runs without memory locking, and the FIPS core is not statically linked. Run production deployments on Linux. +- **The audit log grows quickly in PQC mode.** ML-DSA signatures make each entry about 7KB, and every encryption and decryption also adds an entry. + +> [!CAUTION] +> pairier has not been released yet. Even after development is complete, verifying the safety of its features may take additional time. Please use pairier for development or research purposes only. +> +> **The items above are provisional until a stable release is published. Security requirements may increase significantly as verification progresses.** + +## Limits + +| Item | Limit | +|------------------------------|--------------------------------------------------------------------------------------------------------------------------------| +| Member id | Up to 64 characters, starting with a lowercase letter or digit and containing only lowercase letters, digits, `.`, `_` and `-` | +| Key label | 256 bytes | +| Encryption plaintext, AAD | 64KiB each | +| kmsctl input files and stdin | 1MiB | +| Envelope context | 1KiB | + +## Troubleshooting + +| Message | Cause and fix | +|----------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------| +| `mlockall: Cannot allocate memory ... (pass --allow-unlocked-memory to proceed)` | The memlock limit is too small. Raise it with `ulimit -l unlimited` or `LimitMEMLOCK=infinity`. | +| `node: fingerprint does not match the card` | The fingerprint differs. Get the card again and re-verify the fingerprint over a separate channel. | +| `core: op 0x0023 failed: capability disabled` | A pqc card was registered on a node initialized without pqc. | +| `core: op 0x0030 failed: usage denied` | The key lacks `export`, or the requested usage is broader than the original. | +| `node: envelope is addressed to another member` | The envelope was sent to a different member. | +| `node: unknown member` | The sender or the recipient is not registered. | +| `store: object already exists` | The key was already imported or the envelope was already received. | +| `node: stored metadata does not match the core` | Records in the node directory have been tampered with. Check backups and the audit log. | +| `audit: checkpoint signature is invalid` | The audit log was modified without the core. | diff --git a/USAGE_KR.md b/USAGE_KR.md index 683c0b4..e9cfd9b 100644 --- a/USAGE_KR.md +++ b/USAGE_KR.md @@ -1,63 +1,75 @@ # pairier 사용법 -pairier는 구성원 간에 대칭 키를 안전하게 생성하고 교환하기 위한 KMS입니다. 구성원마다 자기 노드 디렉터리를 두고, 키 재료는 항상 각자의 보안 코어(`kms-core`) 안에서만 평문으로 존재합니다. 다른 구성원에게 키를 보낼 때는 수신자의 KEM 공개키로 봉인하고 송신자의 신원 키로 서명한 봉투(envelope)를 전달합니다. +
+ +한국어 | [English](USAGE.md) + +
+ +pairier는 구성원 간에 대칭 키를 안전하게 생성하고 교환하기 위한 KMS입니다. 구성원마다 자기 노드 디렉토리를 두고, 키 재료는 항상 각자의 보안 코어(`kms-core`) 안에서만 평문으로 존재합니다. 다른 구성원에게 키를 보낼 때는 수신자의 KEM 공개키로 봉인하고 송신자의 신원 키로 서명한 봉투(envelope)를 전달합니다. + +> [!WARNING] +> 현재 pairier는 릴리즈가 공개되지 않았으며, 그 이전에 개발이 완료되더라도 기능의 안전성 검증을 위해 시간이 소요될 수 있습니다. pairier는 개발 또는 연구용으로만 사용해주시길 바랍니다. ## 구성 요소 -| 바이너리 | 역할 | -| --- | --- | -| `kms-core` | Rust 보안 코어. 루트 키와 모든 암호 연산을 담당하며 호스트가 자식 프로세스로 기동합니다. | -| `kmsctl` | 구성원이 쓰는 명령행 도구. 노드 초기화, 구성원 등록, 키 생성과 교환, 암복호화를 수행합니다. | -| `kmsd` | 다른 구성원이 보낸 봉투를 mTLS로 받는 데몬. 온라인 전달을 쓸 때만 필요합니다. | +| 바이너리 | 역할 | +|------------|---------------------------------------------------------------| +| `kms-core` | Rust 보안 코어로, 루트 키와 모든 암호 연산을 담당하며 호스트가 자식 프로세스로 기동합니다. | +| `kmsctl` | 구성원이 쓰는 명령줄 도구입니다. 노드 초기화, 구성원 등록, 키 생성과 교환, 암/복호화를 수행합니다. | +| `kmsd` | 다른 구성원이 보낸 봉투를 mTLS로 받는 데몬(daemon)입니다. 온라인 전달을 사용할 때에만 필요합니다. | -`kmsctl`과 `kmsd`는 실행될 때마다 `kms-core`를 자식 프로세스로 띄우고, 상속한 소켓 하나로만 통신합니다. 파일 시스템에 소켓이 노출되지 않습니다. +`kmsctl`과 `kmsd`는 실행될 때마다 `kms-core`를 자식 프로세스로 띄우고 상속한 소켓 하나로만 통신하며, **파일 시스템에 소켓이 노출되지 않습니다.** ## 빌드 요구 사항은 다음과 같습니다. - Rust `1.95.0` (`core/rust-toolchain.toml`로 고정), Go `1.27.2` -- FIPS 코어 빌드: `cmake`, `perl`, Go. 정적 링크는 Linux x86_64/aarch64에서만 지원 -- 배포본 생성: GNU tar (macOS는 `brew install gnu-tar`), `python3` +- FIPS 코어 빌드: `cmake`, `perl`, Go (정적 링크는 Linux x86_64/aarch64에서만 지원) +- 배포본 생성: GNU tar (macOS의 경우 `brew install gnu-tar`), `python3` 모든 빌드는 벤더링된 의존성만 사용하며 네트워크 없이 동작합니다. ```bash -$ make build # 코어(release, pqc 포함) + kmsd, kmsctl -$ make build-core FIPS=1 # AWS-LC FIPS 140-3 모듈로 코어 빌드 (Linux) -$ make test # Rust 전체 피처 테스트 + Go race 테스트 (실제 코어 사용) +$ make build # 코어(release, pqc 포함) + kmsd, kmsctl +$ make build-core FIPS=1 # AWS-LC FIPS 140-3 모듈로 코어 빌드 (Linux) +$ make test # Rust 전체 feature 테스트 + Go race 테스트 (실제 코어 사용) $ make lint -$ make audit # cargo-deny로 취약점, 라이선스, 소스 점검 (로컬 DB 사용) -$ make dist FIPS=1 # 재현 가능한 배포 tarball, SBOM, SHA256SUMS를 dist/에 생성 +$ make audit # cargo-deny로 취약점, 라이선스, 소스 점검 (로컬 DB 사용) +$ make dist FIPS=1 # 재현 가능한 배포 tarball, SBOM, SHA256SUMS를 dist/에 생성 ``` -Go 바이너리는 기본적으로 Go 인증 암호 모듈(`GOFIPS140=certified`)로 빌드됩니다. 해제하려면 `GOFIPS140=off make build-host`를 사용합니다. 현재 모드는 버전 출력에서 확인할 수 있습니다. +Go 바이너리는 기본적으로 Go 인증 암호 모듈(`GOFIPS140=certified`)로 빌드됩니다. 해제하려면 `GOFIPS140=off make build-host`를 입력하세요. 현재 모드는 **버전 출력**에서 확인할 수 있습니다. ```bash $ kms-core --version kms-core 1.0.0 (kms-engine 1.0.0, fips=true, pqc=true) + $ kmsctl version kmsctl 1.0.0 (go go1.27.2, go_fips=true) ``` ## 설치 -배포 tarball에는 `kms-core`, `kmsd`, `kmsctl`, `VERSION`, `sbom.cdx.json`이 들어 있습니다. 풀기 전에 체크섬을 확인합니다. +배포 tarball에는 `kms-core`, `kmsd`, `kmsctl`, `VERSION`, `sbom.cdx.json`이 들어 있습니다. 해제 전 체크섬(checksum)을 확인하세요. ```bash $ sha256sum -c SHA256SUMS $ tar -xzf pairier-1.0.0-linux-amd64.tar.gz ``` -`kmsctl`과 `kmsd`는 같은 디렉터리에 있는 `kms-core`를 기본으로 사용합니다. 다른 위치에 두었다면 `--core` 플래그나 `KMS_CORE` 환경 변수로 지정합니다. 노드 디렉터리는 `--dir` 플래그나 `KMS_DIR` 환경 변수로 지정합니다. +> [!TIP] +> `kmsctl`과 `kmsd`는 같은 디렉토리에 있는 `kms-core`를 기본으로 사용합니다. 다른 위치에 둔 경우 `--core` 플래그 또는 `KMS_CORE` 환경 변수로 지정할 수 있습니다. 노드 디렉토리는 `--dir` 플래그 또는 `KMS_DIR` 환경 변수로 지정할 수 있습니다. ## 빠른 시작 -alice가 키를 만들어 bob에게 복호화 권한만 넘기는 흐름입니다. 각 명령은 해당 구성원의 컴퓨터에서 실행합니다. +다음은 예시로, `alice`가 키를 만들어 `bob`에게 복호화 권한만 넘기는 흐름을 따릅니다. 각 명령은 해당 구성원 로컬에서 실행합니다. ### 1. 노드 초기화 ```bash +alice$ export KMS_CORE=$PWD/core/target/release/kms-core alice$ kmsctl --dir ~/.pairier init --id alice --pqc member alice identity ml-dsa-65 @@ -68,9 +80,10 @@ pqc true fingerprint 664e-c20d-cced-8ac3-5422-d278-d9ad-107b-d225-0aea-1e45-ff50-cce1-c28e-0319-ed0f ``` -초기화는 루트 키, 신원 서명 키, KEM 키, 감사 서명 키, TLS 키를 만듭니다. `--pqc`를 주면 ML-DSA-65 신원 키와 P-256 + ML-KEM-768 하이브리드 KEM을, 생략하면 ECDSA P-256과 DHKEM(P-256)을 사용합니다. +초기화는 루트 키, 신원 서명 키, KEM 키, 감사 서명 키, TLS 키를 만들어냅니다. `--pqc` 플래그를 사용하면 ML-DSA-65 신원 키와 P-256 + ML-KEM-768 하이브리드 KEM을, 생략하면 ECDSA P-256과 DHKEM(P-256)을 사용합니다. -> 한 그룹 안에서는 모든 구성원이 같은 모드를 쓰는 것을 권장합니다. pqc 노드는 일반 카드를 등록할 수 있지만, pqc 없이 초기화한 노드는 pqc 카드를 검증할 수 없어 등록을 거부합니다. +> [!TIP] +> 한 그룹 안에서는 모든 구성원이 같은 모드를 사용하는 것을 권장합니다. pqc 노드는 일반 카드를 등록할 수 있지만, pqc 없이 초기화한 노드는 pqc 카드를 검증할 수 없어 등록을 거부합니다. ### 2. 신원 카드 교환과 지문 확인 @@ -80,7 +93,7 @@ member alice fingerprint 664e-c20d-...-ed0f ``` -카드 파일은 이메일이나 USB 등 어떤 경로로 전달해도 됩니다. 단, **지문은 반드시 카드와 다른 채널**(대면, 전화, 서명된 메시지 등)로 확인해야 합니다. 지문이 신뢰의 유일한 근거이며, CA나 TOFU(최초 접속 신뢰)는 쓰지 않습니다. +카드 파일은 이메일이나 USB 등 어떤 경로로 전달해도 됩니다. 단, **지문(fingerprint)은 반드시 카드와 다른 채널**(대면, 전화, 서명된 메시지 등)로 확인해야 합니다. 지문이 신뢰의 유일한 근거이며, CA나 TOFU(최초 접속 신뢰)는 사용하지 않는 것을 권장합니다. ### 3. 구성원 등록 @@ -91,7 +104,7 @@ added bob b567-baee-...-3d65 bob$ kmsctl --dir ~/.pairier member add --card alice.card --fingerprint 664e-c20d-...-ed0f ``` -지문 비교는 대소문자, 공백, `-`, `:`를 무시합니다. 카드의 자기 서명이 맞지 않거나 지문이 다르면 등록이 거부됩니다. 키를 주고받으려면 양쪽 모두 상대를 등록해야 합니다. +지문 비교는 대소문자, 공백, `-`, `:`를 무시합니다. 카드의 자기 서명이 일치하지 않거나 지문이 다르면 등록이 거부됩니다. 키를 주고받기 위해선 양측 모두 상대방을 등록해야 합니다. ### 4. 키 생성 @@ -100,7 +113,7 @@ alice$ kmsctl --dir ~/.pairier key gen --label project/db af8477cc1076f219bfb047d846629eb8 aes-256-gcm encrypt,decrypt,export project/db ``` -생성되는 키는 AES-256-GCM 대칭 키입니다. `--usage`를 생략하면 `encrypt,decrypt,export`입니다. 다른 구성원에게 보낼 키에는 반드시 `export`가 있어야 합니다. +생성되는 키는 AES-256-GCM 대칭 키입니다. `--usage` 플래그를 생략하면 기본 갑으로 `encrypt,decrypt,export`가 적용됩니다. 다른 구성원에게 보낼 키에는 반드시 `export`가 포함되어 있어야 합니다. ### 5-a. 오프라인 전달 (파일) @@ -112,11 +125,11 @@ bob$ kmsctl --dir ~/.pairier key receive --in db.kxp af8477cc1076f219bfb047d846629eb8 aes-256-gcm decrypt project/db from alice ``` -`--usage`로 수신자에게 줄 권한을 정하며 기본값은 `decrypt`입니다. 원래 키가 가진 용도의 부분집합만 줄 수 있습니다. 봉투는 bob에게만 열리고, bob 쪽에서는 서명, 수신자, 컨텍스트를 모두 검증한 뒤에만 가져옵니다. 같은 키를 두 번 가져올 수는 없습니다. +`--usage`로 수신자에게 줄 권한을 정할 수 있으며, 기본값은 `decrypt`입니다. 원래 키가 가진 용도의 부분집합만 정할 수 있습니다. 해당 봉투는 `bob`에게만 열리고, `bob` 쪽에서는 서명, 수신자, 컨텍스트를 모두 검증한 뒤에만 가져옵니다. 같은 키를 두 번 가져올 수는 없습니다. ### 5-b. 온라인 전달 (kmsd) -수신자가 `kmsd`를 실행해 두면 송신자가 직접 밀어 넣을 수 있습니다. +수신자가 `kmsd`를 실행해 둔다면 송신자가 직접 밀어(push) 넣을 수 있습니다. ```bash bob$ kmsd --dir ~/.pairier --listen 0.0.0.0:7443 @@ -128,7 +141,7 @@ alice$ kmsctl --dir ~/.pairier key push --id af8477cc... --to bob --url https:// pushed af8477cc... to bob envelope 854a37b90d476c1070be8c5449403071 ``` -받은 봉투는 바로 키가 되지 않고 inbox에 보관됩니다. 수신자가 확인 후 수락하거나 거절합니다. +받은 봉투는 바로 키가 되지 않고 inbox에 보관됩니다. 수신자가 확인 후 수락하거나 거절할 수 있습니다. ```bash bob$ kmsctl --dir ~/.pairier inbox list @@ -139,7 +152,7 @@ bob$ kmsctl --dir ~/.pairier inbox reject --id 연결은 TLS 1.3 상호 인증이며, 양쪽 모두 상대 카드에 들어 있는 TLS 공개키와 일치하는지 확인합니다. 등록되지 않은 구성원은 TLS 단계에서 거부됩니다. -### 6. 암복호화 +### 6. 암/복호화 ```bash alice$ printf 'db password' | kmsctl --dir ~/.pairier encrypt --id af8477cc... --aad project/db --out secret.bin @@ -147,7 +160,7 @@ bob$ kmsctl --dir ~/.pairier decrypt --id af8477cc... --aad project/db --in secr db password ``` -암호문은 바이너리이며 평문보다 48바이트 깁니다(salt 32 + 태그 16). `--aad`는 암호화와 복호화에서 같아야 합니다. bob은 `decrypt` 권한만 받았으므로 `encrypt`를 시도하면 `usage denied`로 거부됩니다. +암호문은 바이너리이며 평문보다 정확히 48바이트 깁니다(salt 32 + tag 16). `--aad` 플래그는 암호화와 복호화 과정에서 반드시 같아야 합니다. `bob`은 `decrypt` 권한만 받았기 때문에 `encrypt`를 시도하는 경우 `usage denied`로 거부됩니다. ### 7. 감사 로그 검증 @@ -160,64 +173,68 @@ verified 13 entries ### 전역 옵션 -| 옵션 | 기본값 | 설명 | -| --- | --- | --- | -| `--dir DIR` | `$KMS_DIR` | 노드 디렉터리 | -| `--core PATH` | `$KMS_CORE`, 없으면 kmsctl 옆의 `kms-core` | 코어 바이너리 | -| `--allow-unlocked-memory` | macOS `true`, 그 외 `false` | 메모리 잠금 없이 코어 실행 | -| `--timeout DUR` | `10s` | 코어 호출 제한 시간 | +| 옵션 | 기본값 | 설명 | +|---------------------------|---------------------------------------|-----------------| +| `--dir DIR` | `$KMS_DIR` | 노드 디렉토리 | +| `--core PATH` | `$KMS_CORE`, 없으면 kmsctl 옆의 `kms-core` | 코어 바이너리 | +| `--allow-unlocked-memory` | macOS `true`, 그 외 `false` | 메모리 잠금 없이 코어 실행 | +| `--timeout DUR` | `10s` | 코어 호출 제한 시간 | -전역 옵션은 명령보다 앞에 둡니다. 예를 들어 `kmsctl --dir X key list`처럼 씁니다. +전역 옵션은 명령보다 앞에 두어야 합니다. 예를 들어 `kmsctl --dir X key list`처럼 사용할 수 있습니다. ### 명령 -| 명령 | 설명 | -| --- | --- | -| `init --id MEMBER [--pqc]` | 빈 디렉터리에 노드를 만듭니다. | -| `card [--out FILE]` | 자기 서명된 신원 카드를 출력합니다. 지문은 stderr에 표시됩니다. | -| `member add --card FILE --fingerprint FP` | 지문을 확인한 뒤 구성원을 등록합니다. | -| `member list` | 등록된 구성원과 알고리즘, 지문을 표시합니다. | -| `key gen --label LABEL [--usage LIST]` | 대칭 키를 생성합니다. | -| `key list` | 키 목록을 표시합니다. 마지막 열은 `local` 또는 `from <송신자>`입니다. | -| `key info --id KEYID` | 키 상세와 출처를 표시합니다. | -| `key send --id KEYID --to MEMBER [--usage LIST] [--out FILE]` | 봉투 파일을 만듭니다. | -| `key receive [--in FILE]` | 봉투를 검증하고 키를 가져옵니다. | -| `key push --id KEYID --to MEMBER --url URL [--usage LIST]` | 수신자의 kmsd로 봉투를 보냅니다. | -| `inbox list`, `inbox accept --id ID`, `inbox reject --id ID` | kmsd가 받은 봉투를 처리합니다. | -| `peer health --member MEMBER --url URL` | 상대 kmsd와 상호 인증이 되는지 확인합니다. | -| `encrypt --id KEYID [--in FILE] [--out FILE] [--aad STR]` | 암호화합니다. 입력이 없으면 stdin을 씁니다. | -| `decrypt --id KEYID [--in FILE] [--out FILE] [--aad STR]` | 복호화합니다. | -| `audit verify` | 감사 로그 체인과 모든 체크포인트 서명을 검증합니다. | -| `version` | 버전과 Go FIPS 상태를 출력합니다. | - -`--out`으로 지정한 파일이 이미 있으면 덮어쓰지 않고 실패하며, 새 파일은 `0600` 권한으로 만들어집니다. 종료 코드는 성공 `0`, 실행 실패 `1`, 인자 오류 `2`입니다. +| 명령 | 설명 | +|---------------------------------------------------------------|-------------------------------------------------| +| `init --id MEMBER [--pqc]` | 빈 디렉토리에 노드를 만듭니다. | +| `card [--out FILE]` | 자기 서명된 신원 카드를 출력합니다. 지문은 stderr에 표시됩니다. | +| `member add --card FILE --fingerprint FP` | 지문을 확인한 뒤 구성원을 등록합니다. | +| `member list` | 등록된 구성원과 알고리즘, 지문을 표시합니다. | +| `key gen --label LABEL [--usage LIST]` | 대칭 키를 생성합니다. | +| `key list` | 키 목록을 표시합니다. 마지막 열은 `local` 또는 `from <송신자>`입니다. | +| `key info --id KEYID` | 키 상세와 출처를 표시합니다. | +| `key send --id KEYID --to MEMBER [--usage LIST] [--out FILE]` | 봉투 파일을 만듭니다. | +| `key receive [--in FILE]` | 봉투를 검증하고 키를 가져옵니다. | +| `key push --id KEYID --to MEMBER --url URL [--usage LIST]` | 수신자의 kmsd로 봉투를 보냅니다. | +| `inbox list`, `inbox accept --id ID`, `inbox reject --id ID` | kmsd가 받은 봉투를 처리합니다. | +| `peer health --member MEMBER --url URL` | 상대 kmsd와 상호 인증이 되는지 확인합니다. | +| `encrypt --id KEYID [--in FILE] [--out FILE] [--aad STR]` | 암호화합니다. 입력이 없으면 stdin을 씁니다. | +| `decrypt --id KEYID [--in FILE] [--out FILE] [--aad STR]` | 복호화합니다. | +| `audit verify` | 감사 로그 체인과 모든 체크포인트 서명을 검증합니다. | +| `version` | 버전과 Go FIPS 상태를 출력합니다. | + +`--out`으로 지정한 파일이 이미 있으면 덮어쓰지 않고 실패하며, 새 파일은 `0600` 권한으로 만들어집니다. 종료 코드는 다음과 같습니다. + +- 성공 `0` +- 실행 실패 `1` +- 인자 오류 `2` ## 키 용도 -| 용도 | 의미 | -| --- | --- | -| `encrypt`, `decrypt` | AEAD 암호화, 복호화 | -| `export` | 다른 구성원에게 보낼 수 있음 | -| `wrap`, `unwrap`, `derive` | 예약된 대칭 키 용도 | +| 용도 | 의미 | +|---------------------------------|----------------------------| +| `encrypt`, `decrypt` | AEAD 암호화, 복호화 | +| `export` | 다른 구성원에게 보낼 수 있음 | +| `wrap`, `unwrap`, `derive` | 예약된 대칭 키 용도 | | `sign`, `verify`, `decapsulate` | 신원, 감사, KEM 키 전용 (내부에서 사용) | - `key gen`으로는 대칭 키 용도(`encrypt`, `decrypt`, `wrap`, `unwrap`, `derive`, `export`)만 지정할 수 있습니다. - 보낼 때 주는 용도는 원래 키 용도의 부분집합이어야 합니다. 권한 상승은 코어가 거부합니다. -- 수신자에게 `export`를 주면 수신자가 그 키를 다시 다른 구성원에게 보낼 수 있습니다. 재공유를 막으려면 `export`를 주지 마십시오. +- 수신자에게 `export`를 주면 수신자가 그 키를 다시 다른 구성원에게 보낼 수 있습니다. 재공유를 막으려면 `export`를 설정하지 마세요. ## kmsd 운영 ### 플래그와 설정 파일 -| 플래그 | 설정 파일 필드 | 기본값 | -| --- | --- | --- | -| `--dir` | `dir` | 없음 (필수) | -| `--core` | `core` | kmsd 옆의 `kms-core` | -| `--listen` | `listen` | `127.0.0.1:7443` | +| 플래그 | 설정 파일 필드 | 기본값 | +|---------------------------|-------------------------|---------------------------| +| `--dir` | `dir` | 없음 (필수) | +| `--core` | `core` | kmsd 옆의 `kms-core` | +| `--listen` | `listen` | `127.0.0.1:7443` | | `--allow-unlocked-memory` | `allow_unlocked_memory` | macOS `true`, 그 외 `false` | -| `--timeout` | `timeout` | `10s` | -| `--config FILE` | | | -| `--version` | | | +| `--timeout` | `timeout` | `10s` | +| `--config FILE` | | | +| `--version` | | | ```json { @@ -230,7 +247,7 @@ verified 13 entries ``` - 우선순위는 명시한 플래그 -> 설정 파일 -> 기본값 순입니다. -- 설정 파일은 일반 파일이어야 하고 그룹이나 다른 사용자가 쓸 수 없어야 합니다. 모르는 필드가 있으면 기동하지 않습니다. +- 설정 파일은 일반 파일이어야 하며, 그룹 또는 다른 사용자가 사용할 수 없어야 합니다. 알 수 없는 필드가 있다면 기동하지 않습니다. - `listen`의 호스트는 IP 주소만 허용합니다. - `kmsd`는 `kmsctl init`으로 이미 초기화한 노드만 엽니다. - `SIGINT`나 `SIGTERM`을 받으면 진행 중인 요청을 마무리하고 종료합니다. @@ -240,13 +257,13 @@ verified 13 entries 모든 엔드포인트는 등록된 구성원의 클라이언트 인증서를 요구합니다. -| 메서드와 경로 | 설명 | -| --- | --- | -| `GET /v1/health` | 구성원 id, FIPS, PQC 상태 | -| `GET /v1/card` | 이 노드의 신원 카드 | +| 메서드와 경로 | 설명 | +|------------------|---------------------------------------| +| `GET /v1/health` | 구성원 id, FIPS, PQC 상태 | +| `GET /v1/card` | 이 노드의 신원 카드 | | `POST /v1/inbox` | 봉투 제출 (본문은 봉투 바이트). 성공 시 `201`과 봉투 id | -봉투의 송신자가 인증된 피어와 다르거나, 수신자가 이 노드가 아니거나, 서명이 맞지 않으면 `400`으로 거부합니다. 같은 봉투를 다시 보내면 `409`입니다. +봉투의 송신자가 인증된 피어와 다르거나, 수신자가 이 노드가 아니거나, 서명이 일치하지 않으면 `400`으로 거부됩니다. 같은 봉투를 다시 보내면 `409`로 응답합니다. ### systemd 예시 @@ -262,74 +279,80 @@ ReadWritePaths=/var/lib/pairier PrivateTmp=yes ``` -Linux에서 코어는 `mlockall`로 모든 메모리를 잠급니다. memlock 한도가 작으면 코어가 기동을 거부하므로 `LimitMEMLOCK=infinity`가 필요합니다. +Linux에서 코어는 `mlockall` 시스템 콜로 모든 메모리를 잠급니다. memlock 한도가 작으면 코어가 기동을 거부하기 때문에 `LimitMEMLOCK=infinity`가 필요합니다. ## kms-core 단독 실행 -보통은 `kmsctl`과 `kmsd`가 코어를 직접 띄웁니다. 코어를 별도 사용자로 분리해 운영하려면 소켓 경로 모드를 씁니다. +보통은 `kmsctl`과 `kmsd`가 코어를 직접 띄웁니다. 코어를 별도 사용자로 분리해 운영하려면 다음과 같이 소켓 경로 모드를 사용해야 합니다. ```bash $ kms-core --generate-root-key /path/root.key $ kms-core --socket /run/pairier/core.sock --root-key-file /path/root.key --pqc --peer-uid 1001 ``` -| 옵션 | 설명 | -| --- | --- | -| `--fd N` | 상속받은 소켓 사용 (호스트가 기동할 때 `3`) | -| `--socket PATH` | 소켓 경로에서 연결 하나만 받은 뒤 경로를 삭제 | -| `--root-key-file PATH` | 32바이트 일반 파일만 허용. 현재 사용자 소유, 그룹과 기타 권한 없음, 심볼릭 링크와 추가 하드 링크 없음 | -| `--ephemeral-root` | 메모리에만 있는 임시 루트 키 (재시작하면 블롭을 열 수 없음) | -| `--pqc` | PQC 알고리즘 활성화 (pqc 피처로 빌드한 경우) | -| `--allow-unlocked-memory` | `mlockall`을 쓸 수 없는 환경에서 계속 진행 | -| `--peer-uid UID` | 연결한 피어의 uid 검증 (기본은 자신의 uid) | +| 옵션 | 설명 | +|---------------------------|---------------------------------------------------------------| +| `--fd N` | 상속받은 소켓 사용 (호스트가 기동할 때 `3`) | +| `--socket PATH` | 소켓 경로에서 연결 하나만 받은 뒤 경로를 삭제 | +| `--root-key-file PATH` | 32바이트 일반 파일만 허용. 현재 사용자 소유, 그룹과 기타 권한 없음, 심볼릭 링크와 추가 하드 링크 없음 | +| `--ephemeral-root` | 메모리에만 있는 임시 루트 키 (재시작하면 blob을 열 수 없음) | +| `--pqc` | PQC 알고리즘 활성화 (pqc feature로 빌드한 경우) | +| `--allow-unlocked-memory` | `mlockall`을 쓸 수 없는 환경에서 계속 진행 | +| `--peer-uid UID` | 연결한 피어의 uid 검증 (기본은 자신의 uid) | -현재 `kmsctl`과 `kmsd`는 노드 디렉터리의 루트 키로 코어를 직접 기동하는 방식만 지원합니다. +> [!NOTE] +> 현재 `kmsctl`과 `kmsd`는 노드 디렉토리의 루트 키로 코어를 직접 기동하는 방식만 지원합니다. -## 노드 디렉터리 +## 노드 디렉토리 ```text / - root.key 루트 키 (0400). 모든 키 블롭을 여는 유일한 비밀 + root.key 루트 키 (0400) - 모든 키 blob을 여는 유일한 비밀(secret) node.json 구성원 id와 신원, KEM, 감사 키 id - keys/ 키 레코드. 키 재료는 루트 키로 봉인된 블롭으로만 저장 + keys/ 키 레코드 - 키 재료는 루트 키로 봉인된 blob으로만 저장 members/ 등록된 구성원 카드와 지문 inbox/ kmsd가 받은 봉투 tls.json TLS 인증서와 코어로 암호화한 TLS 개인키 audit.log 해시 체인 감사 로그 (항목마다 감사 키 서명) ``` -- 디렉터리는 `0700`이고 현재 사용자 소유여야 하며, 파일에는 그룹과 기타 권한이 없어야 합니다. 심볼릭 링크는 열지 않습니다. -- 저장된 메타데이터와 공개키는 사용할 때마다 코어가 블롭에서 다시 계산한 값과 대조합니다. -- 백업에는 `root.key`가 포함되므로 백업 매체도 루트 키와 같은 수준으로 보호해야 합니다. +- 디렉토리는 `0700`이고 현재 사용자 소유여야 하며, 파일에는 그룹과 기타 권한이 없어야 합니다. 심볼릭 링크는 열지 않습니다. +- 저장된 메타데이터와 공개키는 사용할 때마다 코어가 blob에서 다시 계산한 값과 대조합니다. +- 백업에는 `root.key`가 포함되기 때문에 **백업 매체도 루트 키와 같은 수준으로 보호**해야 합니다. ## 보안 운영 주의 사항 -- **지문은 별도 채널로 확인합니다.** 카드와 지문을 같은 경로로 받으면 중간자 공격을 막을 수 없습니다. +- **지문은 별도 채널로 확인하세요.** 카드와 지문을 같은 경로로 받으면 중간자 공격을 막을 수 없습니다. - **감사 로그의 한계를 이해해야 합니다.** 각 항목은 코어의 감사 키로 서명되므로, 코어를 쓸 수 없는 공격자(디스크 탈취, 변조된 백업 등)는 로그를 위조하거나 재계산할 수 없습니다. 반면 노드 사용자 권한으로 코어를 기동할 수 있으면 유효한 서명을 만들 수 있습니다. 또 로그 끝부분을 잘라 내는 것은 외부 기준점이 없으면 탐지할 수 없습니다. - **kmsd는 TLS 개인키를 메모리에 둡니다.** 키 교환 대상인 대칭 키와 신원, KEM, 감사 키는 코어 밖으로 나오지 않지만, 전송용 TLS 키는 예외입니다. -- **macOS는 개발용입니다.** `mlockall`이 없어 메모리 잠금 없이 실행되고, FIPS 코어도 정적 링크되지 않습니다. 운영은 Linux에서 하십시오. -- **PQC 모드 감사 로그는 빠르게 커집니다.** ML-DSA 서명 때문에 항목 하나가 약 7KB입니다. 암복호화도 한 번마다 항목이 추가됩니다. +- **macOS는 개발용으로 사용하세요.** `mlockall`이 없어 메모리 잠금 없이 실행되고, FIPS 코어도 정적 링크되지 않습니다. 운영은 Linux에서 하십시오. +- **PQC 모드 감사 로깅은 빠른 속도로 증가합니다.** ML-DSA 서명으로 인해 항목 하나가 약 7KB입니다. 암/복호화도 한 번마다 항목이 추가됩니다. + +> [!CAUTION] +> 현재 pairier는 릴리즈가 공개되지 않았으며, 그 이전에 개발이 완료되더라도 기능의 안전성 검증을 위해 시간이 소요될 수 있습니다. pairier는 개발 또는 연구용으로만 사용해주시길 바랍니다. +> +> **위 항목들은 안정된 릴리즈가 출시되지 전까지는 형식상 존재합니다. 검증이 진행됨에 따라 필요한 보안 요구사항이 충분히 증가될 수 있습니다.** ## 한도 -| 항목 | 한도 | -| --- | --- | -| 구성원 id | 영문 소문자, 숫자로 시작하고 소문자, 숫자, `.`, `_`, `-`로 최대 64자 | -| 키 label | 256바이트 | -| 암호화 평문, AAD | 각각 64KiB | -| kmsctl 입력 파일과 stdin | 1MiB | -| 봉투 컨텍스트 | 1KiB | +| 항목 | 한도 | +|---------------------|-------------------------------------------------| +| 구성원 id | 영문 소문자, 숫자로 시작하고 소문자, 숫자, `.`, `_`, `-`로 최대 64자 | +| 키 label | 256바이트 | +| 암호화 평문, AAD | 각각 64KiB | +| kmsctl 입력 파일과 stdin | 1MiB | +| 봉투 컨텍스트 | 1KiB | ## 문제 해결 -| 메시지 | 원인과 조치 | -| --- | --- | -| `mlockall: Cannot allocate memory ... (pass --allow-unlocked-memory to proceed)` | memlock 한도가 작습니다. `ulimit -l unlimited` 또는 `LimitMEMLOCK=infinity`로 늘립니다. | -| `node: fingerprint does not match the card` | 지문이 다릅니다. 카드를 다시 받고 별도 채널로 지문을 재확인합니다. | -| `core: op 0x0023 failed: capability disabled` | pqc 없이 초기화한 노드에 pqc 카드를 등록하려 했습니다. | -| `core: op 0x0030 failed: usage denied` | 키에 `export`가 없거나, 원래 용도보다 넓은 용도를 주려 했습니다. | -| `node: envelope is addressed to another member` | 다른 구성원에게 보낸 봉투입니다. | -| `node: unknown member` | 송신자 또는 수신자가 등록되지 않았습니다. | -| `store: object already exists` | 이미 가져온 키이거나 이미 받은 봉투입니다. | -| `node: stored metadata does not match the core` | 노드 디렉터리의 레코드가 변조되었습니다. 백업과 감사 로그를 확인합니다. | -| `audit: checkpoint signature is invalid` | 감사 로그가 코어 없이 수정되었습니다. | +| 메시지 | 원인 및 조치 | +|----------------------------------------------------------------------------------|------------------------------------------------------------------------------| +| `mlockall: Cannot allocate memory ... (pass --allow-unlocked-memory to proceed)` | memlock 한도가 작습니다. `ulimit -l unlimited` 또는 `LimitMEMLOCK=infinity`로 늘려야 합니다. | +| `node: fingerprint does not match the card` | 지문이 다릅니다. 카드를 다시 받아 별도 채널로 지문을 재확인해야 합니다. | +| `core: op 0x0023 failed: capability disabled` | pqc 없이 초기화한 노드에 pqc 카드를 등록하려 했습니다. | +| `core: op 0x0030 failed: usage denied` | 키에 `export`가 없거나, 원래 용도보다 넓은 용도를 주려 했습니다. | +| `node: envelope is addressed to another member` | 다른 구성원에게 보낸 봉투입니다. | +| `node: unknown member` | 송신자 또는 수신자가 등록되지 않았습니다. | +| `store: object already exists` | 이미 가져온 키이거나 이미 받은 봉투입니다. | +| `node: stored metadata does not match the core` | 노드 디렉토리의 레코드가 변조되었습니다. 백업과 감사 로그를 확인해야 합니다. | +| `audit: checkpoint signature is invalid` | 감사 로그가 코어 없이 수정되었습니다. | From edd745b9cc352d5c2970b578a31ecb3556f2e141 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 23:08:18 +0900 Subject: [PATCH 33/48] =?UTF-8?q?CI=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=8A=A4=ED=85=9D=EC=9D=98=20memlock=20=ED=95=9C=EB=8F=84=20?= =?UTF-8?q?=ED=95=B4=EC=A0=9C=20-=20Linux=20=EB=9F=AC=EB=84=88=EC=9D=98=20?= =?UTF-8?q?8MiB=20=ED=95=9C=EB=8F=84=20=EB=95=8C=EB=AC=B8=EC=97=90=20mlock?= =?UTF-8?q?all=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EA=B0=80=20=EC=8B=A4=ED=8C=A8?= =?UTF-8?q?=ED=95=98=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba09a91..8d75b2e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,9 +36,12 @@ jobs: - name: lint run: make lint-core - name: test - run: make test-core + run: | + [ "$RUNNER_OS" != Linux ] || sudo prlimit --pid $$ --memlock=unlimited:unlimited + make test-core - name: feature matrix run: | + [ "$RUNNER_OS" != Linux ] || sudo prlimit --pid $$ --memlock=unlimited:unlimited cd core cargo test --frozen -p kms-engine cargo test --frozen -p kms-engine --features pqc @@ -88,6 +91,7 @@ jobs: run: cd core && rustup show active-toolchain - name: fips engine tests run: | + sudo prlimit --pid $$ --memlock=unlimited:unlimited cd core cargo test --frozen --no-default-features --features fips cargo test --frozen --no-default-features --features fips,pqc From 3b0a02edfde62730dc32f61b643f33cefe177b50 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 23:59:15 +0900 Subject: [PATCH 34/48] =?UTF-8?q?init=20=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=ED=95=9C=20=ED=8C=8C=EC=9D=BC=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20-=20=EB=A3=A8=ED=8A=B8=20=ED=82=A4=EB=8A=94=200?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=8D=AE=EC=96=B4=EC=93=B4=20=EB=92=A4=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=ED=95=98=EA=B3=A0=20=EC=83=88=EB=A1=9C=20?= =?UTF-8?q?=EB=A7=8C=EB=93=A0=20=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC?= =?UTF-8?q?=EB=8A=94=20=ED=95=A8=EA=BB=98=20=EC=A0=9C=EA=B1=B0=20-=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20=EC=A7=81=ED=9B=84=20=EA=B0=99=EC=9D=80=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=EB=A1=9C=20=EB=8B=A4=EC=8B=9C=20init=20?= =?UTF-8?q?=EA=B0=80=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- host/internal/node/node.go | 8 +++++ host/internal/node/node_test.go | 29 ++++++++++++++++++ host/internal/store/store.go | 52 +++++++++++++++++++++++++++++++-- 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/host/internal/node/node.go b/host/internal/node/node.go index a9eaa32..67140aa 100644 --- a/host/internal/node/node.go +++ b/host/internal/node/node.go @@ -151,6 +151,14 @@ func Init(ctx context.Context, o Options, memberID string, pqc bool) (*Node, err if err != nil { return nil, err } + n, err := buildNode(ctx, o, st, memberID, pqc) + if err != nil { + return nil, errors.Join(err, st.Discard()) + } + return n, nil +} + +func buildNode(ctx context.Context, o Options, st *store.Store, memberID string, pqc bool) (*Node, error) { if err := generateRootKey(ctx, o, st.RootKeyPath()); err != nil { return nil, err } diff --git a/host/internal/node/node_test.go b/host/internal/node/node_test.go index 319bdda..ed9992e 100644 --- a/host/internal/node/node_test.go +++ b/host/internal/node/node_test.go @@ -56,6 +56,35 @@ func TestInitChecksCoreBeforeCreatingDir(t *testing.T) { } } +func TestFailedInitLeavesNothingBehind(t *testing.T) { + base := t.TempDir() + existing := filepath.Join(base, "existing") + if err := os.Mkdir(existing, 0o700); err != nil { + t.Fatal(err) + } + for _, dir := range []string{filepath.Join(base, "fresh"), existing} { + o := opts(t, dir) + o.Timeout = time.Nanosecond + if _, err := Init(context.Background(), o, "alice", false); err == nil { + t.Fatal("init succeeded with an unusable core timeout") + } + entries, err := os.ReadDir(dir) + if dir == existing { + if err != nil || len(entries) != 0 { + t.Fatalf("pre-existing dir not emptied: %v %d", err, len(entries)) + } + } else if !errors.Is(err, os.ErrNotExist) { + t.Fatalf("created dir not removed: %v %d", err, len(entries)) + } + o.Timeout = 5 * time.Second + n, err := Init(context.Background(), o, "alice", false) + if err != nil { + t.Fatalf("re-init after failure: %v", err) + } + _ = n.Close() + } +} + func TestRelativeCoreBinary(t *testing.T) { o := opts(t, filepath.Join(t.TempDir(), "alice")) abs, err := filepath.Abs(o.CoreBinary) diff --git a/host/internal/store/store.go b/host/internal/store/store.go index 39fb8e1..27a58e5 100644 --- a/host/internal/store/store.go +++ b/host/internal/store/store.go @@ -44,7 +44,8 @@ var ( ) type Store struct { - dir string + dir string + created bool } type Origin struct { @@ -98,8 +99,10 @@ type InboxRecord struct { } func Init(dir string) (*Store, error) { + created := true if err := os.Mkdir(dir, dirMode); err != nil { if errors.Is(err, fs.ErrExist) { + created = false entries, rerr := os.ReadDir(dir) if rerr != nil || len(entries) != 0 { return nil, fmt.Errorf("%w: %s is not empty", ErrExists, dir) @@ -119,7 +122,52 @@ func Init(dir string) (*Store, error) { if err := syncDir(dir); err != nil { return nil, err } - return Open(dir) + st, err := Open(dir) + if err != nil { + return nil, err + } + st.created = created + return st, nil +} + +func (s *Store) Discard() error { + entries, err := os.ReadDir(s.dir) + if err != nil { + return err + } + var errs []error + for _, e := range entries { + path := filepath.Join(s.dir, e.Name()) + if e.Name() == rootFile { + errs = append(errs, wipeFile(path)) + } + errs = append(errs, os.RemoveAll(path)) + } + if s.created { + errs = append(errs, os.Remove(s.dir)) + } else { + errs = append(errs, syncDir(s.dir)) + } + return errors.Join(errs...) +} + +func wipeFile(path string) error { + fi, err := os.Lstat(path) + if err != nil || !fi.Mode().IsRegular() { + return err + } + if err := os.Chmod(path, 0o600); err != nil { + return err + } + f, err := os.OpenFile(path, os.O_WRONLY, 0) + if err != nil { + return err + } + defer f.Close() + if _, err := f.Write(make([]byte, fi.Size())); err != nil { + return err + } + return f.Sync() } func Open(dir string) (*Store, error) { From 1d32e9cbe072c5ce3296dc4b0454df01ded94f56 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 23:59:15 +0900 Subject: [PATCH 35/48] =?UTF-8?q?kmsd=EA=B0=80=20KMS=5FDIR,=20KMS=5FCORE?= =?UTF-8?q?=20=ED=99=98=EA=B2=BD=20=EB=B3=80=EC=88=98=EB=A5=BC=20=EC=9D=BD?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20-=20=EC=9A=B0?= =?UTF-8?q?=EC=84=A0=EC=88=9C=EC=9C=84=EB=8A=94=20=ED=94=8C=EB=9E=98?= =?UTF-8?q?=EA=B7=B8,=20=EC=84=A4=EC=A0=95=20=ED=8C=8C=EC=9D=BC,=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EB=B3=80=EC=88=98,=20=EA=B8=B0=EB=B3=B8?= =?UTF-8?q?=EA=B0=92=20=EC=88=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- host/cmd/kmsd/main.go | 16 +++++++++++----- host/cmd/kmsd/main_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/host/cmd/kmsd/main.go b/host/cmd/kmsd/main.go index f78d953..e8b84b4 100644 --- a/host/cmd/kmsd/main.go +++ b/host/cmd/kmsd/main.go @@ -41,8 +41,8 @@ func run(args []string, stdout, stderr io.Writer, ready func(addr string)) int { fs := flag.NewFlagSet("kmsd", flag.ContinueOnError) fs.SetOutput(stderr) cfgPath := fs.String("config", "", "configuration file (json)") - dir := fs.String("dir", "", "node directory") - corePath := fs.String("core", "", "kms-core binary (default next to kmsd)") + dir := fs.String("dir", "", "node directory (default $KMS_DIR)") + corePath := fs.String("core", "", "kms-core binary (default $KMS_CORE or next to kmsd)") listen := fs.String("listen", "", "listen address host:port") allowUnlocked := fs.Bool("allow-unlocked-memory", runtime.GOOS == "darwin", "run the core without locked memory") timeout := fs.Duration("timeout", defaultTimeout, "core call timeout") @@ -78,12 +78,18 @@ func run(args []string, stdout, stderr io.Writer, ready func(addr string)) int { } s.node.Timeout = c.TimeoutDuration(*timeout) } - if set["dir"] || s.node.Dir == "" { + if set["dir"] { s.node.Dir = *dir } - if set["core"] || s.node.CoreBinary == "" { + if s.node.Dir == "" { + s.node.Dir = os.Getenv("KMS_DIR") + } + if set["core"] { s.node.CoreBinary = *corePath } + if s.node.CoreBinary == "" { + s.node.CoreBinary = os.Getenv("KMS_CORE") + } if set["listen"] { s.listen = *listen } @@ -94,7 +100,7 @@ func run(args []string, stdout, stderr io.Writer, ready func(addr string)) int { s.node.Timeout = *timeout } if s.node.Dir == "" { - fmt.Fprintln(stderr, "kmsd: --dir or config dir is required") + fmt.Fprintln(stderr, "kmsd: --dir, config dir or $KMS_DIR is required") return 2 } if s.node.CoreBinary == "" { diff --git a/host/cmd/kmsd/main_test.go b/host/cmd/kmsd/main_test.go index 2451872..689ef06 100644 --- a/host/cmd/kmsd/main_test.go +++ b/host/cmd/kmsd/main_test.go @@ -32,6 +32,35 @@ func TestFlagsAndVersion(t *testing.T) { } } +func TestEnvironmentDefaults(t *testing.T) { + t.Setenv("KMS_DIR", "") + t.Setenv("KMS_CORE", "") + var out, errb bytes.Buffer + if code := run([]string{"--listen", "127.0.0.1:0"}, &out, &errb, nil); code != 2 { + t.Fatalf("missing dir accepted: %d", code) + } + envDir := filepath.Join(t.TempDir(), "env") + envCore := filepath.Join(t.TempDir(), "env-core") + t.Setenv("KMS_DIR", envDir) + t.Setenv("KMS_CORE", envCore) + errb.Reset() + if code := run([]string{"--listen", "127.0.0.1:0"}, &out, &errb, nil); code != 1 || !strings.Contains(errb.String(), envCore) { + t.Fatalf("env core not used: %d %s", code, errb.String()) + } + flagCore := filepath.Join(t.TempDir(), "flag-core") + errb.Reset() + if code := run([]string{"--listen", "127.0.0.1:0", "--core", flagCore}, &out, &errb, nil); code != 1 || !strings.Contains(errb.String(), flagCore) { + t.Fatalf("flag did not override env: %d %s", code, errb.String()) + } + cfgCore := filepath.Join(t.TempDir(), "cfg-core") + cfg := filepath.Join(t.TempDir(), "c.json") + _ = os.WriteFile(cfg, []byte(`{"core":"`+cfgCore+`","listen":"127.0.0.1:0"}`), 0o600) + errb.Reset() + if code := run([]string{"--config", cfg}, &out, &errb, nil); code != 1 || !strings.Contains(errb.String(), cfgCore) { + t.Fatalf("config did not override env: %d %s", code, errb.String()) + } +} + func TestServeAndShutdown(t *testing.T) { bin := os.Getenv("KMS_CORE_BIN") if bin == "" { From 47f23060043f5b221f6f6425f5461448f87e3061 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 14 Oct 2026 23:59:15 +0900 Subject: [PATCH 36/48] =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20-=20=EC=A0=84=EC=97=AD=20=EC=98=B5=EC=85=98=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98,=20kmsd=20=ED=99=98=EA=B2=BD=20=EB=B3=80=EC=88=98,=20?= =?UTF-8?q?README=20=EA=B5=AC=EC=A1=B0=20=EC=A0=88=20=EA=B0=B1=EC=8B=A0?= =?UTF-8?q?=EA=B3=BC=20=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +++++- README_KR.md | 6 +++++- USAGE.md | 22 +++++++++++----------- USAGE_KR.md | 26 +++++++++++++------------- 4 files changed, 34 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 3ca29f5..3da1bb9 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,11 @@ A Zero-Trust hybrid Key Management System (KMS) built with Go and Rust. The Go h - `host/` Go module (host) - `internal/wire` Go implementation of the wire protocol - `internal/core` core process launcher and client - - `cmd/kmsd`, `cmd/kmsctl` daemon and management tool (API layer not yet implemented) + - `internal/store`, `internal/node` node directory and key, member and envelope management + - `internal/exchange` identity card and key exchange envelope formats + - `internal/audit` hash-chained audit log + - `internal/auth`, `internal/api` card-pinned mTLS and the HTTP API + - `cmd/kmsd`, `cmd/kmsctl` daemon and management tool - `conformance/wire/` protocol conformance vectors that both Rust and Go must pass ## Features diff --git a/README_KR.md b/README_KR.md index 60514a5..17d31b4 100644 --- a/README_KR.md +++ b/README_KR.md @@ -18,7 +18,11 @@ - `host/` Go 모듈 (호스트) - `internal/wire` 와이어 프로토콜 Go 구현 - `internal/core` 코어 프로세스 기동과 클라이언트 - - `cmd/kmsd`, `cmd/kmsctl` 데몬과 관리 도구 (API 계층은 구현 예정) + - `internal/store`, `internal/node` 노드 디렉토리와 키, 구성원, 봉투 관리 + - `internal/exchange` 신원 카드와 키 교환 봉투 형식 + - `internal/audit` 해시 체인 감사 로그 + - `internal/auth`, `internal/api` 카드에 고정된 mTLS와 HTTP API + - `cmd/kmsd`, `cmd/kmsctl` 데몬과 관리 도구 - `conformance/wire/` Rust와 Go가 함께 통과해야 하는 프로토콜 적합성 벡터 ## 기능 diff --git a/USAGE.md b/USAGE.md index 3227ab7..da77b1d 100644 --- a/USAGE.md +++ b/USAGE.md @@ -180,7 +180,7 @@ verified 13 entries | `--allow-unlocked-memory` | `true` on macOS, `false` elsewhere | Run the core without locked memory | | `--timeout DUR` | `10s` | Core call timeout | -Global options must come before the command, for example `kmsctl --dir X key list`. +Global options may appear before or after the command. `kmsctl --dir X key list` and `kmsctl key list --dir X` are equivalent, and when both are given the value after the command wins. ### Commands @@ -226,15 +226,15 @@ If the file given with `--out` already exists, the command fails instead of over ### Flags and configuration file -| Flag | Config field | Default | -|---------------------------|-------------------------|------------------------------------| -| `--dir` | `dir` | None (required) | -| `--core` | `core` | `kms-core` next to kmsd | -| `--listen` | `listen` | `127.0.0.1:7443` | -| `--allow-unlocked-memory` | `allow_unlocked_memory` | `true` on macOS, `false` elsewhere | -| `--timeout` | `timeout` | `10s` | -| `--config FILE` | | | -| `--version` | | | +| Flag | Config field | Default | +|---------------------------|-------------------------|------------------------------------------------| +| `--dir` | `dir` | `$KMS_DIR`, otherwise required | +| `--core` | `core` | `$KMS_CORE`, otherwise `kms-core` next to kmsd | +| `--listen` | `listen` | `127.0.0.1:7443` | +| `--allow-unlocked-memory` | `allow_unlocked_memory` | `true` on macOS, `false` elsewhere | +| `--timeout` | `timeout` | `10s` | +| `--config FILE` | | | +| `--version` | | | ```json { @@ -246,7 +246,7 @@ If the file given with `--out` already exists, the command fails instead of over } ``` -- Precedence is explicit flags -> configuration file -> defaults. +- Precedence is explicit flags -> configuration file -> environment variables -> defaults. - The configuration file must be a regular file that the group and other users cannot access. kmsd refuses to start if the file contains unknown fields. - The `listen` host must be an IP address. - `kmsd` opens only nodes already initialized with `kmsctl init`. diff --git a/USAGE_KR.md b/USAGE_KR.md index e9cfd9b..645931a 100644 --- a/USAGE_KR.md +++ b/USAGE_KR.md @@ -113,7 +113,7 @@ alice$ kmsctl --dir ~/.pairier key gen --label project/db af8477cc1076f219bfb047d846629eb8 aes-256-gcm encrypt,decrypt,export project/db ``` -생성되는 키는 AES-256-GCM 대칭 키입니다. `--usage` 플래그를 생략하면 기본 갑으로 `encrypt,decrypt,export`가 적용됩니다. 다른 구성원에게 보낼 키에는 반드시 `export`가 포함되어 있어야 합니다. +생성되는 키는 AES-256-GCM 대칭 키입니다. `--usage` 플래그를 생략하면 기본값으로 `encrypt,decrypt,export`가 적용됩니다. 다른 구성원에게 보낼 키에는 반드시 `export`가 포함되어 있어야 합니다. ### 5-a. 오프라인 전달 (파일) @@ -180,7 +180,7 @@ verified 13 entries | `--allow-unlocked-memory` | macOS `true`, 그 외 `false` | 메모리 잠금 없이 코어 실행 | | `--timeout DUR` | `10s` | 코어 호출 제한 시간 | -전역 옵션은 명령보다 앞에 두어야 합니다. 예를 들어 `kmsctl --dir X key list`처럼 사용할 수 있습니다. +전역 옵션은 명령 앞/뒤 어디에 두어도 됩니다. `kmsctl --dir X key list`와 `kmsctl key list --dir X`는 같으며, 양쪽에 모두 설정하면 명령 뒤의 값이 적용됩니다. ### 명령 @@ -226,15 +226,15 @@ verified 13 entries ### 플래그와 설정 파일 -| 플래그 | 설정 파일 필드 | 기본값 | -|---------------------------|-------------------------|---------------------------| -| `--dir` | `dir` | 없음 (필수) | -| `--core` | `core` | kmsd 옆의 `kms-core` | -| `--listen` | `listen` | `127.0.0.1:7443` | -| `--allow-unlocked-memory` | `allow_unlocked_memory` | macOS `true`, 그 외 `false` | -| `--timeout` | `timeout` | `10s` | -| `--config FILE` | | | -| `--version` | | | +| 플래그 | 설정 파일 필드 | 기본값 | +|---------------------------|-------------------------|---------------------------------------| +| `--dir` | `dir` | `$KMS_DIR`, 없는 경우 필수 | +| `--core` | `core` | `$KMS_CORE`, 없는 경우 kmsd 옆의 `kms-core` | +| `--listen` | `listen` | `127.0.0.1:7443` | +| `--allow-unlocked-memory` | `allow_unlocked_memory` | macOS `true`, 그 외 `false` | +| `--timeout` | `timeout` | `10s` | +| `--config FILE` | | | +| `--version` | | | ```json { @@ -246,7 +246,7 @@ verified 13 entries } ``` -- 우선순위는 명시한 플래그 -> 설정 파일 -> 기본값 순입니다. +- 우선순위는 명시한 플래그 -> 설정 파일 -> 환경 변수 -> 기본값 순입니다. - 설정 파일은 일반 파일이어야 하며, 그룹 또는 다른 사용자가 사용할 수 없어야 합니다. 알 수 없는 필드가 있다면 기동하지 않습니다. - `listen`의 호스트는 IP 주소만 허용합니다. - `kmsd`는 `kmsctl init`으로 이미 초기화한 노드만 엽니다. @@ -331,7 +331,7 @@ $ kms-core --socket /run/pairier/core.sock --root-key-file /path/root.key --pqc > [!CAUTION] > 현재 pairier는 릴리즈가 공개되지 않았으며, 그 이전에 개발이 완료되더라도 기능의 안전성 검증을 위해 시간이 소요될 수 있습니다. pairier는 개발 또는 연구용으로만 사용해주시길 바랍니다. > -> **위 항목들은 안정된 릴리즈가 출시되지 전까지는 형식상 존재합니다. 검증이 진행됨에 따라 필요한 보안 요구사항이 충분히 증가될 수 있습니다.** +> **위 항목들은 안정된 릴리즈가 출시되기 전까지는 형식상 존재합니다. 검증이 진행됨에 따라 필요한 보안 요구사항이 충분히 증가될 수 있습니다.** ## 한도 From 2ec01ea696ff4f4190f3c86aca26683f3d346c77 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 15 Oct 2026 00:13:35 +0900 Subject: [PATCH 37/48] =?UTF-8?q?Apache-2=20=EB=9D=BC=EC=9D=B4=EC=84=A0?= =?UTF-8?q?=EC=8A=A4=20=EC=A0=81=EC=9A=A9=20-=20LICENSE,=20NOTICE=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=EC=99=80=20crate=20license=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=84=A4=EC=A0=95=20-=20SBOM=EA=B3=BC=20=EB=B0=B0?= =?UTF-8?q?=ED=8F=AC=20tarball=EC=97=90=20=EB=9D=BC=EC=9D=B4=EC=84=A0?= =?UTF-8?q?=EC=8A=A4=20=EC=A0=95=EB=B3=B4=20=ED=8F=AC=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 202 ++++++++++++++++++++++++++++++ NOTICE | 2 + core/Cargo.toml | 1 + core/crates/kms-core/Cargo.toml | 1 + core/crates/kms-engine/Cargo.toml | 1 + core/crates/kms-sys/Cargo.toml | 1 + core/crates/kms-wire/Cargo.toml | 1 + scripts/dist.sh | 5 +- scripts/sbom.py | 2 +- 9 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 LICENSE create mode 100644 NOTICE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..8d423b0 --- /dev/null +++ b/NOTICE @@ -0,0 +1,2 @@ +pairier +Copyright 2026 Quant-TheodoreFelix diff --git a/core/Cargo.toml b/core/Cargo.toml index d15b578..44d170f 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -6,6 +6,7 @@ members = ["crates/*"] version = "1.0.0" edition = "2024" rust-version = "1.95" +license = "Apache-2.0" publish = false [workspace.dependencies] diff --git a/core/crates/kms-core/Cargo.toml b/core/crates/kms-core/Cargo.toml index cbc1ba5..dabd823 100644 --- a/core/crates/kms-core/Cargo.toml +++ b/core/crates/kms-core/Cargo.toml @@ -3,6 +3,7 @@ name = "kms-core" version.workspace = true edition.workspace = true rust-version.workspace = true +license.workspace = true publish.workspace = true [[bin]] diff --git a/core/crates/kms-engine/Cargo.toml b/core/crates/kms-engine/Cargo.toml index 8435ba6..989c67a 100644 --- a/core/crates/kms-engine/Cargo.toml +++ b/core/crates/kms-engine/Cargo.toml @@ -3,6 +3,7 @@ name = "kms-engine" version.workspace = true edition.workspace = true rust-version.workspace = true +license.workspace = true publish.workspace = true [features] diff --git a/core/crates/kms-sys/Cargo.toml b/core/crates/kms-sys/Cargo.toml index b830382..15bc0f8 100644 --- a/core/crates/kms-sys/Cargo.toml +++ b/core/crates/kms-sys/Cargo.toml @@ -3,6 +3,7 @@ name = "kms-sys" version.workspace = true edition.workspace = true rust-version.workspace = true +license.workspace = true publish.workspace = true [dependencies] diff --git a/core/crates/kms-wire/Cargo.toml b/core/crates/kms-wire/Cargo.toml index efd69d6..a467b68 100644 --- a/core/crates/kms-wire/Cargo.toml +++ b/core/crates/kms-wire/Cargo.toml @@ -3,6 +3,7 @@ name = "kms-wire" version.workspace = true edition.workspace = true rust-version.workspace = true +license.workspace = true publish.workspace = true [dependencies] diff --git a/scripts/dist.sh b/scripts/dist.sh index 973f783..184a5fc 100755 --- a/scripts/dist.sh +++ b/scripts/dist.sh @@ -8,6 +8,7 @@ kmsctl_bin="$4" out_dir="$5" sbom="$6" epoch="${SOURCE_DATE_EPOCH:-0}" +root=$(cd "$(dirname "$0")/.." && pwd) os=$(uname -s | tr '[:upper:]' '[:lower:]') arch=$(uname -m) @@ -37,7 +38,9 @@ chmod 0755 "$stage"/* "$stage/kmsd" --version >> "$stage/VERSION" "$stage/kmsctl" version >> "$stage/VERSION" cp "$sbom" "$stage/sbom.cdx.json" -chmod 0644 "$stage/VERSION" "$stage/sbom.cdx.json" +cp "$root/LICENSE" "$stage/LICENSE" +cp "$root/NOTICE" "$stage/NOTICE" +chmod 0644 "$stage/VERSION" "$stage/sbom.cdx.json" "$stage/LICENSE" "$stage/NOTICE" ( cd "$out_dir" diff --git a/scripts/sbom.py b/scripts/sbom.py index 6b6dc0a..391bfc8 100644 --- a/scripts/sbom.py +++ b/scripts/sbom.py @@ -133,7 +133,7 @@ def main(): "version": 1, "metadata": { "timestamp": datetime.datetime.fromtimestamp(a.epoch, datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), - "component": {"type": "application", "bom-ref": top, "name": "pairier", "version": a.version}, + "component": {"type": "application", "bom-ref": top, "name": "pairier", "version": a.version, "licenses": [{"expression": "Apache-2.0"}]}, "properties": [{"name": "pairier:core-flags", "value": a.core_flags.strip()}], }, "components": sorted(cc + gc, key=lambda c: c["bom-ref"]), From 18652feb33316dffd8d1ab74849a87d2892aa575 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 15 Oct 2026 00:13:35 +0900 Subject: [PATCH 38/48] =?UTF-8?q?minisign=20=EB=A6=B4=EB=A6=AC=EC=A6=88=20?= =?UTF-8?q?=EC=84=9C=EB=AA=85=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20SHA256SUMS=20=EC=84=9C=EB=AA=85?= =?UTF-8?q?=EA=B3=BC=20=EA=B2=80=EC=A6=9D,=20make=20sign=EA=B3=BC=20verify?= =?UTF-8?q?-dist=20=ED=83=80=EA=B2=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 8 +++++++- scripts/sign.sh | 29 +++++++++++++++++++++++++++++ scripts/verify.sh | 43 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100755 scripts/sign.sh create mode 100755 scripts/verify.sh diff --git a/Makefile b/Makefile index afdfcc0..89e3783 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ CARGO_NET := $(CARGO) +$(RUST_TOOLCHAIN) CORE_MANIFEST := --manifest-path $(CORE_DIR)/Cargo.toml GO_NET := GOPROXY=https://proxy.golang.org,direct GOFLAGS=-trimpath $(GO) -.PHONY: all build build-core build-host test test-core test-host lint lint-core lint-host fmt vendor update clean dist audit +.PHONY: all build build-core build-host test test-core test-host lint lint-core lint-host fmt vendor update clean dist sign verify-dist audit all: build @@ -82,6 +82,12 @@ dist: build ./scripts/dist.sh $(VERSION) $(CORE_RELEASE) $(HOST_DIR)/bin/kmsd $(HOST_DIR)/bin/kmsctl $(DIST_DIR) $(DIST_DIR)/sbom.cdx.json rm -f $(DIST_DIR)/sbom.cdx.json +sign: + ./scripts/sign.sh $(DIST_DIR) $(VERSION) + +verify-dist: + ./scripts/verify.sh $(DIST_DIR) minisign.pub $(VERSION) + audit: cd $(CORE_DIR) && $(CARGO) deny check --disable-fetch advisories licenses bans sources @command -v govulncheck >/dev/null && (cd $(HOST_DIR) && govulncheck ./...) || echo "govulncheck not installed, skipping" diff --git a/scripts/sign.sh b/scripts/sign.sh new file mode 100755 index 0000000..f655045 --- /dev/null +++ b/scripts/sign.sh @@ -0,0 +1,29 @@ +#!/bin/sh +set -eu + +if [ $# -ne 2 ]; then + echo "usage: sign.sh DIST_DIR VERSION" >&2 + exit 2 +fi +dist_dir="$1" +version="$2" +key="${MINISIGN_KEY:-$HOME/.minisign/minisign.key}" +sums="$dist_dir/SHA256SUMS" + +if ! command -v minisign >/dev/null 2>&1; then + echo "sign: minisign is required" >&2 + exit 1 +fi +if [ ! -f "$sums" ]; then + echo "sign: $sums not found (run make dist first)" >&2 + exit 1 +fi +if [ ! -f "$key" ]; then + echo "sign: secret key $key not found (set MINISIGN_KEY)" >&2 + exit 1 +fi + +rm -f "$sums.minisig" +minisign -S -s "$key" -m "$sums" -x "$sums.minisig" \ + -t "pairier $version SHA256SUMS" -c "pairier release signature" +echo "$sums.minisig" diff --git a/scripts/verify.sh b/scripts/verify.sh new file mode 100755 index 0000000..ccf7e11 --- /dev/null +++ b/scripts/verify.sh @@ -0,0 +1,43 @@ +#!/bin/sh +set -eu + +if [ $# -lt 2 ] || [ $# -gt 3 ]; then + echo "usage: verify.sh DIST_DIR PUBLIC_KEY [VERSION]" >&2 + exit 2 +fi +dist_dir="$1" +pub="$2" +version="${3:-}" +sums="$dist_dir/SHA256SUMS" + +if ! command -v minisign >/dev/null 2>&1; then + echo "verify: minisign is required" >&2 + exit 1 +fi +if [ ! -f "$sums" ] || [ ! -f "$sums.minisig" ]; then + echo "verify: $sums and $sums.minisig are required" >&2 + exit 1 +fi +if [ ! -f "$pub" ]; then + echo "verify: public key $pub not found" >&2 + exit 1 +fi + +out=$(minisign -V -p "$pub" -m "$sums" -x "$sums.minisig") +printf '%s\n' "$out" +if [ -n "$version" ]; then + case "$out" in + *"Trusted comment: pairier $version SHA256SUMS"*) ;; + *) + echo "verify: trusted comment does not name version $version" >&2 + exit 1 + ;; + esac +fi + +cd "$dist_dir" +if command -v sha256sum >/dev/null 2>&1; then + sha256sum -c SHA256SUMS +else + shasum -a 256 -c SHA256SUMS +fi From 41bef0ea8b6f17df3680d6034addf42a9dcdcb40 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 15 Oct 2026 00:13:35 +0900 Subject: [PATCH 39/48] =?UTF-8?q?=EB=B3=B4=EC=95=88=20=EC=A0=95=EC=B1=85?= =?UTF-8?q?=20=EB=AC=B8=EC=84=9C=20=EC=B6=94=EA=B0=80=20-=20SECURITY=20?= =?UTF-8?q?=ED=95=9C/=EC=98=81=20=EC=9E=91=EC=84=B1,=20README=EC=99=80=20U?= =?UTF-8?q?SAGE=EC=97=90=20=EB=B3=B4=EC=95=88=EA=B3=BC=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EC=84=A0=EC=8A=A4,=20=EC=84=9C=EB=AA=85=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 +++++ README_KR.md | 8 +++++ SECURITY.md | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++ SECURITY_KR.md | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++ USAGE.md | 3 +- USAGE_KR.md | 3 +- 6 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 SECURITY.md create mode 100644 SECURITY_KR.md diff --git a/README.md b/README.md index 3da1bb9..02e4fa8 100644 --- a/README.md +++ b/README.md @@ -44,3 +44,11 @@ The following features are also included. ## Build and run For details, see [USAGE.md](USAGE.md). + +## Security + +See [SECURITY.md](SECURITY.md) for how to report a vulnerability and how to verify a release. + +## License + +Apache-2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE). diff --git a/README_KR.md b/README_KR.md index 17d31b4..3d1b373 100644 --- a/README_KR.md +++ b/README_KR.md @@ -44,3 +44,11 @@ ## 빌드 및 실행 이 내용에 관해서는 [USAGE.md](USAGE_KR.md)를 확인할 수 있습니다. + +## 보안 + +취약점 신고 방법과 릴리즈 검증 방법은 [SECURITY_KR.md](SECURITY_KR.md)를 확인하세요. + +## 라이선스 + +Apache-2.0입니다. [LICENSE](LICENSE)와 [NOTICE](NOTICE)를 확인하세요. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..d73d83c --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,91 @@ +# Security policy + +
+ +[한국어](SECURITY_KR.md) | English + +
+ +> [!WARNING] +> pairier has not been released yet. Even after development is complete, verifying the safety of its features may take additional time. Please use pairier for development or research purposes only. + +## Reporting a vulnerability + +Do not open a public issue for a suspected vulnerability. Use one of the following private channels. + +- Email: qtfelix@qu4nt.space +- GitHub: [Report a vulnerability](https://github.com/Quant-Off/pairier/security/advisories/new) (private vulnerability reporting) + +Please include the affected component and version (release or commit hash), steps to reproduce or a proof of concept, and the impact you expect. Reports in Korean or English are both welcome. + +- You will receive an acknowledgement within 7 days. +- Confirmed issues are fixed in a patch release and credited in the advisory unless you ask otherwise. +- Please allow up to 90 days before public disclosure so that a fix can be released. If the fix ships earlier, disclosure can happen earlier. + +## Supported versions + +| Version | Supported | +|---------|-----------| +| 1.0.x | Yes, once released | +| Unreleased commits | No | + +Security fixes are released only for the latest 1.0.x release. + +## Scope + +In scope: + +- `kms-core`, `kmsd` and `kmsctl` +- The KWP1 wire protocol, the KID1 identity card and the KXP1 envelope formats +- Node directory storage, the audit log and the mTLS layer +- The build, vendoring and release pipeline (`Makefile`, `scripts/`, `.github/workflows/`) + +Out of scope, given the trust boundaries below: + +- An attacker who already runs code as the node user. The node user can start the core and read the node directory by design. +- Vulnerabilities in vendored upstream projects (AWS-LC, aws-lc-rs, the Go toolchain). Please report those upstream, but a note to us is welcome so that the vendored copy can be updated. +- Resource exhaustion of a node by its own operator. +- Attacks that require a tampered build toolchain or a compromised operating system. + +## Trust boundaries + +pairier is designed around "Abandon trust. Choose isolation." + +- Key material exists in plaintext only inside `kms-core`, a separate process. The host never sees an unsealed key. +- Every key is stored as a blob sealed under the node's root key. `root.key` is the only secret in the node directory. +- Trust between members comes only from identity card fingerprints verified over a separate channel. There is no CA and no trust on first use. +- Nothing listens on the network except `kmsd`, and `kmsd` accepts only mTLS clients whose certificate matches a registered member's card. +- Builds use vendored dependencies only and run without network access. + +## Known limitations + +These are design choices or open items that are disclosed here rather than treated as vulnerabilities. Reports that go beyond them are welcome. + +- `kmsd` holds its TLS private key in process memory. Identity, KEM, audit and symmetric keys never leave the core, but the transport key does. +- The P-256 + ML-KEM-768 hybrid KEM combines the two shared secrets with an HKDF construction specific to pairier. It is not a standardized combiner. +- A member who receives a key with `export` can send it on to others. Omit `export` to prevent re-sharing. +- The audit log is a hash chain with a per-entry signature by the core's audit key. Anyone who can start the core as the node user can produce valid entries, and truncation of the log tail cannot be detected without an external reference. +- On macOS the core runs without locked memory and the FIPS core is not statically linked. macOS is for development only. +- `make audit` uses a local RustSec advisory database that must be refreshed on a connected machine. +- The Rust wire decoder mutation test is seeded, not coverage guided. +- Linux x86_64 is covered by CI only. Offline verification has been done on Linux arm64. + +## Verifying a release + +Release tarballs are signed with [minisign](https://jedisct1.github.io/minisign/). The signing key is generated and kept offline and is never available to CI. + +- Public key: `minisign.pub` in the repository root +- Before trusting the key for the first time, compare the full key string with a copy obtained through a channel other than this repository. + +```bash +$ minisign -Vm SHA256SUMS -p minisign.pub +$ sha256sum -c SHA256SUMS +``` + +Releases are also reproducible. `make dist FIPS=1` on Linux with the pinned toolchains produces a byte-identical tarball, so you can rebuild and compare `SHA256SUMS` instead of, or in addition to, checking the signature. + +## Cryptography + +- Core: aws-lc-rs. The `fips` feature selects the AWS-LC FIPS 140-3 validated module (static Linux builds). +- Host: Go standard library only, built with `GOFIPS140=certified` by default. +- Algorithms: AES-256-GCM, ECDSA P-256/P-384, Ed25519, DHKEM(P-256) per RFC 9180, ML-DSA-65/87, ML-KEM-768/1024, and the P-256 + ML-KEM-768 hybrid. diff --git a/SECURITY_KR.md b/SECURITY_KR.md new file mode 100644 index 0000000..63e48be --- /dev/null +++ b/SECURITY_KR.md @@ -0,0 +1,91 @@ +# 보안 정책 + +
+ +한국어 | [English](SECURITY.md) + +
+ +> [!WARNING] +> 현재 pairier는 릴리즈가 공개되지 않았으며, 그 이전에 개발이 완료되더라도 기능의 안전성 검증을 위해 시간이 소요될 수 있습니다. pairier는 개발 또는 연구용으로만 사용해주시길 바랍니다. + +## 취약점 신고 + +취약점으로 의심되는 내용은 공개 이슈로 올리지 마시고, 다음 비공개 채널 중 하나를 이용해 주세요. + +- 이메일: qtfelix@qu4nt.space +- GitHub: [Report a vulnerability](https://github.com/Quant-Off/pairier/security/advisories/new) (비공개 취약점 신고) + +영향을 받는 구성 요소와 버전(릴리즈 또는 커밋 해시), 재현 절차 또는 개념 증명, 예상되는 영향을 함께 적어 주세요. 한국어와 영어 모두 괜찮습니다. + +- 7일 이내에 접수 확인 답변을 드립니다. +- 확인된 문제는 패치 릴리즈로 수정하며, 원하지 않으시는 경우를 제외하고 권고문에 신고자를 표기합니다. +- 수정본이 배포될 수 있도록 공개까지 최대 90일을 기다려 주세요. 수정본이 더 빨리 나오면 더 빨리 공개할 수 있습니다. + +## 지원 버전 + +| 버전 | 지원 여부 | +|------|-----------| +| 1.0.x | 릴리즈 이후 지원 | +| 릴리즈되지 않은 커밋 | 지원하지 않음 | + +보안 수정은 최신 1.0.x 릴리즈에만 제공됩니다. + +## 범위 + +범위 안: + +- `kms-core`, `kmsd`, `kmsctl` +- KWP1 와이어 프로토콜, KID1 신원 카드, KXP1 봉투 형식 +- 노드 디렉토리 저장소, 감사 로그, mTLS 계층 +- 빌드, 벤더링, 릴리즈 파이프라인 (`Makefile`, `scripts/`, `.github/workflows/`) + +범위 밖 (아래 신뢰 경계를 전제로 합니다): + +- 이미 노드 사용자 권한으로 코드를 실행하는 공격자. 노드 사용자는 설계상 코어를 기동하고 노드 디렉토리를 읽을 수 있습니다. +- 벤더링된 상위 프로젝트(AWS-LC, aws-lc-rs, Go 툴체인)의 취약점. 해당 프로젝트에 신고해 주시되, 벤더링 사본을 갱신할 수 있도록 저희에게도 알려 주시면 감사하겠습니다. +- 운영자 자신에 의한 노드 자원 고갈 +- 변조된 빌드 툴체인이나 침해된 운영 체제를 전제로 하는 공격 + +## 신뢰 경계 + +pairier는 "Abandon trust. Choose isolation." 원칙으로 설계되었습니다. + +- 키 재료는 별도 프로세스인 `kms-core` 안에서만 평문으로 존재합니다. 호스트는 봉인이 풀린 키를 보지 못합니다. +- 모든 키는 노드의 루트 키로 봉인된 blob으로 저장됩니다. 노드 디렉토리의 비밀은 `root.key` 하나뿐입니다. +- 구성원 간 신뢰는 별도 채널로 확인한 신원 카드 지문에서만 나옵니다. CA도, 최초 접속 신뢰(TOFU)도 없습니다. +- 네트워크를 listen하는 것은 `kmsd`뿐이며, `kmsd`는 등록된 구성원 카드와 인증서가 일치하는 mTLS 클라이언트만 받습니다. +- 빌드는 벤더링된 의존성만 사용하며 네트워크 없이 동작합니다. + +## 알려진 한계 + +다음은 취약점이 아니라 설계상 선택이거나 공개된 미해결 항목입니다. 이 범위를 넘어서는 신고는 환영합니다. + +- `kmsd`는 TLS 개인키를 프로세스 메모리에 둡니다. 신원, KEM, 감사, 대칭 키는 코어 밖으로 나오지 않지만 전송용 키는 예외입니다. +- P-256 + ML-KEM-768 하이브리드 KEM은 두 공유 비밀을 pairier 고유의 HKDF 구성으로 결합합니다. 표준화된 결합 방식이 아닙니다. +- `export` 권한과 함께 키를 받은 구성원은 그 키를 다른 구성원에게 다시 보낼 수 있습니다. 재공유를 막으려면 `export`를 주지 마세요. +- 감사 로그는 항목마다 코어의 감사 키로 서명한 해시 체인입니다. 노드 사용자 권한으로 코어를 기동할 수 있으면 유효한 항목을 만들 수 있고, 로그 끝부분이 잘리는 것은 외부 기준점 없이는 탐지할 수 없습니다. +- macOS에서는 코어가 메모리 잠금 없이 실행되고 FIPS 코어도 정적 링크되지 않습니다. macOS는 개발용입니다. +- `make audit`는 로컬 RustSec 권고 데이터베이스를 사용하며, 네트워크가 연결된 장비에서 갱신해야 합니다. +- Rust 와이어 디코더 변이 테스트는 시드 기반이며 커버리지 유도 방식이 아닙니다. +- Linux x86_64는 CI에서만 검증됩니다. 오프라인 검증은 Linux arm64에서 수행했습니다. + +## 릴리즈 검증 + +릴리즈 tarball은 [minisign](https://jedisct1.github.io/minisign/)으로 서명합니다. 서명 키는 오프라인에서 생성·보관하며 CI에서는 사용할 수 없습니다. + +- 공개키: 저장소 루트의 `minisign.pub` +- 키를 처음 신뢰하기 전에, 이 저장소가 아닌 다른 경로로 받은 사본과 키 문자열 전체를 비교하세요. + +```bash +$ minisign -Vm SHA256SUMS -p minisign.pub +$ sha256sum -c SHA256SUMS +``` + +릴리즈는 재현 가능합니다. 고정된 툴체인으로 Linux에서 `make dist FIPS=1`을 실행하면 바이트 단위로 동일한 tarball이 생성되므로, 서명 확인 대신 또는 서명 확인과 함께 직접 빌드해 `SHA256SUMS`를 비교할 수 있습니다. + +## 암호 + +- 코어: aws-lc-rs. `fips` feature로 AWS-LC FIPS 140-3 검증 모듈을 선택합니다 (Linux 정적 빌드). +- 호스트: Go 표준 라이브러리만 사용하며 기본적으로 `GOFIPS140=certified`로 빌드합니다. +- 알고리즘: AES-256-GCM, ECDSA P-256/P-384, Ed25519, RFC 9180 DHKEM(P-256), ML-DSA-65/87, ML-KEM-768/1024, P-256 + ML-KEM-768 하이브리드 diff --git a/USAGE.md b/USAGE.md index da77b1d..4c474f7 100644 --- a/USAGE.md +++ b/USAGE.md @@ -52,9 +52,10 @@ kmsctl 1.0.0 (go go1.27.2, go_fips=true) ## Installation -The distribution tarball contains `kms-core`, `kmsd`, `kmsctl`, `VERSION` and `sbom.cdx.json`. Verify the checksums before extracting it. +The distribution tarball contains `kms-core`, `kmsd`, `kmsctl`, `VERSION`, `sbom.cdx.json`, `LICENSE` and `NOTICE`. Before extracting it, verify the minisign signature on `SHA256SUMS` and then the checksums. The public key is `minisign.pub` in the repository root; see [SECURITY.md](SECURITY.md) for how to check it. ```bash +$ minisign -Vm SHA256SUMS -p minisign.pub $ sha256sum -c SHA256SUMS $ tar -xzf pairier-1.0.0-linux-amd64.tar.gz ``` diff --git a/USAGE_KR.md b/USAGE_KR.md index 645931a..10dd4e5 100644 --- a/USAGE_KR.md +++ b/USAGE_KR.md @@ -52,9 +52,10 @@ kmsctl 1.0.0 (go go1.27.2, go_fips=true) ## 설치 -배포 tarball에는 `kms-core`, `kmsd`, `kmsctl`, `VERSION`, `sbom.cdx.json`이 들어 있습니다. 해제 전 체크섬(checksum)을 확인하세요. +배포 tarball에는 `kms-core`, `kmsd`, `kmsctl`, `VERSION`, `sbom.cdx.json`, `LICENSE`, `NOTICE`가 들어 있습니다. 해제 전에 `SHA256SUMS`의 minisign 서명(signature)을 먼저 확인하고, 그다음 체크섬(checksum)을 확인하세요. 공개키는 저장소 루트의 `minisign.pub`이며, 확인 방법은 [SECURITY_KR.md](SECURITY_KR.md)를 참고하세요. ```bash +$ minisign -Vm SHA256SUMS -p minisign.pub $ sha256sum -c SHA256SUMS $ tar -xzf pairier-1.0.0-linux-amd64.tar.gz ``` From dbeec4a8da921d95aa3db9c6d31608796a6d46ce Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 15 Oct 2026 01:17:31 +0900 Subject: [PATCH 40/48] =?UTF-8?q?=EB=A6=B4=EB=A6=AC=EC=A6=88=20=EC=84=9C?= =?UTF-8?q?=EB=AA=85=20=EA=B3=B5=EA=B0=9C=ED=82=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - minisign 공개키를 저장소 루트에 두고 SECURITY 문서에 키 id와 키 문자열 기재 minisign.pub, SECURITY.md, SECURITY_KR.md --- SECURITY.md | 5 +++++ SECURITY_KR.md | 5 +++++ minisign.pub | 2 ++ 3 files changed, 12 insertions(+) create mode 100644 minisign.pub diff --git a/SECURITY.md b/SECURITY.md index d73d83c..dd7f2ef 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -75,13 +75,18 @@ These are design choices or open items that are disclosed here rather than treat Release tarballs are signed with [minisign](https://jedisct1.github.io/minisign/). The signing key is generated and kept offline and is never available to CI. - Public key: `minisign.pub` in the repository root +- Key id: `5AE4F2AC05D840CA` +- Key: `RWTKQNgFrPLkWgNMj67g1XXR7A7IWAfeXYtnGZ1At0+QFEoUUqFZg7pP` - Before trusting the key for the first time, compare the full key string with a copy obtained through a channel other than this repository. ```bash $ minisign -Vm SHA256SUMS -p minisign.pub +$ minisign -Vm SHA256SUMS -P RWTKQNgFrPLkWgNMj67g1XXR7A7IWAfeXYtnGZ1At0+QFEoUUqFZg7pP $ sha256sum -c SHA256SUMS ``` +Either form works. The first uses the key file, the second the key string itself. + Releases are also reproducible. `make dist FIPS=1` on Linux with the pinned toolchains produces a byte-identical tarball, so you can rebuild and compare `SHA256SUMS` instead of, or in addition to, checking the signature. ## Cryptography diff --git a/SECURITY_KR.md b/SECURITY_KR.md index 63e48be..efa40b0 100644 --- a/SECURITY_KR.md +++ b/SECURITY_KR.md @@ -75,13 +75,18 @@ pairier는 "Abandon trust. Choose isolation." 원칙으로 설계되었습니다 릴리즈 tarball은 [minisign](https://jedisct1.github.io/minisign/)으로 서명합니다. 서명 키는 오프라인에서 생성·보관하며 CI에서는 사용할 수 없습니다. - 공개키: 저장소 루트의 `minisign.pub` +- 키 id: `5AE4F2AC05D840CA` +- 키: `RWTKQNgFrPLkWgNMj67g1XXR7A7IWAfeXYtnGZ1At0+QFEoUUqFZg7pP` - 키를 처음 신뢰하기 전에, 이 저장소가 아닌 다른 경로로 받은 사본과 키 문자열 전체를 비교하세요. ```bash $ minisign -Vm SHA256SUMS -p minisign.pub +$ minisign -Vm SHA256SUMS -P RWTKQNgFrPLkWgNMj67g1XXR7A7IWAfeXYtnGZ1At0+QFEoUUqFZg7pP $ sha256sum -c SHA256SUMS ``` +두 방식 모두 동일하게 동작합니다. 첫 번째는 키 파일을, 두 번째는 키 문자열 자체를 사용합니다. + 릴리즈는 재현 가능합니다. 고정된 툴체인으로 Linux에서 `make dist FIPS=1`을 실행하면 바이트 단위로 동일한 tarball이 생성되므로, 서명 확인 대신 또는 서명 확인과 함께 직접 빌드해 `SHA256SUMS`를 비교할 수 있습니다. ## 암호 diff --git a/minisign.pub b/minisign.pub new file mode 100644 index 0000000..ff21b57 --- /dev/null +++ b/minisign.pub @@ -0,0 +1,2 @@ +untrusted comment: minisign public key 5AE4F2AC05D840CA +RWTKQNgFrPLkWgNMj67g1XXR7A7IWAfeXYtnGZ1At0+QFEoUUqFZg7pP From 74f34bd231b6d1eccc482bc597758f856322df59 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 15 Oct 2026 02:18:31 +0900 Subject: [PATCH 41/48] =?UTF-8?q?=EB=AF=B8=EC=9D=B8=EC=A6=9D=20=ED=95=B8?= =?UTF-8?q?=EB=93=9C=EC=85=B0=EC=9D=B4=ED=81=AC=EC=9D=98=20=EC=BD=94?= =?UTF-8?q?=EC=96=B4=20=ED=98=B8=EC=B6=9C=20=EC=A6=9D=ED=8F=AD=20=EC=B0=A8?= =?UTF-8?q?=EB=8B=A8=20-=20=ED=94=BC=EC=96=B4=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=EB=A5=BC=20=EC=9D=B8=EC=A6=9D=EC=84=9C=20CN=EA=B3=BC=20SPKI?= =?UTF-8?q?=EB=A1=9C=20=ED=95=9C=20=EB=B2=88=EC=97=90=20=EC=B0=BE=EA=B3=A0?= =?UTF-8?q?=20=EC=9D=BC=EC=B9=98=ED=95=A0=20=EB=95=8C=EB=A7=8C=20=EC=BD=94?= =?UTF-8?q?=EC=96=B4=20=EA=B2=80=EC=A6=9D=20-=20kmsd=20=EB=8F=99=EC=8B=9C?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0=20=EC=88=98=20=EC=A0=9C=ED=95=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- host/cmd/kmsd/main.go | 46 +++++++++++++++++++++++++- host/cmd/kmsd/main_test.go | 58 +++++++++++++++++++++++++++++++++ host/internal/auth/auth.go | 6 ++-- host/internal/node/node_test.go | 45 +++++++++++++++++++++---- host/internal/node/tls.go | 32 +++++++++++------- 5 files changed, 165 insertions(+), 22 deletions(-) diff --git a/host/cmd/kmsd/main.go b/host/cmd/kmsd/main.go index e8b84b4..9fe22fe 100644 --- a/host/cmd/kmsd/main.go +++ b/host/cmd/kmsd/main.go @@ -13,6 +13,7 @@ import ( "os/signal" "path/filepath" "runtime" + "sync" "syscall" "time" @@ -26,8 +27,51 @@ var version = "dev" const ( defaultListen = "127.0.0.1:7443" defaultTimeout = 10 * time.Second + maxConns = 64 ) +type limitListener struct { + net.Listener + sem chan struct{} + done chan struct{} + once sync.Once +} + +func limit(ln net.Listener, n int) net.Listener { + return &limitListener{Listener: ln, sem: make(chan struct{}, n), done: make(chan struct{})} +} + +func (l *limitListener) Accept() (net.Conn, error) { + select { + case l.sem <- struct{}{}: + case <-l.done: + return nil, net.ErrClosed + } + c, err := l.Listener.Accept() + if err != nil { + <-l.sem + return nil, err + } + return &limitConn{Conn: c, release: func() { <-l.sem }}, nil +} + +func (l *limitListener) Close() error { + l.once.Do(func() { close(l.done) }) + return l.Listener.Close() +} + +type limitConn struct { + net.Conn + once sync.Once + release func() +} + +func (c *limitConn) Close() error { + err := c.Conn.Close() + c.once.Do(c.release) + return err +} + type settings struct { node node.Options listen string @@ -151,7 +195,7 @@ func serve(ctx context.Context, s settings, stderr io.Writer, ready func(addr st ready(ln.Addr().String()) } errc := make(chan error, 1) - go func() { errc <- hs.ServeTLS(ln, "", "") }() + go func() { errc <- hs.ServeTLS(limit(ln, maxConns), "", "") }() select { case err := <-errc: if errors.Is(err, http.ErrServerClosed) { diff --git a/host/cmd/kmsd/main_test.go b/host/cmd/kmsd/main_test.go index 689ef06..287781c 100644 --- a/host/cmd/kmsd/main_test.go +++ b/host/cmd/kmsd/main_test.go @@ -3,6 +3,7 @@ package main import ( "bytes" "context" + "net" "os" "path/filepath" "strings" @@ -142,3 +143,60 @@ func TestServeAndShutdown(t *testing.T) { t.Fatalf("inbox %+v %v", items, err) } } + +func TestLimitListener(t *testing.T) { + raw, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + ln := limit(raw, 1) + defer ln.Close() + accepted := make(chan net.Conn, 2) + go func() { + for { + c, err := ln.Accept() + if err != nil { + close(accepted) + return + } + accepted <- c + } + }() + dial := func() net.Conn { + c, err := net.Dial("tcp", raw.Addr().String()) + if err != nil { + t.Fatal(err) + } + return c + } + c1, c2 := dial(), dial() + defer c1.Close() + defer c2.Close() + first := <-accepted + select { + case <-accepted: + t.Fatal("second connection accepted beyond the limit") + case <-time.After(200 * time.Millisecond): + } + if err := first.Close(); err != nil { + t.Fatal(err) + } + _ = first.Close() + select { + case second := <-accepted: + _ = second.Close() + case <-time.After(5 * time.Second): + t.Fatal("slot was not released") + } + if err := ln.Close(); err != nil { + t.Fatal(err) + } + select { + case _, ok := <-accepted: + if ok { + t.Fatal("accept after close") + } + case <-time.After(5 * time.Second): + t.Fatal("accept did not stop after close") + } +} diff --git a/host/internal/auth/auth.go b/host/internal/auth/auth.go index 276d256..0dfaccf 100644 --- a/host/internal/auth/auth.go +++ b/host/internal/auth/auth.go @@ -18,7 +18,7 @@ var ( ) type Resolver interface { - MemberByTLSKey(ctx context.Context, spki []byte) (*exchange.Card, error) + MemberByTLS(ctx context.Context, memberID string, spki []byte) (*exchange.Card, error) } func leaf(raw [][]byte, now time.Time) (*x509.Certificate, error) { @@ -46,7 +46,7 @@ func Peer(ctx context.Context, r Resolver, cs *tls.ConnectionState, now time.Tim if err != nil { return nil, err } - card, err := r.MemberByTLSKey(ctx, c.RawSubjectPublicKeyInfo) + card, err := r.MemberByTLS(ctx, c.Subject.CommonName, c.RawSubjectPublicKeyInfo) if err != nil { return nil, fmt.Errorf("%w: %v", ErrUnknownPeer, err) } @@ -68,7 +68,7 @@ func ServerTLSConfig(cert tls.Certificate, r Resolver, now func() time.Time) *tl } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - card, err := r.MemberByTLSKey(ctx, c.RawSubjectPublicKeyInfo) + card, err := r.MemberByTLS(ctx, c.Subject.CommonName, c.RawSubjectPublicKeyInfo) if err != nil { return fmt.Errorf("%w: %v", ErrUnknownPeer, err) } diff --git a/host/internal/node/node_test.go b/host/internal/node/node_test.go index ed9992e..bff06f1 100644 --- a/host/internal/node/node_test.go +++ b/host/internal/node/node_test.go @@ -412,6 +412,31 @@ func TestPQCNodes(t *testing.T) { } } +func TestUnknownPeerNeedsNoCoreCall(t *testing.T) { + ctx := context.Background() + alice, _ := initNode(t, "alice", false) + bob, _ := initNode(t, "bob", false) + pair(t, alice, bob) + if err := alice.core.Close(); err != nil { + t.Fatal(err) + } + for _, c := range []struct { + id string + spki []byte + }{ + {"carol", bob.TLSPublicKey()}, + {"bob", alice.TLSPublicKey()}, + {"bob", []byte("stranger")}, + } { + if _, err := alice.MemberByTLS(ctx, c.id, c.spki); !errors.Is(err, ErrUnknownMember) { + t.Fatalf("%s: unknown peer reached the core: %v", c.id, err) + } + } + if _, err := alice.MemberByTLS(ctx, "bob", bob.TLSPublicKey()); err == nil || errors.Is(err, ErrUnknownMember) { + t.Fatalf("known peer must still be verified by the core: %v", err) + } +} + func TestTLSMaterialAndPeerLookup(t *testing.T) { ctx := context.Background() alice, aliceOpts := initNode(t, "alice", false) @@ -424,15 +449,23 @@ func TestTLSMaterialAndPeerLookup(t *testing.T) { t.Fatalf("card tls key mismatch: %v", err) } pair(t, alice, bob) - found, err := alice.MemberByTLSKey(ctx, bob.TLSPublicKey()) + found, err := alice.MemberByTLS(ctx, "bob", bob.TLSPublicKey()) if err != nil || found.MemberID != "bob" { t.Fatalf("lookup: %+v %v", found, err) } - if _, err := alice.MemberByTLSKey(ctx, alice.TLSPublicKey()); !errors.Is(err, ErrUnknownMember) { - t.Fatalf("own key resolved to a member: %v", err) - } - if _, err := alice.MemberByTLSKey(ctx, nil); !errors.Is(err, ErrUnknownMember) { - t.Fatalf("empty key resolved: %v", err) + for _, c := range []struct { + id string + spki []byte + }{ + {"alice", alice.TLSPublicKey()}, + {"bob", alice.TLSPublicKey()}, + {"carol", bob.TLSPublicKey()}, + {"../bob", bob.TLSPublicKey()}, + {"bob", nil}, + } { + if _, err := alice.MemberByTLS(ctx, c.id, c.spki); !errors.Is(err, ErrUnknownMember) { + t.Fatalf("%s resolved: %v", c.id, err) + } } spki := alice.TLSPublicKey() if err := alice.Close(); err != nil { diff --git a/host/internal/node/tls.go b/host/internal/node/tls.go index cf94aaa..55779be 100644 --- a/host/internal/node/tls.go +++ b/host/internal/node/tls.go @@ -110,22 +110,30 @@ func (n *Node) TLSPublicKey() []byte { return n.tls.Leaf.RawSubjectPublicKeyInfo } -func (n *Node) MemberByTLSKey(ctx context.Context, spki []byte) (*exchange.Card, error) { - if len(spki) == 0 { +func (n *Node) MemberByTLS(ctx context.Context, memberID string, spki []byte) (*exchange.Card, error) { + if len(spki) == 0 || !store.ValidMemberID(memberID) || memberID == n.info.MemberID { return nil, ErrUnknownMember } - members, err := n.Members() + rec, err := n.st.GetMember(memberID) if err != nil { + if errors.Is(err, store.ErrNotFound) || errors.Is(err, store.ErrBadID) { + return nil, ErrUnknownMember + } return nil, err } - for _, m := range members { - card, err := n.Member(ctx, m.MemberID) - if err != nil { - return nil, err - } - if bytes.Equal(card.TLSPublicKey, spki) { - return card, nil - } + claimed, err := exchange.DecodeCard(rec.Card) + if err != nil { + return nil, err + } + if claimed.MemberID != memberID || !bytes.Equal(claimed.TLSPublicKey, spki) { + return nil, ErrUnknownMember + } + card, err := n.Member(ctx, memberID) + if err != nil { + return nil, err + } + if !bytes.Equal(card.TLSPublicKey, spki) { + return nil, ErrUnknownMember } - return nil, ErrUnknownMember + return card, nil } From 8e60fe28928aa1b255ddb234af9b6926f46b377c Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 15 Oct 2026 02:19:07 +0900 Subject: [PATCH 42/48] =?UTF-8?q?=EB=B0=9B=EC=9D=80=20=ED=8E=B8=EC=A7=80?= =?UTF-8?q?=ED=95=A8=20=EB=B0=9C=EC=8B=A0=EC=9E=90=EB=B3=84=20=EB=8C=80?= =?UTF-8?q?=EA=B8=B0=20=EC=83=81=ED=95=9C=20=EC=B6=94=EA=B0=80=20-=20?= =?UTF-8?q?=EB=B0=9C=EC=8B=A0=EC=9E=90=EB=8B=B9=20=EB=8C=80=EA=B8=B0=20?= =?UTF-8?q?=EB=B4=89=ED=88=AC=2064=EA=B0=9C=20=EC=B4=88=EA=B3=BC=20?= =?UTF-8?q?=EC=8B=9C=20=EA=B1=B0=EB=B6=80=ED=95=98=EA=B3=A0=20API=EB=8A=94?= =?UTF-8?q?=20429=20=EC=9D=91=EB=8B=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- host/internal/api/api.go | 2 ++ host/internal/node/node.go | 20 ++++++++++++++++++ host/internal/node/node_test.go | 37 +++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/host/internal/api/api.go b/host/internal/api/api.go index 4663e86..2060003 100644 --- a/host/internal/api/api.go +++ b/host/internal/api/api.go @@ -120,6 +120,8 @@ func (s *Server) inbox(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusCreated, map[string]string{"id": rec.ID}) case errors.Is(err, store.ErrExists): fail(w, http.StatusConflict, "duplicate envelope") + case errors.Is(err, node.ErrInboxFull): + fail(w, http.StatusTooManyRequests, "inbox full for this sender") case errors.Is(err, node.ErrNotRecipient), errors.Is(err, node.ErrSenderMismatch), errors.Is(err, node.ErrUnknownMember), errors.Is(err, exchange.ErrFormat), errors.Is(err, exchange.ErrSignature), errors.Is(err, exchange.ErrBadMemberID), errors.Is(err, exchange.ErrAlgorithm), diff --git a/host/internal/node/node.go b/host/internal/node/node.go index 67140aa..e9f8640 100644 --- a/host/internal/node/node.go +++ b/host/internal/node/node.go @@ -11,6 +11,7 @@ import ( "os/exec" "path/filepath" "strings" + "sync" "time" "github.com/Quant-Off/pairier/host/internal/audit" @@ -32,6 +33,7 @@ var ( ErrSignatureAlg = errors.New("node: envelope signature algorithm differs from the member card") ErrUsageNotAllowed = errors.New("node: usage not allowed for this key kind") ErrSenderMismatch = errors.New("node: envelope sender differs from the authenticated peer") + ErrInboxFull = errors.New("node: too many pending envelopes from this sender") ) const auditTimeout = 10 * time.Second @@ -47,7 +49,10 @@ type Options struct { Now func() time.Time } +const MaxPendingPerSender = 64 + type Node struct { + inboxMu sync.Mutex opts Options st *store.Store core *core.Client @@ -582,6 +587,21 @@ func (n *Node) storeInbox(ctx context.Context, raw []byte, from string) (*store. if ectx.Sender != from { return nil, ErrSenderMismatch } + n.inboxMu.Lock() + defer n.inboxMu.Unlock() + items, err := n.Inbox() + if err != nil { + return nil, err + } + pending := 0 + for _, it := range items { + if it.Sender == from { + pending++ + } + } + if pending >= MaxPendingPerSender { + return nil, ErrInboxFull + } rec := &store.InboxRecord{ID: store.InboxID(raw), Envelope: raw, ReceivedAt: n.opts.now().Unix()} if err := n.st.PutInbox(rec); err != nil { return nil, err diff --git a/host/internal/node/node_test.go b/host/internal/node/node_test.go index bff06f1..bea0967 100644 --- a/host/internal/node/node_test.go +++ b/host/internal/node/node_test.go @@ -487,3 +487,40 @@ func TestTLSMaterialAndPeerLookup(t *testing.T) { t.Fatalf("expected identity, kem, audit and tls-wrap keys, got %d", len(keys)) } } + +func TestInboxPendingLimitPerSender(t *testing.T) { + ctx := context.Background() + alice, _ := initNode(t, "alice", false) + bob, _ := initNode(t, "bob", false) + pair(t, alice, bob) + k, err := alice.GenerateKey(ctx, "k", wire.UsageDecrypt|wire.UsageExport) + if err != nil { + t.Fatal(err) + } + send := func() []byte { + env, err := alice.Send(ctx, k.KeyID, "bob", wire.UsageDecrypt) + if err != nil { + t.Fatal(err) + } + return env + } + var first string + for i := 0; i < MaxPendingPerSender; i++ { + rec, err := bob.StoreInbox(ctx, send(), "alice") + if err != nil { + t.Fatalf("envelope %d: %v", i, err) + } + if first == "" { + first = rec.ID + } + } + if _, err := bob.StoreInbox(ctx, send(), "alice"); !errors.Is(err, ErrInboxFull) { + t.Fatalf("limit not enforced: %v", err) + } + if err := bob.Reject(ctx, first); err != nil { + t.Fatal(err) + } + if _, err := bob.StoreInbox(ctx, send(), "alice"); err != nil { + t.Fatalf("slot not freed after reject: %v", err) + } +} From 0b87edc0d7ca14725d441d15fa735f2c5ea20379 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 15 Oct 2026 02:20:47 +0900 Subject: [PATCH 43/48] =?UTF-8?q?=EA=B0=90=EC=82=AC=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=20=EA=B8=B0=EC=A4=80=EC=A0=90=20=EA=B2=80=EC=A6=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20audit=20verify=EA=B0=80=20head=EB=A5=BC=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5=ED=95=98=EA=B3=A0=20--anchor=EB=A1=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=A1=9D=ED=95=B4=20=EB=91=94=20head=20=ED=8F=AC?= =?UTF-8?q?=ED=95=A8=20=EC=97=AC=EB=B6=80=20=EA=B2=80=EC=82=AC=20-=20?= =?UTF-8?q?=EB=81=9D=EB=B6=80=EB=B6=84=20=EC=9E=98=EB=9D=BC=EB=82=B4?= =?UTF-8?q?=EA=B8=B0=EC=99=80=20=EB=90=98=EB=8F=8C=EB=A6=B0=20=EB=92=A4=20?= =?UTF-8?q?=EC=9D=B4=EC=96=B4=20=EC=93=B4=20=EB=A1=9C=EA=B7=B8=EB=A5=BC=20?= =?UTF-8?q?=ED=83=90=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- host/cmd/kmsctl/main.go | 17 +++++-- host/cmd/kmsctl/main_test.go | 30 +++++++++++-- host/internal/audit/audit.go | 50 +++++++++++++++++++-- host/internal/audit/audit_test.go | 73 +++++++++++++++++++++++++++++++ host/internal/node/node.go | 4 +- host/internal/node/node_test.go | 4 +- 6 files changed, 164 insertions(+), 14 deletions(-) diff --git a/host/cmd/kmsctl/main.go b/host/cmd/kmsctl/main.go index 303f747..d57e7d8 100644 --- a/host/cmd/kmsctl/main.go +++ b/host/cmd/kmsctl/main.go @@ -14,6 +14,7 @@ import ( "time" "github.com/Quant-Off/pairier/host/internal/api" + "github.com/Quant-Off/pairier/host/internal/audit" "github.com/Quant-Off/pairier/host/internal/node" "github.com/Quant-Off/pairier/host/internal/wire" ) @@ -37,7 +38,7 @@ commands: inbox accept --id ENVELOPEID inbox reject --id ENVELOPEID peer health --member MEMBER --url https://HOST:PORT - audit verify + audit verify [--anchor SEQ:HASH] encrypt --id KEYID [--in FILE] [--out FILE] [--aad STRING] decrypt --id KEYID [--in FILE] [--out FILE] [--aad STRING] version @@ -577,19 +578,29 @@ func (c *cli) cmdPeerHealth(ctx context.Context, args []string) (int, error) { func (c *cli) cmdAuditVerify(ctx context.Context, args []string) (int, error) { fs := c.flags("audit verify") + anchorText := fs.String("anchor", "", "head recorded out of band (SEQ:HASH)") if err := fs.Parse(args); err != nil || fs.NArg() != 0 { - return 2, errors.New("audit verify: no arguments expected") + return 2, errors.New("audit verify: usage: audit verify [--anchor SEQ:HASH]") + } + var anchor *audit.Anchor + if *anchorText != "" { + a, err := audit.ParseAnchor(*anchorText) + if err != nil { + return 2, err + } + anchor = &a } n, err := c.open(ctx) if err != nil { return 1, err } defer n.Close() - count, err := n.VerifyAudit() + count, head, err := n.VerifyAudit(anchor) if err != nil { return 1, err } fmt.Fprintf(c.stdout, "verified\t%d\tentries\n", count) + fmt.Fprintf(c.stdout, "head\t%s\n", head) return 0, nil } diff --git a/host/cmd/kmsctl/main_test.go b/host/cmd/kmsctl/main_test.go index 56382f0..9c348d8 100644 --- a/host/cmd/kmsctl/main_test.go +++ b/host/cmd/kmsctl/main_test.go @@ -192,9 +192,31 @@ func TestEndToEndExchange(t *testing.T) { if list := must(nil, "--dir", bob, "key", "list"); strings.Count(list, "\n") != 5 { t.Fatalf("bob key list %q", list) } - if out := must(nil, "--dir", bob, "audit", "verify"); !strings.HasPrefix(out, "verified\t") { + out := must(nil, "--dir", bob, "audit", "verify") + if !strings.HasPrefix(out, "verified\t") { t.Fatalf("audit verify %q", out) } + anchor := field(out, "head") + must(nil, "--dir", bob, "key", "list") + must(nil, "--dir", bob, "audit", "verify", "--anchor", anchor) + if _, _, code := exec(nil, "--dir", bob, "audit", "verify", "--anchor", "1:zz"); code != 2 { + t.Fatalf("malformed anchor exit %d", code) + } + logPath := filepath.Join(bob, "audit.log") + raw, err := os.ReadFile(logPath) + if err != nil { + t.Fatal(err) + } + lines := strings.SplitAfter(string(raw), "\n") + if err := os.WriteFile(logPath, []byte(strings.Join(lines[:3], "")), 0o600); err != nil { + t.Fatal(err) + } + if _, errOut, code := exec(nil, "--dir", bob, "audit", "verify", "--anchor", anchor); code == 0 || !strings.Contains(errOut, "anchored entry") { + t.Fatalf("truncated log passed the anchor: %d %s", code, errOut) + } + if err := os.WriteFile(logPath, raw, 0o600); err != nil { + t.Fatal(err) + } abs, err := filepath.Abs(bin) if err != nil { t.Fatal(err) @@ -207,8 +229,8 @@ func TestEndToEndExchange(t *testing.T) { if err != nil { t.Fatal(err) } - var out, errb syncBuffer - if code := run([]string{"audit", "verify", "--dir", bob, "--core", rel, "--allow-unlocked-memory"}, nil, &out, &errb); code != 0 || !strings.HasPrefix(out.String(), "verified\t") { - t.Fatalf("global flags after command with relative core: %d %s%s", code, out.String(), errb.String()) + var vout, errb syncBuffer + if code := run([]string{"audit", "verify", "--dir", bob, "--core", rel, "--allow-unlocked-memory"}, nil, &vout, &errb); code != 0 || !strings.HasPrefix(vout.String(), "verified\t") { + t.Fatalf("global flags after command with relative core: %d %s%s", code, vout.String(), errb.String()) } } diff --git a/host/internal/audit/audit.go b/host/internal/audit/audit.go index d61d318..4b4a923 100644 --- a/host/internal/audit/audit.go +++ b/host/internal/audit/audit.go @@ -13,6 +13,8 @@ import ( "io/fs" "os" "sort" + "strconv" + "strings" "sync" "syscall" "time" @@ -31,8 +33,32 @@ var ( ErrInsecure = errors.New("audit: insecure log file") ErrField = errors.New("audit: invalid event or field") ErrSignature = errors.New("audit: checkpoint signature is invalid") + ErrAnchor = errors.New("audit: invalid anchor") + ErrRollback = errors.New("audit: log does not contain the anchored entry") ) +type Anchor struct { + Seq uint64 + Hash string +} + +func (a Anchor) String() string { return strconv.FormatUint(a.Seq, 10) + ":" + a.Hash } + +func ParseAnchor(s string) (Anchor, error) { + seqText, hash, ok := strings.Cut(s, ":") + if !ok { + return Anchor{}, ErrAnchor + } + seq, err := strconv.ParseUint(seqText, 10, 64) + if err != nil || seq == 0 || seqText != strconv.FormatUint(seq, 10) { + return Anchor{}, ErrAnchor + } + if h, err := hex.DecodeString(hash); err != nil || len(h) != hashLen || hash != hex.EncodeToString(h) { + return Anchor{}, ErrAnchor + } + return Anchor{Seq: seq, Hash: hash}, nil +} + const checkpointDomain = "pairier/audit/checkpoint/v1" type Signer interface { @@ -324,18 +350,36 @@ func replayFrom(r io.Reader, seq uint64, prev string, requireSig bool) (uint64, } func Verify(path string, v Verifier) (uint64, error) { + n, _, err := VerifyAnchored(path, v, nil) + return n, err +} + +func VerifyAnchored(path string, v Verifier, anchor *Anchor) (uint64, Anchor, error) { entries, err := Read(path) if err != nil { - return 0, err + return 0, Anchor{}, err } if v != nil { for i := range entries { if err := verifyEntry(v, &entries[i]); err != nil { - return 0, err + return 0, Anchor{}, err } } } - return uint64(len(entries)), nil + head := Anchor{Seq: 0, Hash: genesis} + if len(entries) > 0 { + last := entries[len(entries)-1] + head = Anchor{Seq: last.Seq, Hash: last.Hash} + } + if anchor != nil { + if anchor.Seq == 0 || anchor.Seq > uint64(len(entries)) { + return 0, Anchor{}, fmt.Errorf("%w: log ends at entry %d before anchored entry %d", ErrRollback, head.Seq, anchor.Seq) + } + if e := entries[anchor.Seq-1]; e.Seq != anchor.Seq || e.Hash != anchor.Hash { + return 0, Anchor{}, fmt.Errorf("%w: entry %d differs from the anchor", ErrRollback, anchor.Seq) + } + } + return uint64(len(entries)), head, nil } func Read(path string) ([]Entry, error) { diff --git a/host/internal/audit/audit_test.go b/host/internal/audit/audit_test.go index 7c9144c..664384d 100644 --- a/host/internal/audit/audit_test.go +++ b/host/internal/audit/audit_test.go @@ -229,3 +229,76 @@ func TestSignedCheckpoints(t *testing.T) { } _ = l2.Close() } + +func TestAnchorDetectsTruncationAndRollback(t *testing.T) { + path := filepath.Join(t.TempDir(), "audit.log") + now := func() time.Time { return time.Unix(1_700_000_000, 0) } + l, err := Open(path, Options{Now: now}) + if err != nil { + t.Fatal(err) + } + for i := 0; i < 5; i++ { + if err := l.Append("key.send", map[string]string{"n": string(rune('a' + i))}); err != nil { + t.Fatal(err) + } + } + _ = l.Close() + n, head, err := VerifyAnchored(path, nil, nil) + if err != nil || n != 5 || head.Seq != 5 { + t.Fatalf("head %v %d %v", head, n, err) + } + parsed, err := ParseAnchor(head.String()) + if err != nil || parsed != head { + t.Fatalf("round trip %v %v", parsed, err) + } + if _, _, err := VerifyAnchored(path, nil, &head); err != nil { + t.Fatal(err) + } + entries, _ := Read(path) + mid := Anchor{Seq: 3, Hash: entries[2].Hash} + + raw, _ := os.ReadFile(path) + lines := strings.SplitAfter(string(raw), "\n") + if err := os.WriteFile(path, []byte(strings.Join(lines[:3], "")), 0o600); err != nil { + t.Fatal(err) + } + if _, err := Verify(path, nil); err != nil { + t.Fatalf("truncated prefix is still a valid chain: %v", err) + } + if _, _, err := VerifyAnchored(path, nil, &head); !errors.Is(err, ErrRollback) { + t.Fatalf("truncation not detected: %v", err) + } + if _, _, err := VerifyAnchored(path, nil, &mid); err != nil { + t.Fatalf("older anchor rejected: %v", err) + } + wrong := Anchor{Seq: 3, Hash: entries[1].Hash} + if _, _, err := VerifyAnchored(path, nil, &wrong); !errors.Is(err, ErrRollback) { + t.Fatalf("wrong hash accepted: %v", err) + } + + l, err = Open(path, Options{Now: now}) + if err != nil { + t.Fatal(err) + } + for i := 0; i < 2; i++ { + if err := l.Append("key.receive", nil); err != nil { + t.Fatal(err) + } + } + _ = l.Close() + if _, _, err := VerifyAnchored(path, nil, &head); !errors.Is(err, ErrRollback) { + t.Fatalf("rollback followed by new entries not detected: %v", err) + } +} + +func TestParseAnchorRejectsMalformed(t *testing.T) { + h := strings.Repeat("ab", 32) + for _, s := range []string{"", "1", ":" + h, "0:" + h, "-1:" + h, "01:" + h, "1:" + strings.ToUpper(h), "1:" + h[:62], "1:" + h + "00", "x:" + h, "1:" + h + ":"} { + if _, err := ParseAnchor(s); !errors.Is(err, ErrAnchor) { + t.Fatalf("%q accepted: %v", s, err) + } + } + if _, err := ParseAnchor("7:" + h); err != nil { + t.Fatal(err) + } +} diff --git a/host/internal/node/node.go b/host/internal/node/node.go index e9f8640..7d51157 100644 --- a/host/internal/node/node.go +++ b/host/internal/node/node.go @@ -88,8 +88,8 @@ func (n *Node) openAudit() error { return nil } -func (n *Node) VerifyAudit() (uint64, error) { - return audit.Verify(n.st.AuditLogPath(), checkpoints{n: n}) +func (n *Node) VerifyAudit(anchor *audit.Anchor) (uint64, audit.Anchor, error) { + return audit.VerifyAnchored(n.st.AuditLogPath(), checkpoints{n: n}, anchor) } func (n *Node) audit(event string, fields map[string]string, err error) error { diff --git a/host/internal/node/node_test.go b/host/internal/node/node_test.go index bea0967..372e354 100644 --- a/host/internal/node/node_test.go +++ b/host/internal/node/node_test.go @@ -262,7 +262,7 @@ func TestExchangeBetweenNodes(t *testing.T) { if !failed { t.Fatal("failed send to unknown member not audited") } - if n, err := bob.VerifyAudit(); err != nil || n == 0 { + if n, _, err := bob.VerifyAudit(nil); err != nil || n == 0 { t.Fatalf("bob audit %d %v", n, err) } if _, err := audit.Verify(bob.AuditLogPath(), checkpoints{n: reopened}); !errors.Is(err, audit.ErrSignature) { @@ -276,7 +276,7 @@ func TestAuditLogCannotBeRewrittenWithoutTheCore(t *testing.T) { if _, err := alice.GenerateKey(ctx, "k", wire.UsageEncrypt); err != nil { t.Fatal(err) } - if n, err := alice.VerifyAudit(); err != nil || n != 2 { + if n, _, err := alice.VerifyAudit(nil); err != nil || n != 2 { t.Fatalf("verify %d %v", n, err) } if err := alice.Close(); err != nil { From 2c1af02ed2aec0919c8ed26d85fb8c96857cb6db Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 15 Oct 2026 02:22:10 +0900 Subject: [PATCH 44/48] =?UTF-8?q?=ED=82=A4=20=EB=9D=BC=EB=B2=A8=EC=9D=98?= =?UTF-8?q?=20=EC=A0=9C=EC=96=B4=20=EB=AC=B8=EC=9E=90=EC=99=80=20=EC=96=91?= =?UTF-8?q?=EB=B0=A9=ED=96=A5=20=EC=84=9C=EC=8B=9D=20=EB=AC=B8=EC=9E=90=20?= =?UTF-8?q?=EA=B1=B0=EB=B6=80=20-=20=EC=BD=94=EC=96=B4=EA=B0=80=20?= =?UTF-8?q?=ED=82=A4=20=EC=83=9D=EC=84=B1,=20=EB=B4=89=EC=9D=B8,=20export?= =?UTF-8?q?=EC=99=80=20import=EC=97=90=EC=84=9C=20=EB=9D=BC=EB=B2=A8=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC=20-=20=ED=98=B8=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=82=A4=20=EC=83=9D=EC=84=B1=EC=97=90=EC=84=9C=EB=8F=84=20?= =?UTF-8?q?=EB=AF=B8=EB=A6=AC=20=EA=B2=80=EC=82=AC=ED=95=B4=20=ED=84=B0?= =?UTF-8?q?=EB=AF=B8=EB=84=90=20=EC=B6=9C=EB=A0=A5=20=EC=9C=84=EC=A1=B0=20?= =?UTF-8?q?=EC=B0=A8=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/crates/kms-engine/src/blob.rs | 53 ++++++++++++++++++++++++++-- core/crates/kms-engine/src/export.rs | 40 +++++++++++++++++++-- core/crates/kms-engine/src/lib.rs | 6 ++-- host/internal/node/node.go | 20 +++++++++++ host/internal/node/node_test.go | 16 +++++++++ 5 files changed, 125 insertions(+), 10 deletions(-) diff --git a/core/crates/kms-engine/src/blob.rs b/core/crates/kms-engine/src/blob.rs index 76624a2..6f421ad 100644 --- a/core/crates/kms-engine/src/blob.rs +++ b/core/crates/kms-engine/src/blob.rs @@ -87,10 +87,23 @@ fn encode_header(h: &Header<'_>, salt: &[u8; SALT_LEN]) -> Result, Error Ok(w.finish()) } -pub(crate) fn seal(root: &RootKey, h: &Header<'_>, secret: &[u8]) -> Result, Error> { - if h.label.len() > MAX_LABEL { +pub(crate) fn check_label(label: &str) -> Result<(), Error> { + if label.len() > MAX_LABEL || label.chars().any(forbidden_in_label) { return Err(Error::InvalidArgument); } + Ok(()) +} + +fn forbidden_in_label(c: char) -> bool { + c.is_control() + || matches!( + c, + '\u{061C}' | '\u{200E}' | '\u{200F}' | '\u{202A}'..='\u{202E}' | '\u{2066}'..='\u{2069}' + ) +} + +pub(crate) fn seal(root: &RootKey, h: &Header<'_>, secret: &[u8]) -> Result, Error> { + check_label(h.label)?; let salt = kdf::random_salt()?; let aad = encode_header(h, &salt)?; let d = kdf::derive_aead(root.as_bytes(), &salt, &[INFO, &h.key_id])?; @@ -153,7 +166,7 @@ pub(crate) fn open(root: &RootKey, blob: &[u8]) -> Result { #[cfg(test)] #[allow(clippy::unwrap_used, clippy::indexing_slicing)] mod tests { - use super::{Header, open, seal}; + use super::{Header, check_label, open, seal}; use crate::alg::Alg; use crate::error::Error; use crate::root::RootKey; @@ -202,4 +215,38 @@ mod tests { ); assert_eq!(open(&root, &[]).unwrap_err(), Error::InvalidKeyBlob); } + + #[test] + fn labels_reject_control_and_bidi_characters() { + for ok in ["", "team/a", "프로젝트/db", "a b-c_d.e"] { + assert!(check_label(ok).is_ok(), "{ok:?}"); + } + for bad in [ + "a\nb", + "a\tb", + "\u{1b}[2J", + "a\u{7f}", + "a\u{85}", + "a\u{202e}b", + "\u{2066}x", + "a\u{200f}", + ] { + assert_eq!( + check_label(bad).unwrap_err(), + Error::InvalidArgument, + "{bad:?}" + ); + } + let root = RootKey::generate().unwrap(); + let h = Header { + alg: Alg::Aes256Gcm, + usage: Usage::from_bits(kms_wire::usage::ENCRYPT).unwrap(), + key_id: [2u8; 16], + label: "x\ny", + }; + assert_eq!( + seal(&root, &h, &[0u8; 32]).unwrap_err(), + Error::InvalidArgument + ); + } } diff --git a/core/crates/kms-engine/src/export.rs b/core/crates/kms-engine/src/export.rs index 03432c9..0a7b565 100644 --- a/core/crates/kms-engine/src/export.rs +++ b/core/crates/kms-engine/src/export.rs @@ -5,6 +5,7 @@ use kms_wire::{Reader, Writer}; use zeroize::Zeroizing; use crate::alg::{Alg, AlgKind}; +use crate::blob; use crate::error::Error; use crate::kdf::{self, SALT_LEN, TAG_LEN}; use crate::kem; @@ -68,6 +69,7 @@ fn parse_header<'a>(r: &mut Reader<'a>) -> Result, Error> { .read_array::() .map_err(|_| Error::InvalidArgument)?; let label = r.read_str(MAX_LABEL).map_err(|_| Error::InvalidArgument)?; + blob::check_label(label)?; let context = r .read_bytes(MAX_CONTEXT) .map_err(|_| Error::InvalidArgument)?; @@ -103,9 +105,7 @@ pub(crate) fn seal( if e.kem_alg.kind() != AlgKind::Kem { return Err(Error::InvalidArgument); } - if e.label.len() > MAX_LABEL { - return Err(Error::InvalidArgument); - } + blob::check_label(e.label)?; if e.context.len() > MAX_CONTEXT { return Err(Error::LimitExceeded); } @@ -173,3 +173,37 @@ pub(crate) fn open( secret: buf, }) } + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod tests { + use kms_wire::Reader; + + use super::{Export, encode_header, parse_header}; + use crate::alg::Alg; + use crate::error::Error; + use crate::usage::Usage; + + #[test] + fn imported_label_must_be_printable() { + let bad = Export { + kem_alg: Alg::DhkemP256, + alg: Alg::Aes256Gcm, + usage: Usage::from_bits(kms_wire::usage::DECRYPT).unwrap(), + key_id: [3u8; 16], + label: "a\u{1b}[2Jb", + context: b"ctx", + }; + let raw = encode_header(&bad, &[0u8; 32], &[4u8; 65]).unwrap(); + assert_eq!( + parse_header(&mut Reader::new(&raw)).err(), + Some(Error::InvalidArgument) + ); + let good = Export { + label: "a/b", + ..bad + }; + let raw = encode_header(&good, &[0u8; 32], &[4u8; 65]).unwrap(); + assert!(parse_header(&mut Reader::new(&raw)).is_ok()); + } +} diff --git a/core/crates/kms-engine/src/lib.rs b/core/crates/kms-engine/src/lib.rs index be0228c..19715a8 100644 --- a/core/crates/kms-engine/src/lib.rs +++ b/core/crates/kms-engine/src/lib.rs @@ -11,7 +11,7 @@ mod root; mod sign; mod usage; -use kms_wire::msg::{KEY_ID_LEN, MAX_LABEL, MAX_RANDOM}; +use kms_wire::msg::{KEY_ID_LEN, MAX_RANDOM}; use zeroize::Zeroizing; pub use alg::{Alg, AlgKind}; @@ -96,9 +96,7 @@ impl Engine { pub fn generate_key(&self, alg: Alg, usage: Usage, label: &str) -> Result { self.check_enabled(alg)?; usage.check_for(alg)?; - if label.len() > MAX_LABEL { - return Err(Error::InvalidArgument); - } + blob::check_label(label)?; let mut key_id = [0u8; KEY_ID_LEN]; aws_lc_rs::rand::fill(&mut key_id).map_err(|_| Error::Internal)?; diff --git a/host/internal/node/node.go b/host/internal/node/node.go index 7d51157..7fee344 100644 --- a/host/internal/node/node.go +++ b/host/internal/node/node.go @@ -13,6 +13,8 @@ import ( "strings" "sync" "time" + "unicode" + "unicode/utf8" "github.com/Quant-Off/pairier/host/internal/audit" "github.com/Quant-Off/pairier/host/internal/core" @@ -34,6 +36,7 @@ var ( ErrUsageNotAllowed = errors.New("node: usage not allowed for this key kind") ErrSenderMismatch = errors.New("node: envelope sender differs from the authenticated peer") ErrInboxFull = errors.New("node: too many pending envelopes from this sender") + ErrLabel = errors.New("node: label contains control or bidirectional formatting characters") ) const auditTimeout = 10 * time.Second @@ -330,6 +333,20 @@ func (n *Node) Key(ctx context.Context, id string) (*store.KeyRecord, error) { func (n *Node) Keys() ([]*store.KeyRecord, error) { return n.st.ListKeys() } +func ValidLabel(label string) bool { + if !utf8.ValidString(label) { + return false + } + for _, r := range label { + switch { + case unicode.IsControl(r), r == 0x061c, r == 0x200e, r == 0x200f, + r >= 0x202a && r <= 0x202e, r >= 0x2066 && r <= 0x2069: + return false + } + } + return true +} + func (n *Node) GenerateKey(ctx context.Context, label string, usage uint32) (*store.KeyRecord, error) { if usage == 0 || usage&^SymmetricUsage != 0 { return nil, ErrUsageNotAllowed @@ -337,6 +354,9 @@ func (n *Node) GenerateKey(ctx context.Context, label string, usage uint32) (*st if len(label) > wire.MaxLabel { return nil, fmt.Errorf("node: label exceeds %d bytes", wire.MaxLabel) } + if !ValidLabel(label) { + return nil, ErrLabel + } k, err := n.core.GenerateKey(ctx, wire.AlgAES256GCM, usage, label) if err != nil { return nil, n.audit("key.generate", map[string]string{"label": label}, err) diff --git a/host/internal/node/node_test.go b/host/internal/node/node_test.go index 372e354..db04f9e 100644 --- a/host/internal/node/node_test.go +++ b/host/internal/node/node_test.go @@ -524,3 +524,19 @@ func TestInboxPendingLimitPerSender(t *testing.T) { t.Fatalf("slot not freed after reject: %v", err) } } + +func TestLabelsRejectControlCharacters(t *testing.T) { + ctx := context.Background() + alice, _ := initNode(t, "alice", false) + for _, l := range []string{"a\nb", "\x1b[2J", "a‮b", "⁦x", "a\x7f", "a\u0085", "\xff"} { + if _, err := alice.GenerateKey(ctx, l, wire.UsageEncrypt); !errors.Is(err, ErrLabel) { + t.Fatalf("%q accepted: %v", l, err) + } + } + if _, err := alice.GenerateKey(ctx, "프로젝트/db", wire.UsageEncrypt); err != nil { + t.Fatal(err) + } + if _, err := alice.core.GenerateKey(ctx, wire.AlgAES256GCM, wire.UsageEncrypt, "a\nb"); err == nil { + t.Fatal("core accepted a control character in a label") + } +} From 7cd7702d78d7df5d0c3d21f33511f919101c7b2e Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 15 Oct 2026 02:26:02 +0900 Subject: [PATCH 45/48] =?UTF-8?q?=EB=A6=B4=EB=A6=AC=EC=A6=88=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EA=B0=95?= =?UTF-8?q?=ED=99=94=20-=20SHA256SUMS=20=ED=98=95=EC=8B=9D=20=EA=B2=80?= =?UTF-8?q?=EC=82=AC=EC=99=80=20=EB=AA=A9=EB=A1=9D=EC=97=90=20=EC=97=86?= =?UTF-8?q?=EB=8A=94=20tarball,=20SBOM=20=EA=B1=B0=EB=B6=80=20-=20?= =?UTF-8?q?=EB=B2=84=EC=A0=84=20=EC=A7=80=EC=A0=95=20=EC=8B=9C=20=ED=95=B4?= =?UTF-8?q?=EB=8B=B9=20tarball=20=ED=8F=AC=ED=95=A8=20=EC=97=AC=EB=B6=80?= =?UTF-8?q?=20=ED=99=95=EC=9D=B8,=20=ED=95=B4=EC=8B=9C=20=EA=B2=80?= =?UTF-8?q?=EC=82=AC=20strict=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/verify.sh | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/scripts/verify.sh b/scripts/verify.sh index ccf7e11..4666480 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -36,8 +36,37 @@ if [ -n "$version" ]; then fi cd "$dist_dir" +if [ ! -s SHA256SUMS ]; then + echo "verify: SHA256SUMS is empty" >&2 + exit 1 +fi +if grep -Evq '^[0-9a-f]{64} [A-Za-z0-9._+-]+$' SHA256SUMS; then + echo "verify: SHA256SUMS contains a malformed line" >&2 + exit 1 +fi +listed=$(sed 's/^[0-9a-f]* //' SHA256SUMS) +for f in ./*.tar.gz ./*.cdx.json; do + [ -e "$f" ] || continue + name=${f#./} + if ! printf '%s\n' "$listed" | grep -Fxq -- "$name"; then + echo "verify: $name is not covered by SHA256SUMS" >&2 + exit 1 + fi +done +if [ -n "$version" ]; then + found=0 + for name in $listed; do + case "$name" in + "pairier-$version-"*.tar.gz) found=1 ;; + esac + done + if [ "$found" -ne 1 ]; then + echo "verify: SHA256SUMS lists no pairier-$version tarball" >&2 + exit 1 + fi +fi if command -v sha256sum >/dev/null 2>&1; then - sha256sum -c SHA256SUMS + sha256sum --strict -c SHA256SUMS else - shasum -a 256 -c SHA256SUMS + shasum -a 256 --strict -c SHA256SUMS fi From 78ffe051ce9a406d3473ee24caaf8e741bda4c76 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 15 Oct 2026 02:27:50 +0900 Subject: [PATCH 46/48] =?UTF-8?q?=EC=BD=94=EC=96=B4=20=EC=86=8C=EC=BC=93?= =?UTF-8?q?=20=EA=B2=BD=EB=A1=9C=20=EB=AA=A8=EB=93=9C=EB=A5=BC=20=EB=B3=84?= =?UTF-8?q?=EB=8F=84=20UID=20=EC=9A=B4=EC=98=81=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=8F=99=EC=9E=91=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20-=20=ED=94=BC=EC=96=B4=20uid=EA=B0=80=20=EB=8B=A4?= =?UTF-8?q?=EB=A5=B4=EB=A9=B4=20=EC=86=8C=EC=BC=93=EC=9D=84=200666?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=97=B4=EA=B3=A0=20=EB=94=94=EB=A0=89?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EA=B6=8C=ED=95=9C=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=91=EA=B7=BC=20=EC=A0=9C=ED=95=9C=20-=20=EB=8B=A4?= =?UTF-8?q?=EB=A5=B8=20uid=20=EC=97=B0=EA=B2=B0=EC=9D=80=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=ED=9B=84=20=EB=81=8A=EA=B3=A0=20=EA=B3=84=EC=86=8D?= =?UTF-8?q?=20=EB=8C=80=EA=B8=B0,=20=EA=B3=B5=EC=9A=A9=20=EC=93=B0?= =?UTF-8?q?=EA=B8=B0=20=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC=20=EA=B1=B0?= =?UTF-8?q?=EB=B6=80=20-=20=EC=86=8C=EC=BC=93=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EC=A0=84=20=EA=B0=99=EC=9D=80=20=ED=8C=8C=EC=9D=BC=EC=9D=B8?= =?UTF-8?q?=EC=A7=80=20=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/crates/kms-core/src/main.rs | 11 +-- core/crates/kms-core/src/transport.rs | 114 +++++++++++++++++++++++--- core/crates/kms-core/tests/daemon.rs | 92 +++++++++++++++++++++ 3 files changed, 196 insertions(+), 21 deletions(-) diff --git a/core/crates/kms-core/src/main.rs b/core/crates/kms-core/src/main.rs index 37c5be5..6eac6bd 100644 --- a/core/crates/kms-core/src/main.rs +++ b/core/crates/kms-core/src/main.rs @@ -68,16 +68,9 @@ fn harden(cfg: &Config) -> Result<(), String> { fn serve(cfg: &Config) -> Result<(), String> { harden(cfg)?; - let mut stream = transport::acquire(&cfg.transport).map_err(|e| format!("transport: {e}"))?; - - let cred = kms_sys::peer::peer_credentials(&stream).map_err(|e| e.to_string())?; let expected_uid = cfg.peer_uid.unwrap_or_else(kms_sys::peer::current_uid); - if cred.uid != expected_uid { - return Err(format!( - "peer uid {} does not match expected uid {expected_uid}", - cred.uid - )); - } + let (mut stream, cred) = + transport::acquire(&cfg.transport, expected_uid).map_err(|e| format!("transport: {e}"))?; let root = match &cfg.root { RootSource::File(path) => { diff --git a/core/crates/kms-core/src/transport.rs b/core/crates/kms-core/src/transport.rs index edfb81f..3620f82 100644 --- a/core/crates/kms-core/src/transport.rs +++ b/core/crates/kms-core/src/transport.rs @@ -1,20 +1,110 @@ -use std::io; +use std::fs; use std::os::fd::AsRawFd; +use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; use std::os::unix::net::{UnixListener, UnixStream}; +use std::path::Path; + +use kms_sys::peer::{self, PeerCred}; use crate::args::Transport; -pub(crate) fn acquire(t: &Transport) -> io::Result { - let stream = match t { - Transport::Fd(fd) => kms_sys::peer::inherited_socket(*fd).map_err(io::Error::other)?, - Transport::Socket(path) => { - let listener = UnixListener::bind(path)?; - let (stream, _) = listener.accept()?; - drop(listener); - std::fs::remove_file(path)?; - stream +const STICKY: u32 = 0o1000; +const GROUP_OTHER_WRITE: u32 = 0o022; +const CROSS_UID_SOCKET_MODE: u32 = 0o666; + +pub(crate) fn acquire(t: &Transport, expected_uid: u32) -> Result<(UnixStream, PeerCred), String> { + let (stream, cred) = match t { + Transport::Fd(fd) => { + let s = peer::inherited_socket(*fd).map_err(|e| e.to_string())?; + let cred = peer::peer_credentials(&s).map_err(|e| e.to_string())?; + if cred.uid != expected_uid { + return Err(mismatch(cred.uid, expected_uid)); + } + (s, cred) } + Transport::Socket(path) => listen(path, expected_uid)?, + }; + peer::close_other_fds(&[0, 1, 2, stream.as_raw_fd()]).map_err(|e| e.to_string())?; + Ok((stream, cred)) +} + +fn mismatch(got: u32, want: u32) -> String { + format!("peer uid {got} does not match expected uid {want}") +} + +fn check_parent(path: &Path) -> Result<(), String> { + let parent = match path.parent() { + Some(p) if !p.as_os_str().is_empty() => p, + _ => Path::new("."), }; - kms_sys::peer::close_other_fds(&[0, 1, 2, stream.as_raw_fd()]).map_err(io::Error::other)?; - Ok(stream) + let md = fs::symlink_metadata(parent) + .map_err(|e| format!("socket directory {}: {e}", parent.display()))?; + if !md.file_type().is_dir() { + return Err(format!( + "socket directory {} is not a directory", + parent.display() + )); + } + if md.uid() != peer::current_uid() && md.uid() != 0 { + return Err(format!( + "socket directory {} must be owned by this user or root", + parent.display() + )); + } + if md.mode() & GROUP_OTHER_WRITE != 0 && md.mode() & STICKY == 0 { + return Err(format!( + "socket directory {} is writable by other users", + parent.display() + )); + } + Ok(()) +} + +fn listen(path: &Path, expected_uid: u32) -> Result<(UnixStream, PeerCred), String> { + check_parent(path)?; + let listener = UnixListener::bind(path).map_err(|e| format!("bind {}: {e}", path.display()))?; + let md = fs::symlink_metadata(path).map_err(|e| format!("socket {}: {e}", path.display()))?; + let identity = (md.dev(), md.ino()); + let accepted = open_to(path, expected_uid).and_then(|()| accept_peer(&listener, expected_uid)); + drop(listener); + let removed = remove_if_same(path, identity); + let (stream, cred) = accepted?; + removed?; + Ok((stream, cred)) +} + +fn open_to(path: &Path, expected_uid: u32) -> Result<(), String> { + if expected_uid == peer::current_uid() { + return Ok(()); + } + fs::set_permissions(path, fs::Permissions::from_mode(CROSS_UID_SOCKET_MODE)) + .map_err(|e| format!("socket {}: {e}", path.display())) +} + +fn accept_peer( + listener: &UnixListener, + expected_uid: u32, +) -> Result<(UnixStream, PeerCred), String> { + loop { + let (s, _) = listener.accept().map_err(|e| format!("accept: {e}"))?; + match peer::peer_credentials(&s) { + Ok(c) if c.uid == expected_uid => return Ok((s, c)), + Ok(c) => crate::log(&format!( + "rejected connection: {}", + mismatch(c.uid, expected_uid) + )), + Err(e) => crate::log(&format!("rejected connection: {e}")), + } + } +} + +fn remove_if_same(path: &Path, identity: (u64, u64)) -> Result<(), String> { + let md = fs::symlink_metadata(path).map_err(|e| format!("socket {}: {e}", path.display()))?; + if !md.file_type().is_socket() || (md.dev(), md.ino()) != identity { + return Err(format!( + "socket path {} was replaced; refusing to remove it", + path.display() + )); + } + fs::remove_file(path).map_err(|e| format!("socket {}: {e}", path.display())) } diff --git a/core/crates/kms-core/tests/daemon.rs b/core/crates/kms-core/tests/daemon.rs index c25e228..ee1c8fe 100644 --- a/core/crates/kms-core/tests/daemon.rs +++ b/core/crates/kms-core/tests/daemon.rs @@ -238,3 +238,95 @@ fn bad_arguments_fail_fast() { assert!(!ok); assert!(err.contains("usage"), "stderr: {err}"); } + +fn wait_for(path: &PathBuf) -> fs::Metadata { + let deadline = Instant::now() + Duration::from_secs(10); + loop { + if let Ok(md) = fs::symlink_metadata(path) { + return md; + } + assert!( + Instant::now() < deadline, + "{} never appeared", + path.display() + ); + std::thread::sleep(Duration::from_millis(20)); + } +} + +fn private_dir(tag: &str) -> PathBuf { + let d = scratch(tag); + let _ = fs::remove_dir_all(&d); + fs::create_dir(&d).unwrap(); + fs::set_permissions(&d, fs::Permissions::from_mode(0o700)).unwrap(); + d +} + +#[test] +fn same_uid_socket_is_owner_only() { + let dir = private_dir("own-dir"); + let sock = dir.join("core.sock"); + let child = spawn(&[ + "--socket", + sock.to_str().unwrap(), + "--ephemeral-root", + "--allow-unlocked-memory", + ]); + assert_eq!(wait_for(&sock).permissions().mode() & 0o077, 0); + let mut s = connect(&sock); + let (h, _) = roundtrip( + &mut s, + 1, + &HelloRequest { + protocol_version: PROTOCOL_VERSION, + }, + ); + assert_eq!(h.status, Status::Ok); + drop(s); + let _ = finish(child); + fs::remove_dir_all(&dir).unwrap(); +} + +#[test] +fn foreign_uid_is_rejected_without_ending_the_daemon() { + let dir = private_dir("foreign-dir"); + let sock = dir.join("core.sock"); + let other = kms_sys::peer::current_uid().wrapping_add(1).to_string(); + let mut child = spawn(&[ + "--socket", + sock.to_str().unwrap(), + "--ephemeral-root", + "--allow-unlocked-memory", + "--peer-uid", + &other, + ]); + assert_eq!(wait_for(&sock).permissions().mode() & 0o777, 0o666); + let mut s = connect(&sock); + let mut buf = [0u8; 1]; + assert_eq!(s.read(&mut buf).unwrap_or(0), 0, "foreign peer was served"); + std::thread::sleep(Duration::from_millis(200)); + assert!(child.try_wait().unwrap().is_none(), "daemon exited"); + assert!(sock.exists(), "socket removed before an accepted peer"); + child.kill().unwrap(); + let (_, err) = finish(child); + assert!(err.contains("rejected connection"), "{err}"); + fs::remove_dir_all(&dir).unwrap(); +} + +#[test] +fn shared_socket_directory_is_refused() { + let dir = private_dir("shared-dir"); + fs::set_permissions(&dir, fs::Permissions::from_mode(0o777)).unwrap(); + let sock = dir.join("core.sock"); + let child = spawn(&[ + "--socket", + sock.to_str().unwrap(), + "--ephemeral-root", + "--allow-unlocked-memory", + ]); + let (ok, err) = finish(child); + assert!(!ok); + assert!(err.contains("writable by other users"), "{err}"); + assert!(!sock.exists()); + fs::remove_dir_all(&dir).unwrap(); +} From dcf93af3b67a3e1f66a26b5442aee8b0d58eca7c Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 15 Oct 2026 02:30:07 +0900 Subject: [PATCH 47/48] =?UTF-8?q?=EB=B3=B4=EC=95=88=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20=EC=82=AC=ED=95=AD=20=EB=AC=B8=EC=84=9C=20=EB=B0=98=EC=98=81?= =?UTF-8?q?=20-=20=EA=B0=90=EC=82=AC=20=EB=A1=9C=EA=B7=B8=20head=20?= =?UTF-8?q?=EA=B8=B0=EB=A1=9D=EA=B3=BC=20--anchor=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=A0=88=EC=B0=A8,=20=EC=86=8C=EC=BC=93=20=EB=94=94=EB=A0=89?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EA=B6=8C=ED=95=9C=20=EC=9A=94=EA=B1=B4=20?= =?UTF-8?q?-=20=EB=B0=9B=EC=9D=80=20=ED=8E=B8=EC=A7=80=ED=95=A8=EA=B3=BC?= =?UTF-8?q?=20=EB=8F=99=EC=8B=9C=20=EC=97=B0=EA=B2=B0=20=EC=83=81=ED=95=9C?= =?UTF-8?q?,=20=EB=9D=BC=EB=B2=A8=20=EB=AC=B8=EC=9E=90=20=EC=A0=9C?= =?UTF-8?q?=ED=95=9C,=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0=20?= =?UTF-8?q?=ED=95=AD=EB=AA=A9=20=EC=B6=94=EA=B0=80=20-=20README=EC=9D=98?= =?UTF-8?q?=20=EC=86=8C=EC=BC=93=20=EB=AA=A8=EB=93=9C=20=EC=84=A4=EB=AA=85?= =?UTF-8?q?=EC=9D=84=20=EC=8B=A4=EC=A0=9C=20=EC=A7=80=EC=9B=90=20=EB=B2=94?= =?UTF-8?q?=EC=9C=84=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- README_KR.md | 2 +- SECURITY.md | 6 +-- SECURITY_KR.md | 6 +-- USAGE.md | 102 ++++++++++++++++++++++++++++--------------------- USAGE_KR.md | 66 ++++++++++++++++++++------------ 6 files changed, 108 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 02e4fa8..3ab9cb4 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ A Zero-Trust hybrid Key Management System (KMS) built with Go and Rust. The Go h ## Features -The core (Rust) runs as a separate process and never shares an address space with the host (Go). The host creates a `socketpair` inside its own process, keeps one end for itself, and hands the other end to the child as `fd` 3 when it executes `kms-core`. No socket path appears on the filesystem and nothing listens, so there is no way for a third party to connect; only the host and the core share the channel. Because the host launches the core, both run under the same UID. For production, the core can instead be started first under a separate UID, with the host connecting through a `--socket` path; in that mode the core verifies the connecting peer with `--peer-uid`. Either way, TCP is never used, only local IPC on the same machine. +The core (Rust) runs as a separate process and never shares an address space with the host (Go). The host creates a `socketpair` inside its own process, keeps one end for itself, and hands the other end to the child as `fd` 3 when it executes `kms-core`. No socket path appears on the filesystem and nothing listens, so there is no way for a third party to connect; only the host and the core share the channel. Because the host launches the core, both run under the same UID. The core itself can also be started first under a separate UID and accept a connection on a `--socket` path, in which case it accepts only a peer whose UID matches `--peer-uid`. Currently, however, `kmsctl` and `kmsd` only launch the core themselves. Either way, TCP is never used, only local IPC on the same machine. A stateless core is another key property. The core keeps only a 32-byte root key in memory. Individual keys are stored on the host as blobs sealed under the root key, and the host cannot open them. The blob header (algorithm, usage, key id, label) is fully authenticated. diff --git a/README_KR.md b/README_KR.md index 3d1b373..06a2d19 100644 --- a/README_KR.md +++ b/README_KR.md @@ -27,7 +27,7 @@ ## 기능 -코어(Rust)는 별도 프로세스로 동작하며, 호스트(Go)와 같은 주소 공간을 공유하지 않습니다. 호스트가 자기 프로세스 안에서 `socketpair`를 만들어 한쪽 끝은 자신이 갖고, 다른 쪽 끝을 자식 프로세스의 `fd` 3번으로 넘기며 `kms-core`를 실행합니다. 이 방식에서는 파일시스템(fs)에 소켓 경로가 생기지 않고 listen도 하지 않으므로 제3자가 접속할 방법이 없고, 호스트와 코어만 해당 통로를 공유합니다. 다만 호스트가 코어를 띄우기 때문에 같은 UID로 동작합니다. 운영 환경에서는 코어를 별도 UID로 먼저 띄우고 호스트가 `--socket` 경로로 접속하는 방식도 지원하며, 이때 코어는 `--peer-uid`로 접속자를 검증합니다. 어느 방식이든 TCP는 쓰지 않고 같은 머신 안의 로컬 IPC만 사용합니다. +코어(Rust)는 별도 프로세스로 동작하며, 호스트(Go)와 같은 주소 공간을 공유하지 않습니다. 호스트가 자기 프로세스 안에서 `socketpair`를 만들어 한쪽 끝은 자신이 갖고, 다른 쪽 끝을 자식 프로세스의 `fd` 3번으로 넘기며 `kms-core`를 실행합니다. 이 방식에서는 파일시스템(fs)에 소켓 경로가 생기지 않고 listen도 하지 않으므로 제3자가 접속할 방법이 없고, 호스트와 코어만 해당 통로를 공유합니다. 다만 호스트가 코어를 띄우기 때문에 같은 UID로 동작합니다. 코어 자체는 별도 UID로 먼저 띄워 `--socket` 경로에서 접속을 받는 방식도 지원하며, 이때 `--peer-uid`와 UID가 일치하는 접속만 받습니다. 현재 `kmsctl`과 `kmsd`는 코어를 직접 띄우는 방식만 사용합니다. 어느 방식이든 TCP는 쓰지 않고 같은 머신 안의 로컬 IPC만 사용합니다. 상태 없는(stateless) 코어도 핵심 기능입니다. 코어는 32바이트 루트 키(root key)만 메모리에 두고, 개별 키는 루트 키로 봉인된(sealed) blob으로 호스트에 저장되며, 호스트는 해당 blob을 열 수 없습니다. blob 헤더(알고리즘, 용도, 키 id, 라벨)는 모두 인증됩니다. diff --git a/SECURITY.md b/SECURITY.md index dd7f2ef..04c353b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -64,7 +64,7 @@ These are design choices or open items that are disclosed here rather than treat - `kmsd` holds its TLS private key in process memory. Identity, KEM, audit and symmetric keys never leave the core, but the transport key does. - The P-256 + ML-KEM-768 hybrid KEM combines the two shared secrets with an HKDF construction specific to pairier. It is not a standardized combiner. - A member who receives a key with `export` can send it on to others. Omit `export` to prevent re-sharing. -- The audit log is a hash chain with a per-entry signature by the core's audit key. Anyone who can start the core as the node user can produce valid entries, and truncation of the log tail cannot be detected without an external reference. +- The audit log is a hash chain with a per-entry signature by the core's audit key. Anyone who can start the core as the node user can produce valid entries. Truncating the log tail or rolling the log back is detected only by comparing against a head recorded outside the node with `kmsctl audit verify --anchor`. - On macOS the core runs without locked memory and the FIPS core is not statically linked. macOS is for development only. - `make audit` uses a local RustSec advisory database that must be refreshed on a connected machine. - The Rust wire decoder mutation test is seeded, not coverage guided. @@ -82,10 +82,10 @@ Release tarballs are signed with [minisign](https://jedisct1.github.io/minisign/ ```bash $ minisign -Vm SHA256SUMS -p minisign.pub $ minisign -Vm SHA256SUMS -P RWTKQNgFrPLkWgNMj67g1XXR7A7IWAfeXYtnGZ1At0+QFEoUUqFZg7pP -$ sha256sum -c SHA256SUMS +$ sha256sum --strict -c SHA256SUMS ``` -Either form works. The first uses the key file, the second the key string itself. +Either form works. The first uses the key file, the second the key string itself. The repository's `scripts/verify.sh` (`make verify-dist`) also checks, beyond the signature and hashes, that every tarball and SBOM in the directory is listed in `SHA256SUMS` and that a tarball for the given version is present. Releases are also reproducible. `make dist FIPS=1` on Linux with the pinned toolchains produces a byte-identical tarball, so you can rebuild and compare `SHA256SUMS` instead of, or in addition to, checking the signature. diff --git a/SECURITY_KR.md b/SECURITY_KR.md index efa40b0..b367b9b 100644 --- a/SECURITY_KR.md +++ b/SECURITY_KR.md @@ -64,7 +64,7 @@ pairier는 "Abandon trust. Choose isolation." 원칙으로 설계되었습니다 - `kmsd`는 TLS 개인키를 프로세스 메모리에 둡니다. 신원, KEM, 감사, 대칭 키는 코어 밖으로 나오지 않지만 전송용 키는 예외입니다. - P-256 + ML-KEM-768 하이브리드 KEM은 두 공유 비밀을 pairier 고유의 HKDF 구성으로 결합합니다. 표준화된 결합 방식이 아닙니다. - `export` 권한과 함께 키를 받은 구성원은 그 키를 다른 구성원에게 다시 보낼 수 있습니다. 재공유를 막으려면 `export`를 주지 마세요. -- 감사 로그는 항목마다 코어의 감사 키로 서명한 해시 체인입니다. 노드 사용자 권한으로 코어를 기동할 수 있으면 유효한 항목을 만들 수 있고, 로그 끝부분이 잘리는 것은 외부 기준점 없이는 탐지할 수 없습니다. +- 감사 로그는 항목마다 코어의 감사 키로 서명한 해시 체인입니다. 노드 사용자 권한으로 코어를 기동할 수 있으면 유효한 항목을 만들 수 있습니다. 로그 끝부분을 잘라 내거나 이전 로그로 되돌리는 것은, 노드 밖에 기록해 둔 head와 `kmsctl audit verify --anchor`로 대조할 때만 탐지됩니다. - macOS에서는 코어가 메모리 잠금 없이 실행되고 FIPS 코어도 정적 링크되지 않습니다. macOS는 개발용입니다. - `make audit`는 로컬 RustSec 권고 데이터베이스를 사용하며, 네트워크가 연결된 장비에서 갱신해야 합니다. - Rust 와이어 디코더 변이 테스트는 시드 기반이며 커버리지 유도 방식이 아닙니다. @@ -82,10 +82,10 @@ pairier는 "Abandon trust. Choose isolation." 원칙으로 설계되었습니다 ```bash $ minisign -Vm SHA256SUMS -p minisign.pub $ minisign -Vm SHA256SUMS -P RWTKQNgFrPLkWgNMj67g1XXR7A7IWAfeXYtnGZ1At0+QFEoUUqFZg7pP -$ sha256sum -c SHA256SUMS +$ sha256sum --strict -c SHA256SUMS ``` -두 방식 모두 동일하게 동작합니다. 첫 번째는 키 파일을, 두 번째는 키 문자열 자체를 사용합니다. +두 방식 모두 동일하게 동작합니다. 첫 번째는 키 파일을, 두 번째는 키 문자열 자체를 사용합니다. 저장소의 `scripts/verify.sh`(`make verify-dist`)는 서명과 해시 외에도, 디렉토리의 tarball과 SBOM이 모두 `SHA256SUMS`에 들어 있는지와 지정한 버전의 tarball이 있는지까지 확인합니다. 릴리즈는 재현 가능합니다. 고정된 툴체인으로 Linux에서 `make dist FIPS=1`을 실행하면 바이트 단위로 동일한 tarball이 생성되므로, 서명 확인 대신 또는 서명 확인과 함께 직접 빌드해 `SHA256SUMS`를 비교할 수 있습니다. diff --git a/USAGE.md b/USAGE.md index 4c474f7..6dd2028 100644 --- a/USAGE.md +++ b/USAGE.md @@ -114,7 +114,7 @@ alice$ kmsctl --dir ~/.pairier key gen --label project/db af8477cc1076f219bfb047d846629eb8 aes-256-gcm encrypt,decrypt,export project/db ``` -The generated key is an AES-256-GCM symmetric key. If the `--usage` flag is omitted, the default `encrypt,decrypt,export` applies. A key that will be sent to another member must include `export`. +The generated key is an AES-256-GCM symmetric key. If the `--usage` flag is omitted, the default `encrypt,decrypt,export` applies. A key that will be sent to another member must include `export`. Labels cannot contain control characters such as a newline, or bidirectional formatting characters such as U+202E. ### 5-a. Offline delivery (file) @@ -142,7 +142,7 @@ alice$ kmsctl --dir ~/.pairier key push --id af8477cc... --to bob --url https:// pushed af8477cc... to bob envelope 854a37b90d476c1070be8c5449403071 ``` -A received envelope does not become a key right away. It is held in the inbox, and the recipient reviews it and then accepts or rejects it. +A received envelope does not become a key right away. It is held in the inbox, and the recipient reviews it and then accepts or rejects it. Up to 64 pending envelopes are kept per sender, and the next one is accepted only after the recipient accepts or rejects one. ```bash bob$ kmsctl --dir ~/.pairier inbox list @@ -168,8 +168,12 @@ The ciphertext is binary and exactly 48 bytes longer than the plaintext (salt 32 ```bash bob$ kmsctl --dir ~/.pairier audit verify verified 13 entries +head 13:9c1d...e4a0 +bob$ kmsctl --dir ~/.pairier audit verify --anchor 13:9c1d...e4a0 ``` +`head` is the sequence number and hash of the last entry. After important operations such as sending keys or registering members, record this value outside the node (another device, a printout, and so on). Passing it later with `--anchor` checks that the log still contains that exact entry. The check fails if the end of the log was truncated, or if an older log was restored and then appended to. + ## kmsctl command reference ### Global options @@ -185,24 +189,24 @@ Global options may appear before or after the command. `kmsctl --dir X key list` ### Commands -| Command | Description | -|---------------------------------------------------------------|---------------------------------------------------------------------------| -| `init --id MEMBER [--pqc]` | Creates a node in an empty directory. | -| `card [--out FILE]` | Prints the self-signed identity card. The fingerprint is shown on stderr. | -| `member add --card FILE --fingerprint FP` | Registers a member after checking the fingerprint. | -| `member list` | Shows registered members with their algorithms and fingerprints. | -| `key gen --label LABEL [--usage LIST]` | Generates a symmetric key. | -| `key list` | Lists keys. The last column is `local` or `from `. | -| `key info --id KEYID` | Shows key details and origin. | -| `key send --id KEYID --to MEMBER [--usage LIST] [--out FILE]` | Creates an envelope file. | -| `key receive [--in FILE]` | Verifies an envelope and imports the key. | -| `key push --id KEYID --to MEMBER --url URL [--usage LIST]` | Sends an envelope to the recipient's kmsd. | -| `inbox list`, `inbox accept --id ID`, `inbox reject --id ID` | Handles envelopes received by kmsd. | -| `peer health --member MEMBER --url URL` | Checks that mutual authentication with the peer's kmsd works. | -| `encrypt --id KEYID [--in FILE] [--out FILE] [--aad STR]` | Encrypts. Reads stdin when no input is given. | -| `decrypt --id KEYID [--in FILE] [--out FILE] [--aad STR]` | Decrypts. | -| `audit verify` | Verifies the audit log chain and every checkpoint signature. | -| `version` | Prints the version and the Go FIPS status. | +| Command | Description | +|---------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `init --id MEMBER [--pqc]` | Creates a node in an empty directory. | +| `card [--out FILE]` | Prints the self-signed identity card. The fingerprint is shown on stderr. | +| `member add --card FILE --fingerprint FP` | Registers a member after checking the fingerprint. | +| `member list` | Shows registered members with their algorithms and fingerprints. | +| `key gen --label LABEL [--usage LIST]` | Generates a symmetric key. | +| `key list` | Lists keys. The last column is `local` or `from `. | +| `key info --id KEYID` | Shows key details and origin. | +| `key send --id KEYID --to MEMBER [--usage LIST] [--out FILE]` | Creates an envelope file. | +| `key receive [--in FILE]` | Verifies an envelope and imports the key. | +| `key push --id KEYID --to MEMBER --url URL [--usage LIST]` | Sends an envelope to the recipient's kmsd. | +| `inbox list`, `inbox accept --id ID`, `inbox reject --id ID` | Handles envelopes received by kmsd. | +| `peer health --member MEMBER --url URL` | Checks that mutual authentication with the peer's kmsd works. | +| `encrypt --id KEYID [--in FILE] [--out FILE] [--aad STR]` | Encrypts. Reads stdin when no input is given. | +| `decrypt --id KEYID [--in FILE] [--out FILE] [--aad STR]` | Decrypts. | +| `audit verify [--anchor SEQ:HASH]` | Verifies the audit log chain and every checkpoint signature, and prints the head. With `--anchor`, also checks that the anchored entry is still in the log. | +| `version` | Prints the version and the Go FIPS status. | If the file given with `--out` already exists, the command fails instead of overwriting it, and new files are created with `0600` permissions. Exit codes are as follows. @@ -264,7 +268,7 @@ Every endpoint requires a client certificate of a registered member. | `GET /v1/card` | This node's identity card | | `POST /v1/inbox` | Submits an envelope (the body is the envelope bytes). Returns `201` and the envelope id on success | -An envelope is rejected with `400` if its sender differs from the authenticated peer, its recipient is not this node, or its signature does not verify. Submitting the same envelope again returns `409`. +An envelope is rejected with `400` if its sender differs from the authenticated peer, its recipient is not this node, or its signature does not verify. Submitting the same envelope again returns `409`, and once the same sender has 64 pending envelopes the response is `429`. kmsd serves up to 64 connections at a time, and further connections wait until an earlier one ends. ### systemd example @@ -291,15 +295,21 @@ $ kms-core --generate-root-key /path/root.key $ kms-core --socket /run/pairier/core.sock --root-key-file /path/root.key --pqc --peer-uid 1001 ``` -| Option | Description | -|---------------------------|----------------------------------------------------------------------------------------------------------------------------------------------| -| `--fd N` | Use an inherited socket (`3` when the host launches the core) | -| `--socket PATH` | Accept a single connection on the socket path, then remove the path | -| `--root-key-file PATH` | Accepts only a 32-byte regular file owned by the current user, with no group or other permissions, no symbolic links and no extra hard links | -| `--ephemeral-root` | Temporary root key held only in memory (blobs cannot be opened after a restart) | -| `--pqc` | Enable PQC algorithms (when built with the pqc feature) | -| `--allow-unlocked-memory` | Continue where `mlockall` is unavailable | -| `--peer-uid UID` | Verify the uid of the connecting peer (defaults to the core's own uid) | +| Option | Description | +|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `--fd N` | Use an inherited socket (`3` when the host launches the core) | +| `--socket PATH` | Accept a single connection with the expected uid on the socket path, then remove the path. Connections from other uids are logged and closed while the core keeps waiting | +| `--root-key-file PATH` | Accepts only a 32-byte regular file owned by the current user, with no group or other permissions, no symbolic links and no extra hard links | +| `--ephemeral-root` | Temporary root key held only in memory (blobs cannot be opened after a restart) | +| `--pqc` | Enable PQC algorithms (when built with the pqc feature) | +| `--allow-unlocked-memory` | Continue where `mlockall` is unavailable | +| `--peer-uid UID` | uid of the peer to accept (defaults to the core's own uid). If it differs, the socket is left at mode `0666` | + +The directory that holds the socket must be owned by the core user or root. A directory writable by group or others is refused unless it has the sticky bit. When `--peer-uid` differs from the core's own uid, the socket file is mode `0666`, so restrict access with the directory permissions. For example, with the core as uid 1001 and the host as uid 1002, let only the host group traverse the directory. + +```bash +# install -d -o 1001 -g 1002 -m 0710 /run/pairier +``` > [!NOTE] > Currently `kmsctl` and `kmsd` support only launching the core directly with the root key in the node directory. @@ -324,7 +334,7 @@ $ kms-core --socket /run/pairier/core.sock --root-key-file /path/root.key --pqc ## Security operations notes - **Verify fingerprints over a separate channel.** If the card and the fingerprint arrive by the same route, a man-in-the-middle attack cannot be prevented. -- **Understand the limits of the audit log.** Each entry is signed with the core's audit key, so an attacker who cannot use the core (a stolen disk, a tampered backup, and so on) cannot forge or recompute the log. However, anyone who can start the core with the node user's permissions can produce valid signatures. Truncating the end of the log also cannot be detected without an external reference point. +- **Understand the limits of the audit log.** Each entry is signed with the core's audit key, so an attacker who cannot use the core (a stolen disk, a tampered backup, and so on) cannot forge or recompute the log. However, anyone who can start the core with the node user's permissions can produce valid signatures. Truncating the end of the log, or rolling it back to an older copy, is detected only by comparing against a head recorded outside the node with `audit verify --anchor`. - **kmsd keeps its TLS private key in memory.** The symmetric keys being exchanged and the identity, KEM and audit keys never leave the core, but the transport TLS key is an exception. - **Use macOS for development only.** Without `mlockall`, the core runs without memory locking, and the FIPS core is not statically linked. Run production deployments on Linux. - **The audit log grows quickly in PQC mode.** ML-DSA signatures make each entry about 7KB, and every encryption and decryption also adds an entry. @@ -339,21 +349,27 @@ $ kms-core --socket /run/pairier/core.sock --root-key-file /path/root.key --pqc | Item | Limit | |------------------------------|--------------------------------------------------------------------------------------------------------------------------------| | Member id | Up to 64 characters, starting with a lowercase letter or digit and containing only lowercase letters, digits, `.`, `_` and `-` | -| Key label | 256 bytes | +| Key label | 256 bytes, no control or bidirectional formatting characters | | Encryption plaintext, AAD | 64KiB each | | kmsctl input files and stdin | 1MiB | | Envelope context | 1KiB | +| Pending envelopes per sender | 64 | +| kmsd concurrent connections | 64 | ## Troubleshooting -| Message | Cause and fix | -|----------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------| -| `mlockall: Cannot allocate memory ... (pass --allow-unlocked-memory to proceed)` | The memlock limit is too small. Raise it with `ulimit -l unlimited` or `LimitMEMLOCK=infinity`. | -| `node: fingerprint does not match the card` | The fingerprint differs. Get the card again and re-verify the fingerprint over a separate channel. | -| `core: op 0x0023 failed: capability disabled` | A pqc card was registered on a node initialized without pqc. | -| `core: op 0x0030 failed: usage denied` | The key lacks `export`, or the requested usage is broader than the original. | -| `node: envelope is addressed to another member` | The envelope was sent to a different member. | -| `node: unknown member` | The sender or the recipient is not registered. | -| `store: object already exists` | The key was already imported or the envelope was already received. | -| `node: stored metadata does not match the core` | Records in the node directory have been tampered with. Check backups and the audit log. | -| `audit: checkpoint signature is invalid` | The audit log was modified without the core. | +| Message | Cause and fix | +|----------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------| +| `mlockall: Cannot allocate memory ... (pass --allow-unlocked-memory to proceed)` | The memlock limit is too small. Raise it with `ulimit -l unlimited` or `LimitMEMLOCK=infinity`. | +| `node: fingerprint does not match the card` | The fingerprint differs. Get the card again and re-verify the fingerprint over a separate channel. | +| `core: op 0x0023 failed: capability disabled` | A pqc card was registered on a node initialized without pqc. | +| `core: op 0x0030 failed: usage denied` | The key lacks `export`, or the requested usage is broader than the original. | +| `node: envelope is addressed to another member` | The envelope was sent to a different member. | +| `node: unknown member` | The sender or the recipient is not registered. | +| `store: object already exists` | The key was already imported or the envelope was already received. | +| `node: stored metadata does not match the core` | Records in the node directory have been tampered with. Check backups and the audit log. | +| `audit: checkpoint signature is invalid` | The audit log was modified without the core. | +| `audit: log does not contain the anchored entry` | The log was truncated or rolled back after the recorded head. Check backups and access to the node. | +| `node: label contains control or bidirectional formatting characters` | Remove control and bidirectional formatting characters from the label. | +| `api: push rejected: 429 inbox full for this sender` | The recipient's inbox holds 64 pending envelopes from you. The recipient must accept or reject some. | +| `transport: socket directory ... is writable by other users` | Remove group and other write permission from the socket directory. | diff --git a/USAGE_KR.md b/USAGE_KR.md index 10dd4e5..a8d41c9 100644 --- a/USAGE_KR.md +++ b/USAGE_KR.md @@ -114,7 +114,7 @@ alice$ kmsctl --dir ~/.pairier key gen --label project/db af8477cc1076f219bfb047d846629eb8 aes-256-gcm encrypt,decrypt,export project/db ``` -생성되는 키는 AES-256-GCM 대칭 키입니다. `--usage` 플래그를 생략하면 기본값으로 `encrypt,decrypt,export`가 적용됩니다. 다른 구성원에게 보낼 키에는 반드시 `export`가 포함되어 있어야 합니다. +생성되는 키는 AES-256-GCM 대칭 키입니다. `--usage` 플래그를 생략하면 기본값으로 `encrypt,decrypt,export`가 적용됩니다. 다른 구성원에게 보낼 키에는 반드시 `export`가 포함되어 있어야 합니다. 라벨에는 줄바꿈 같은 제어 문자와 U+202E 같은 양방향 서식 문자를 쓸 수 없습니다. ### 5-a. 오프라인 전달 (파일) @@ -142,7 +142,7 @@ alice$ kmsctl --dir ~/.pairier key push --id af8477cc... --to bob --url https:// pushed af8477cc... to bob envelope 854a37b90d476c1070be8c5449403071 ``` -받은 봉투는 바로 키가 되지 않고 inbox에 보관됩니다. 수신자가 확인 후 수락하거나 거절할 수 있습니다. +받은 봉투는 바로 키가 되지 않고 inbox에 보관됩니다. 수신자가 확인 후 수락하거나 거절할 수 있습니다. 송신자 한 명당 대기 중인 봉투는 64개까지 보관하며, 수락하거나 거절해 자리가 비어야 다음 봉투를 받습니다. ```bash bob$ kmsctl --dir ~/.pairier inbox list @@ -168,8 +168,12 @@ db password ```bash bob$ kmsctl --dir ~/.pairier audit verify verified 13 entries +head 13:9c1d...e4a0 +bob$ kmsctl --dir ~/.pairier audit verify --anchor 13:9c1d...e4a0 ``` +`head`는 마지막 항목의 순번과 해시입니다. 키 전달이나 구성원 등록처럼 중요한 작업을 마친 뒤에는 이 값을 노드 밖(다른 기기, 출력물 등)에 기록해 두세요. 나중에 `--anchor`로 넘기면 로그가 그 항목을 그대로 담고 있는지 확인합니다. 로그 끝부분이 잘렸거나, 이전 로그로 되돌린 뒤 이어 쓴 경우에는 실패합니다. + ## kmsctl 명령 참조 ### 전역 옵션 @@ -185,24 +189,24 @@ verified 13 entries ### 명령 -| 명령 | 설명 | -|---------------------------------------------------------------|-------------------------------------------------| -| `init --id MEMBER [--pqc]` | 빈 디렉토리에 노드를 만듭니다. | -| `card [--out FILE]` | 자기 서명된 신원 카드를 출력합니다. 지문은 stderr에 표시됩니다. | -| `member add --card FILE --fingerprint FP` | 지문을 확인한 뒤 구성원을 등록합니다. | -| `member list` | 등록된 구성원과 알고리즘, 지문을 표시합니다. | -| `key gen --label LABEL [--usage LIST]` | 대칭 키를 생성합니다. | -| `key list` | 키 목록을 표시합니다. 마지막 열은 `local` 또는 `from <송신자>`입니다. | -| `key info --id KEYID` | 키 상세와 출처를 표시합니다. | -| `key send --id KEYID --to MEMBER [--usage LIST] [--out FILE]` | 봉투 파일을 만듭니다. | -| `key receive [--in FILE]` | 봉투를 검증하고 키를 가져옵니다. | -| `key push --id KEYID --to MEMBER --url URL [--usage LIST]` | 수신자의 kmsd로 봉투를 보냅니다. | -| `inbox list`, `inbox accept --id ID`, `inbox reject --id ID` | kmsd가 받은 봉투를 처리합니다. | -| `peer health --member MEMBER --url URL` | 상대 kmsd와 상호 인증이 되는지 확인합니다. | -| `encrypt --id KEYID [--in FILE] [--out FILE] [--aad STR]` | 암호화합니다. 입력이 없으면 stdin을 씁니다. | -| `decrypt --id KEYID [--in FILE] [--out FILE] [--aad STR]` | 복호화합니다. | -| `audit verify` | 감사 로그 체인과 모든 체크포인트 서명을 검증합니다. | -| `version` | 버전과 Go FIPS 상태를 출력합니다. | +| 명령 | 설명 | +|---------------------------------------------------------------|----------------------------------------------------------------------------------| +| `init --id MEMBER [--pqc]` | 빈 디렉토리에 노드를 만듭니다. | +| `card [--out FILE]` | 자기 서명된 신원 카드를 출력합니다. 지문은 stderr에 표시됩니다. | +| `member add --card FILE --fingerprint FP` | 지문을 확인한 뒤 구성원을 등록합니다. | +| `member list` | 등록된 구성원과 알고리즘, 지문을 표시합니다. | +| `key gen --label LABEL [--usage LIST]` | 대칭 키를 생성합니다. | +| `key list` | 키 목록을 표시합니다. 마지막 열은 `local` 또는 `from <송신자>`입니다. | +| `key info --id KEYID` | 키 상세와 출처를 표시합니다. | +| `key send --id KEYID --to MEMBER [--usage LIST] [--out FILE]` | 봉투 파일을 만듭니다. | +| `key receive [--in FILE]` | 봉투를 검증하고 키를 가져옵니다. | +| `key push --id KEYID --to MEMBER --url URL [--usage LIST]` | 수신자의 kmsd로 봉투를 보냅니다. | +| `inbox list`, `inbox accept --id ID`, `inbox reject --id ID` | kmsd가 받은 봉투를 처리합니다. | +| `peer health --member MEMBER --url URL` | 상대 kmsd와 상호 인증이 되는지 확인합니다. | +| `encrypt --id KEYID [--in FILE] [--out FILE] [--aad STR]` | 암호화합니다. 입력이 없으면 stdin을 씁니다. | +| `decrypt --id KEYID [--in FILE] [--out FILE] [--aad STR]` | 복호화합니다. | +| `audit verify [--anchor SEQ:HASH]` | 감사 로그 체인과 모든 체크포인트 서명을 검증하고 head를 출력합니다. `--anchor`를 주면 그 항목이 로그에 그대로 있는지 확인합니다. | +| `version` | 버전과 Go FIPS 상태를 출력합니다. | `--out`으로 지정한 파일이 이미 있으면 덮어쓰지 않고 실패하며, 새 파일은 `0600` 권한으로 만들어집니다. 종료 코드는 다음과 같습니다. @@ -264,7 +268,7 @@ verified 13 entries | `GET /v1/card` | 이 노드의 신원 카드 | | `POST /v1/inbox` | 봉투 제출 (본문은 봉투 바이트). 성공 시 `201`과 봉투 id | -봉투의 송신자가 인증된 피어와 다르거나, 수신자가 이 노드가 아니거나, 서명이 일치하지 않으면 `400`으로 거부됩니다. 같은 봉투를 다시 보내면 `409`로 응답합니다. +봉투의 송신자가 인증된 피어와 다르거나, 수신자가 이 노드가 아니거나, 서명이 일치하지 않으면 `400`으로 거부됩니다. 같은 봉투를 다시 보내면 `409`로, 같은 송신자의 대기 봉투가 64개에 이르면 `429`로 응답합니다. kmsd는 동시에 64개 연결까지 처리하고, 그 이상은 앞선 연결이 끝날 때까지 대기시킵니다. ### systemd 예시 @@ -294,12 +298,18 @@ $ kms-core --socket /run/pairier/core.sock --root-key-file /path/root.key --pqc | 옵션 | 설명 | |---------------------------|---------------------------------------------------------------| | `--fd N` | 상속받은 소켓 사용 (호스트가 기동할 때 `3`) | -| `--socket PATH` | 소켓 경로에서 연결 하나만 받은 뒤 경로를 삭제 | +| `--socket PATH` | 소켓 경로에서 uid가 맞는 연결 하나만 받은 뒤 경로를 삭제. 다른 uid의 연결은 기록 후 끊고 계속 대기 | | `--root-key-file PATH` | 32바이트 일반 파일만 허용. 현재 사용자 소유, 그룹과 기타 권한 없음, 심볼릭 링크와 추가 하드 링크 없음 | | `--ephemeral-root` | 메모리에만 있는 임시 루트 키 (재시작하면 blob을 열 수 없음) | | `--pqc` | PQC 알고리즘 활성화 (pqc feature로 빌드한 경우) | | `--allow-unlocked-memory` | `mlockall`을 쓸 수 없는 환경에서 계속 진행 | -| `--peer-uid UID` | 연결한 피어의 uid 검증 (기본은 자신의 uid) | +| `--peer-uid UID` | 받을 피어의 uid (기본은 자신의 uid). 자신과 다르면 소켓을 `0666`으로 열어 둠 | + +소켓을 만드는 디렉토리는 코어 사용자나 root 소유여야 합니다. 그룹이나 기타 사용자가 쓸 수 있는 디렉토리는 sticky 비트가 없으면 기동을 거부합니다. `--peer-uid`가 코어 자신과 다르면 소켓 파일이 `0666`이 되므로, 디렉토리 권한으로 접근을 좁혀야 합니다. 예를 들어 코어가 uid 1001, 호스트가 uid 1002라면 호스트 그룹만 디렉토리를 지날 수 있게 만듭니다. + +```bash +# install -d -o 1001 -g 1002 -m 0710 /run/pairier +``` > [!NOTE] > 현재 `kmsctl`과 `kmsd`는 노드 디렉토리의 루트 키로 코어를 직접 기동하는 방식만 지원합니다. @@ -324,7 +334,7 @@ $ kms-core --socket /run/pairier/core.sock --root-key-file /path/root.key --pqc ## 보안 운영 주의 사항 - **지문은 별도 채널로 확인하세요.** 카드와 지문을 같은 경로로 받으면 중간자 공격을 막을 수 없습니다. -- **감사 로그의 한계를 이해해야 합니다.** 각 항목은 코어의 감사 키로 서명되므로, 코어를 쓸 수 없는 공격자(디스크 탈취, 변조된 백업 등)는 로그를 위조하거나 재계산할 수 없습니다. 반면 노드 사용자 권한으로 코어를 기동할 수 있으면 유효한 서명을 만들 수 있습니다. 또 로그 끝부분을 잘라 내는 것은 외부 기준점이 없으면 탐지할 수 없습니다. +- **감사 로그의 한계를 이해해야 합니다.** 각 항목은 코어의 감사 키로 서명되므로, 코어를 쓸 수 없는 공격자(디스크 탈취, 변조된 백업 등)는 로그를 위조하거나 재계산할 수 없습니다. 반면 노드 사용자 권한으로 코어를 기동할 수 있으면 유효한 서명을 만들 수 있습니다. 또 로그 끝부분을 잘라 내거나 이전 로그로 되돌리는 것은, 노드 밖에 기록해 둔 head와 `audit verify --anchor`로 대조해야 탐지할 수 있습니다. - **kmsd는 TLS 개인키를 메모리에 둡니다.** 키 교환 대상인 대칭 키와 신원, KEM, 감사 키는 코어 밖으로 나오지 않지만, 전송용 TLS 키는 예외입니다. - **macOS는 개발용으로 사용하세요.** `mlockall`이 없어 메모리 잠금 없이 실행되고, FIPS 코어도 정적 링크되지 않습니다. 운영은 Linux에서 하십시오. - **PQC 모드 감사 로깅은 빠른 속도로 증가합니다.** ML-DSA 서명으로 인해 항목 하나가 약 7KB입니다. 암/복호화도 한 번마다 항목이 추가됩니다. @@ -339,10 +349,12 @@ $ kms-core --socket /run/pairier/core.sock --root-key-file /path/root.key --pqc | 항목 | 한도 | |---------------------|-------------------------------------------------| | 구성원 id | 영문 소문자, 숫자로 시작하고 소문자, 숫자, `.`, `_`, `-`로 최대 64자 | -| 키 label | 256바이트 | +| 키 label | 256바이트, 제어 문자와 양방향 서식 문자 불가 | | 암호화 평문, AAD | 각각 64KiB | | kmsctl 입력 파일과 stdin | 1MiB | | 봉투 컨텍스트 | 1KiB | +| 송신자별 대기 봉투 | 64개 | +| kmsd 동시 연결 | 64개 | ## 문제 해결 @@ -357,3 +369,7 @@ $ kms-core --socket /run/pairier/core.sock --root-key-file /path/root.key --pqc | `store: object already exists` | 이미 가져온 키이거나 이미 받은 봉투입니다. | | `node: stored metadata does not match the core` | 노드 디렉토리의 레코드가 변조되었습니다. 백업과 감사 로그를 확인해야 합니다. | | `audit: checkpoint signature is invalid` | 감사 로그가 코어 없이 수정되었습니다. | +| `audit: log does not contain the anchored entry` | 기록해 둔 head 이후 로그가 잘렸거나 이전 로그로 되돌려졌습니다. 백업과 노드 접근 기록을 확인해야 합니다. | +| `node: label contains control or bidirectional formatting characters` | 라벨에서 제어 문자와 양방향 서식 문자를 빼야 합니다. | +| `api: push rejected: 429 inbox full for this sender` | 수신자 inbox에 내가 보낸 대기 봉투가 64개 쌓였습니다. 수신자가 수락하거나 거절해야 합니다. | +| `transport: socket directory ... is writable by other users` | 소켓 디렉토리에서 그룹과 기타 사용자의 쓰기 권한을 빼야 합니다. | From 8bea99353abd5307b01c698bdadee08499a86663 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 15 Oct 2026 10:35:58 +0900 Subject: [PATCH 48/48] =?UTF-8?q?=EB=A6=B4=EB=A6=AC=EC=A6=88=20=EB=B0=B0?= =?UTF-8?q?=ED=8F=AC=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20-=20=EB=AF=B8=EB=A6=B4=EB=A6=AC=EC=A6=88?= =?UTF-8?q?=20=EA=B2=BD=EA=B3=A0=20=EB=AC=B8=EA=B5=AC=EB=A5=BC=20=EC=99=B8?= =?UTF-8?q?=EB=B6=80=20=EB=B3=B4=EC=95=88=20=EA=B2=80=ED=86=A0=20=EC=A0=84?= =?UTF-8?q?=20=EA=B2=BD=EA=B3=A0=EB=A1=9C=20=EB=B0=94=EA=BE=B8=EA=B3=A0=20?= =?UTF-8?q?=EC=A7=80=EC=9B=90=20=EB=B2=84=EC=A0=84=20=ED=91=9C=20=EA=B0=B1?= =?UTF-8?q?=EC=8B=A0=20-=20=EC=84=A4=EC=B9=98=20=EC=9A=94=EA=B1=B4?= =?UTF-8?q?=EC=97=90=20=EC=A7=80=EC=9B=90=20=EC=95=84=ED=82=A4=ED=85=8D?= =?UTF-8?q?=EC=B2=98=EC=99=80=20glibc=20=EC=B5=9C=EC=86=8C=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=20=EB=AA=85=EC=8B=9C,=20=EC=B2=B4=ED=81=AC=EC=84=AC?= =?UTF-8?q?=20=EA=B2=80=EC=82=AC=EC=97=90=20--ignore-missing=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20-=20=EC=9E=AC=ED=98=84=20=EB=B9=8C=EB=93=9C?= =?UTF-8?q?=EC=97=90=20=ED=95=84=EC=9A=94=ED=95=9C=20=EB=B9=8C=EB=93=9C=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=EA=B3=BC=20git=20=EC=B2=B4=ED=81=AC=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=A1=B0=EA=B1=B4=20=EB=AA=85=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SECURITY.md | 6 +++--- SECURITY_KR.md | 6 +++--- USAGE.md | 12 ++++++++---- USAGE_KR.md | 12 ++++++++---- 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 04c353b..fba1992 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -7,7 +7,7 @@ > [!WARNING] -> pairier has not been released yet. Even after development is complete, verifying the safety of its features may take additional time. Please use pairier for development or research purposes only. +> pairier has not had an external security review yet, and verifying the safety of its features may take additional time. Please use pairier for development or research purposes only. ## Reporting a vulnerability @@ -26,7 +26,7 @@ Please include the affected component and version (release or commit hash), step | Version | Supported | |---------|-----------| -| 1.0.x | Yes, once released | +| 1.0.x | Yes | | Unreleased commits | No | Security fixes are released only for the latest 1.0.x release. @@ -87,7 +87,7 @@ $ sha256sum --strict -c SHA256SUMS Either form works. The first uses the key file, the second the key string itself. The repository's `scripts/verify.sh` (`make verify-dist`) also checks, beyond the signature and hashes, that every tarball and SBOM in the directory is listed in `SHA256SUMS` and that a tarball for the given version is present. -Releases are also reproducible. `make dist FIPS=1` on Linux with the pinned toolchains produces a byte-identical tarball, so you can rebuild and compare `SHA256SUMS` instead of, or in addition to, checking the signature. +Releases are also reproducible. Running `make dist FIPS=1` in a git checkout of the release tag on Ubuntu 24.04 (GCC 13.3) with the pinned Rust and Go toolchains produces byte-identical tarballs, so you can rebuild and compare the matching lines of `SHA256SUMS` instead of, or in addition to, checking the signature. Build inside a git checkout rather than an exported source tree, because Go records the commit in `kmsd` and `kmsctl` and the commit time sets the archive timestamps. A different C compiler, such as GCC 12 on Debian 12, produces a different `kms-core`. ## Cryptography diff --git a/SECURITY_KR.md b/SECURITY_KR.md index b367b9b..2b93cbd 100644 --- a/SECURITY_KR.md +++ b/SECURITY_KR.md @@ -7,7 +7,7 @@ > [!WARNING] -> 현재 pairier는 릴리즈가 공개되지 않았으며, 그 이전에 개발이 완료되더라도 기능의 안전성 검증을 위해 시간이 소요될 수 있습니다. pairier는 개발 또는 연구용으로만 사용해주시길 바랍니다. +> pairier는 아직 외부 보안 검토를 받지 않았으며, 기능의 안전성 검증을 위해 시간이 더 소요될 수 있습니다. pairier는 개발 또는 연구용으로만 사용해주시길 바랍니다. ## 취약점 신고 @@ -26,7 +26,7 @@ | 버전 | 지원 여부 | |------|-----------| -| 1.0.x | 릴리즈 이후 지원 | +| 1.0.x | 지원 | | 릴리즈되지 않은 커밋 | 지원하지 않음 | 보안 수정은 최신 1.0.x 릴리즈에만 제공됩니다. @@ -87,7 +87,7 @@ $ sha256sum --strict -c SHA256SUMS 두 방식 모두 동일하게 동작합니다. 첫 번째는 키 파일을, 두 번째는 키 문자열 자체를 사용합니다. 저장소의 `scripts/verify.sh`(`make verify-dist`)는 서명과 해시 외에도, 디렉토리의 tarball과 SBOM이 모두 `SHA256SUMS`에 들어 있는지와 지정한 버전의 tarball이 있는지까지 확인합니다. -릴리즈는 재현 가능합니다. 고정된 툴체인으로 Linux에서 `make dist FIPS=1`을 실행하면 바이트 단위로 동일한 tarball이 생성되므로, 서명 확인 대신 또는 서명 확인과 함께 직접 빌드해 `SHA256SUMS`를 비교할 수 있습니다. +릴리즈는 재현 가능합니다. Ubuntu 24.04(GCC 13.3)에서 고정된 Rust와 Go 툴체인으로 릴리즈 태그의 git 체크아웃 안에서 `make dist FIPS=1`을 실행하면 바이트 단위로 동일한 tarball이 생성되므로, 서명 확인 대신 또는 서명 확인과 함께 직접 빌드해 `SHA256SUMS`의 해당 줄을 비교할 수 있습니다. 내보낸 소스 트리가 아니라 git 체크아웃 안에서 빌드해야 합니다. Go가 `kmsd`와 `kmsctl`에 커밋 정보를 기록하고, 커밋 시각이 아카이브 타임스탬프가 되기 때문입니다. Debian 12의 GCC 12처럼 C 컴파일러가 다르면 `kms-core`도 달라집니다. ## 암호 diff --git a/USAGE.md b/USAGE.md index 6dd2028..f946b57 100644 --- a/USAGE.md +++ b/USAGE.md @@ -9,7 +9,7 @@ pairier is a KMS for securely generating and exchanging symmetric keys between members. Each member keeps their own node directory, and key material exists in plaintext only inside each member's own security core (`kms-core`). To send a key to another member, the sender seals it to the recipient's KEM public key and delivers it in an envelope signed with the sender's identity key. > [!WARNING] -> pairier has not been released yet. Even after development is complete, verifying the safety of its features may take additional time. Please use pairier for development or research purposes only. +> pairier has not had an external security review yet, and verifying the safety of its features may take additional time. Please use pairier for development or research purposes only. ## Components @@ -52,14 +52,18 @@ kmsctl 1.0.0 (go go1.27.2, go_fips=true) ## Installation +Release tarballs are provided for Linux amd64 and arm64 and require glibc 2.38 or later, such as Ubuntu 24.04 or Debian 13. `kmsd` and `kmsctl` are static binaries, and `kms-core` links glibc dynamically. No macOS binaries are provided, so build from source for development (see [Build](#build)). + The distribution tarball contains `kms-core`, `kmsd`, `kmsctl`, `VERSION`, `sbom.cdx.json`, `LICENSE` and `NOTICE`. Before extracting it, verify the minisign signature on `SHA256SUMS` and then the checksums. The public key is `minisign.pub` in the repository root; see [SECURITY.md](SECURITY.md) for how to check it. ```bash $ minisign -Vm SHA256SUMS -p minisign.pub -$ sha256sum -c SHA256SUMS +$ sha256sum --strict --ignore-missing -c SHA256SUMS $ tar -xzf pairier-1.0.0-linux-amd64.tar.gz ``` +`SHA256SUMS` lists the files for both architectures, and `--ignore-missing` skips the ones you did not download. + > [!TIP] > `kmsctl` and `kmsd` use the `kms-core` in their own directory by default. If it is located elsewhere, you can set it with the `--core` flag or the `KMS_CORE` environment variable. The node directory can be set with the `--dir` flag or the `KMS_DIR` environment variable. @@ -340,9 +344,9 @@ The directory that holds the socket must be owned by the core user or root. A di - **The audit log grows quickly in PQC mode.** ML-DSA signatures make each entry about 7KB, and every encryption and decryption also adds an entry. > [!CAUTION] -> pairier has not been released yet. Even after development is complete, verifying the safety of its features may take additional time. Please use pairier for development or research purposes only. +> pairier has not had an external security review yet, and verifying the safety of its features may take additional time. Please use pairier for development or research purposes only. > -> **The items above are provisional until a stable release is published. Security requirements may increase significantly as verification progresses.** +> **The items above may change as verification progresses, and security requirements may increase significantly.** ## Limits diff --git a/USAGE_KR.md b/USAGE_KR.md index a8d41c9..0d08557 100644 --- a/USAGE_KR.md +++ b/USAGE_KR.md @@ -9,7 +9,7 @@ pairier는 구성원 간에 대칭 키를 안전하게 생성하고 교환하기 위한 KMS입니다. 구성원마다 자기 노드 디렉토리를 두고, 키 재료는 항상 각자의 보안 코어(`kms-core`) 안에서만 평문으로 존재합니다. 다른 구성원에게 키를 보낼 때는 수신자의 KEM 공개키로 봉인하고 송신자의 신원 키로 서명한 봉투(envelope)를 전달합니다. > [!WARNING] -> 현재 pairier는 릴리즈가 공개되지 않았으며, 그 이전에 개발이 완료되더라도 기능의 안전성 검증을 위해 시간이 소요될 수 있습니다. pairier는 개발 또는 연구용으로만 사용해주시길 바랍니다. +> pairier는 아직 외부 보안 검토를 받지 않았으며, 기능의 안전성 검증을 위해 시간이 더 소요될 수 있습니다. pairier는 개발 또는 연구용으로만 사용해주시길 바랍니다. ## 구성 요소 @@ -52,14 +52,18 @@ kmsctl 1.0.0 (go go1.27.2, go_fips=true) ## 설치 +배포 tarball은 Linux amd64와 arm64용으로 제공되며 glibc 2.38 이상이 필요합니다(Ubuntu 24.04, Debian 13 등). `kmsd`와 `kmsctl`은 정적 바이너리이고, `kms-core`는 glibc를 동적 링크합니다. macOS 바이너리는 제공하지 않기 때문에 개발용으로는 소스에서 직접 빌드하세요([빌드](#빌드) 참고). + 배포 tarball에는 `kms-core`, `kmsd`, `kmsctl`, `VERSION`, `sbom.cdx.json`, `LICENSE`, `NOTICE`가 들어 있습니다. 해제 전에 `SHA256SUMS`의 minisign 서명(signature)을 먼저 확인하고, 그다음 체크섬(checksum)을 확인하세요. 공개키는 저장소 루트의 `minisign.pub`이며, 확인 방법은 [SECURITY_KR.md](SECURITY_KR.md)를 참고하세요. ```bash $ minisign -Vm SHA256SUMS -p minisign.pub -$ sha256sum -c SHA256SUMS +$ sha256sum --strict --ignore-missing -c SHA256SUMS $ tar -xzf pairier-1.0.0-linux-amd64.tar.gz ``` +`SHA256SUMS`에는 두 아키텍처의 파일이 모두 들어 있으며, `--ignore-missing`은 받지 않은 파일을 건너뜁니다. + > [!TIP] > `kmsctl`과 `kmsd`는 같은 디렉토리에 있는 `kms-core`를 기본으로 사용합니다. 다른 위치에 둔 경우 `--core` 플래그 또는 `KMS_CORE` 환경 변수로 지정할 수 있습니다. 노드 디렉토리는 `--dir` 플래그 또는 `KMS_DIR` 환경 변수로 지정할 수 있습니다. @@ -340,9 +344,9 @@ $ kms-core --socket /run/pairier/core.sock --root-key-file /path/root.key --pqc - **PQC 모드 감사 로깅은 빠른 속도로 증가합니다.** ML-DSA 서명으로 인해 항목 하나가 약 7KB입니다. 암/복호화도 한 번마다 항목이 추가됩니다. > [!CAUTION] -> 현재 pairier는 릴리즈가 공개되지 않았으며, 그 이전에 개발이 완료되더라도 기능의 안전성 검증을 위해 시간이 소요될 수 있습니다. pairier는 개발 또는 연구용으로만 사용해주시길 바랍니다. +> pairier는 아직 외부 보안 검토를 받지 않았으며, 기능의 안전성 검증을 위해 시간이 더 소요될 수 있습니다. pairier는 개발 또는 연구용으로만 사용해주시길 바랍니다. > -> **위 항목들은 안정된 릴리즈가 출시되기 전까지는 형식상 존재합니다. 검증이 진행됨에 따라 필요한 보안 요구사항이 충분히 증가될 수 있습니다.** +> **위 항목들은 검증이 진행됨에 따라 언제든 바뀔 수 있으며, 필요한 보안 요구사항이 충분히 증가될 수 있습니다.** ## 한도