diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..8d75b2e
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,163 @@
+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
+ - 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-core
+ - name: test
+ 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
+ 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: |
+ 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
+ - 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 advisories licenses bans sources
+ - 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/*.cdx.json
+ dist/SHA256SUMS
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/
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/Makefile b/Makefile
index 5dc515b..89e3783 100644
--- a/Makefile
+++ b/Makefile
@@ -17,23 +17,31 @@ 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)
+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 sign verify-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 -o bin/ ./cmd/...
+ cd $(HOST_DIR) && $(GO) build -ldflags "$(GO_LDFLAGS)" -o bin/ ./cmd/...
test: test-core test-host
@@ -42,7 +50,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) && CGO_ENABLED=1 KMS_CORE_BIN=$(KMS_CORE_BIN) $(GO) test -race ./...
lint: lint-core lint-host
@@ -66,6 +74,24 @@ update:
$(CARGO_NET) update $(CORE_MANIFEST)
cd $(HOST_DIR) && $(GO_NET) get -u ./... && $(GO_NET) mod tidy
+dist: build
+ mkdir -p $(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
+
+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"
+
clean:
cd $(CORE_DIR) && $(CARGO) clean
- rm -rf $(HOST_DIR)/bin
+ rm -rf $(HOST_DIR)/bin $(DIST_DIR)
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/README.md b/README.md
new file mode 100644
index 0000000..3ab9cb4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,54 @@
+# 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
+ - `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
+
+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.
+
+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 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
new file mode 100644
index 0000000..06a2d19
--- /dev/null
+++ b/README_KR.md
@@ -0,0 +1,54 @@
+# 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` 코어 프로세스 기동과 클라이언트
+ - `internal/store`, `internal/node` 노드 디렉토리와 키, 구성원, 봉투 관리
+ - `internal/exchange` 신원 카드와 키 교환 봉투 형식
+ - `internal/audit` 해시 체인 감사 로그
+ - `internal/auth`, `internal/api` 카드에 고정된 mTLS와 HTTP API
+ - `cmd/kmsd`, `cmd/kmsctl` 데몬과 관리 도구
+- `conformance/wire/` Rust와 Go가 함께 통과해야 하는 프로토콜 적합성 벡터
+
+## 기능
+
+코어(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, 라벨)는 모두 인증됩니다.
+
+이 외에도 다음과 같은 중요한 기능이 포함됩니다.
+
+- 검인증 암호 모듈: 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`로 빌드하며 외부 의존성이 없습니다.
+
+## 빌드 및 실행
+
+이 내용에 관해서는 [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..fba1992
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,96 @@
+# Security policy
+
+
+
+[한국어](SECURITY_KR.md) | English
+
+
+
+> [!WARNING]
+> 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
+
+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 |
+| 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. 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.
+- 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
+- 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 --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. 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
+
+- 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..2b93cbd
--- /dev/null
+++ b/SECURITY_KR.md
@@ -0,0 +1,96 @@
+# 보안 정책
+
+
+
+한국어 | [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`를 주지 마세요.
+- 감사 로그는 항목마다 코어의 감사 키로 서명한 해시 체인입니다. 노드 사용자 권한으로 코어를 기동할 수 있으면 유효한 항목을 만들 수 있습니다. 로그 끝부분을 잘라 내거나 이전 로그로 되돌리는 것은, 노드 밖에 기록해 둔 head와 `kmsctl audit verify --anchor`로 대조할 때만 탐지됩니다.
+- macOS에서는 코어가 메모리 잠금 없이 실행되고 FIPS 코어도 정적 링크되지 않습니다. macOS는 개발용입니다.
+- `make audit`는 로컬 RustSec 권고 데이터베이스를 사용하며, 네트워크가 연결된 장비에서 갱신해야 합니다.
+- Rust 와이어 디코더 변이 테스트는 시드 기반이며 커버리지 유도 방식이 아닙니다.
+- Linux x86_64는 CI에서만 검증됩니다. 오프라인 검증은 Linux arm64에서 수행했습니다.
+
+## 릴리즈 검증
+
+릴리즈 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 --strict -c SHA256SUMS
+```
+
+두 방식 모두 동일하게 동작합니다. 첫 번째는 키 파일을, 두 번째는 키 문자열 자체를 사용합니다. 저장소의 `scripts/verify.sh`(`make verify-dist`)는 서명과 해시 외에도, 디렉토리의 tarball과 SBOM이 모두 `SHA256SUMS`에 들어 있는지와 지정한 버전의 tarball이 있는지까지 확인합니다.
+
+릴리즈는 재현 가능합니다. 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`도 달라집니다.
+
+## 암호
+
+- 코어: 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
new file mode 100644
index 0000000..f946b57
--- /dev/null
+++ b/USAGE.md
@@ -0,0 +1,379 @@
+# 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 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
+
+| 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
+
+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 --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.
+
+## 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`. Labels cannot contain control characters such as a newline, or bidirectional formatting characters such as U+202E.
+
+### 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. 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
+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
+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
+
+| 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 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
+
+| 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.
+
+- 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` | `$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
+{
+ "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 -> 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`.
+- 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`, 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
+
+```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 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.
+
+## 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, 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.
+
+> [!CAUTION]
+> 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 may change as verification progresses, and security requirements may increase significantly.**
+
+## 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, 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. |
+| `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
new file mode 100644
index 0000000..0d08557
--- /dev/null
+++ b/USAGE_KR.md
@@ -0,0 +1,379 @@
+# pairier 사용법
+
+
+
+한국어 | [English](USAGE.md)
+
+
+
+pairier는 구성원 간에 대칭 키를 안전하게 생성하고 교환하기 위한 KMS입니다. 구성원마다 자기 노드 디렉토리를 두고, 키 재료는 항상 각자의 보안 코어(`kms-core`) 안에서만 평문으로 존재합니다. 다른 구성원에게 키를 보낼 때는 수신자의 KEM 공개키로 봉인하고 송신자의 신원 키로 서명한 봉투(envelope)를 전달합니다.
+
+> [!WARNING]
+> pairier는 아직 외부 보안 검토를 받지 않았으며, 기능의 안전성 검증을 위해 시간이 더 소요될 수 있습니다. pairier는 개발 또는 연구용으로만 사용해주시길 바랍니다.
+
+## 구성 요소
+
+| 바이너리 | 역할 |
+|------------|---------------------------------------------------------------|
+| `kms-core` | Rust 보안 코어로, 루트 키와 모든 암호 연산을 담당하며 호스트가 자식 프로세스로 기동합니다. |
+| `kmsctl` | 구성원이 쓰는 명령줄 도구입니다. 노드 초기화, 구성원 등록, 키 생성과 교환, 암/복호화를 수행합니다. |
+| `kmsd` | 다른 구성원이 보낸 봉투를 mTLS로 받는 데몬(daemon)입니다. 온라인 전달을 사용할 때에만 필요합니다. |
+
+`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 전체 feature 테스트 + 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은 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 --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` 환경 변수로 지정할 수 있습니다.
+
+## 빠른 시작
+
+다음은 예시로, `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
+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)을 사용합니다.
+
+> [!TIP]
+> 한 그룹 안에서는 모든 구성원이 같은 모드를 사용하는 것을 권장합니다. pqc 노드는 일반 카드를 등록할 수 있지만, pqc 없이 초기화한 노드는 pqc 카드를 검증할 수 없어 등록을 거부합니다.
+
+### 2. 신원 카드 교환과 지문 확인
+
+```bash
+alice$ kmsctl --dir ~/.pairier card --out alice.card
+member alice
+fingerprint 664e-c20d-...-ed0f
+```
+
+카드 파일은 이메일이나 USB 등 어떤 경로로 전달해도 됩니다. 단, **지문(fingerprint)은 반드시 카드와 다른 채널**(대면, 전화, 서명된 메시지 등)로 확인해야 합니다. 지문이 신뢰의 유일한 근거이며, 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`가 포함되어 있어야 합니다. 라벨에는 줄바꿈 같은 제어 문자와 U+202E 같은 양방향 서식 문자를 쓸 수 없습니다.
+
+### 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`를 실행해 둔다면 송신자가 직접 밀어(push) 넣을 수 있습니다.
+
+```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에 보관됩니다. 수신자가 확인 후 수락하거나 거절할 수 있습니다. 송신자 한 명당 대기 중인 봉투는 64개까지 보관하며, 수락하거나 거절해 자리가 비어야 다음 봉투를 받습니다.
+
+```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 + tag 16). `--aad` 플래그는 암호화와 복호화 과정에서 반드시 같아야 합니다. `bob`은 `decrypt` 권한만 받았기 때문에 `encrypt`를 시도하는 경우 `usage denied`로 거부됩니다.
+
+### 7. 감사 로그 검증
+
+```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 명령 참조
+
+### 전역 옵션
+
+| 옵션 | 기본값 | 설명 |
+|---------------------------|---------------------------------------|-----------------|
+| `--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 key list --dir X`는 같으며, 양쪽에 모두 설정하면 명령 뒤의 값이 적용됩니다.
+
+### 명령
+
+| 명령 | 설명 |
+|---------------------------------------------------------------|----------------------------------------------------------------------------------|
+| `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` 권한으로 만들어집니다. 종료 코드는 다음과 같습니다.
+
+- 성공 `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` | `$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
+{
+ "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`로, 같은 송신자의 대기 봉투가 64개에 이르면 `429`로 응답합니다. kmsd는 동시에 64개 연결까지 처리하고, 그 이상은 앞선 연결이 끝날 때까지 대기시킵니다.
+
+### 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` | 소켓 경로에서 uid가 맞는 연결 하나만 받은 뒤 경로를 삭제. 다른 uid의 연결은 기록 후 끊고 계속 대기 |
+| `--root-key-file PATH` | 32바이트 일반 파일만 허용. 현재 사용자 소유, 그룹과 기타 권한 없음, 심볼릭 링크와 추가 하드 링크 없음 |
+| `--ephemeral-root` | 메모리에만 있는 임시 루트 키 (재시작하면 blob을 열 수 없음) |
+| `--pqc` | PQC 알고리즘 활성화 (pqc feature로 빌드한 경우) |
+| `--allow-unlocked-memory` | `mlockall`을 쓸 수 없는 환경에서 계속 진행 |
+| `--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`는 노드 디렉토리의 루트 키로 코어를 직접 기동하는 방식만 지원합니다.
+
+## 노드 디렉토리
+
+```text
+/
+ root.key 루트 키 (0400) - 모든 키 blob을 여는 유일한 비밀(secret)
+ node.json 구성원 id와 신원, KEM, 감사 키 id
+ keys/ 키 레코드 - 키 재료는 루트 키로 봉인된 blob으로만 저장
+ members/ 등록된 구성원 카드와 지문
+ inbox/ kmsd가 받은 봉투
+ tls.json TLS 인증서와 코어로 암호화한 TLS 개인키
+ audit.log 해시 체인 감사 로그 (항목마다 감사 키 서명)
+```
+
+- 디렉토리는 `0700`이고 현재 사용자 소유여야 하며, 파일에는 그룹과 기타 권한이 없어야 합니다. 심볼릭 링크는 열지 않습니다.
+- 저장된 메타데이터와 공개키는 사용할 때마다 코어가 blob에서 다시 계산한 값과 대조합니다.
+- 백업에는 `root.key`가 포함되기 때문에 **백업 매체도 루트 키와 같은 수준으로 보호**해야 합니다.
+
+## 보안 운영 주의 사항
+
+- **지문은 별도 채널로 확인하세요.** 카드와 지문을 같은 경로로 받으면 중간자 공격을 막을 수 없습니다.
+- **감사 로그의 한계를 이해해야 합니다.** 각 항목은 코어의 감사 키로 서명되므로, 코어를 쓸 수 없는 공격자(디스크 탈취, 변조된 백업 등)는 로그를 위조하거나 재계산할 수 없습니다. 반면 노드 사용자 권한으로 코어를 기동할 수 있으면 유효한 서명을 만들 수 있습니다. 또 로그 끝부분을 잘라 내거나 이전 로그로 되돌리는 것은, 노드 밖에 기록해 둔 head와 `audit verify --anchor`로 대조해야 탐지할 수 있습니다.
+- **kmsd는 TLS 개인키를 메모리에 둡니다.** 키 교환 대상인 대칭 키와 신원, KEM, 감사 키는 코어 밖으로 나오지 않지만, 전송용 TLS 키는 예외입니다.
+- **macOS는 개발용으로 사용하세요.** `mlockall`이 없어 메모리 잠금 없이 실행되고, FIPS 코어도 정적 링크되지 않습니다. 운영은 Linux에서 하십시오.
+- **PQC 모드 감사 로깅은 빠른 속도로 증가합니다.** ML-DSA 서명으로 인해 항목 하나가 약 7KB입니다. 암/복호화도 한 번마다 항목이 추가됩니다.
+
+> [!CAUTION]
+> pairier는 아직 외부 보안 검토를 받지 않았으며, 기능의 안전성 검증을 위해 시간이 더 소요될 수 있습니다. pairier는 개발 또는 연구용으로만 사용해주시길 바랍니다.
+>
+> **위 항목들은 검증이 진행됨에 따라 언제든 바뀔 수 있으며, 필요한 보안 요구사항이 충분히 증가될 수 있습니다.**
+
+## 한도
+
+| 항목 | 한도 |
+|---------------------|-------------------------------------------------|
+| 구성원 id | 영문 소문자, 숫자로 시작하고 소문자, 숫자, `.`, `_`, `-`로 최대 64자 |
+| 키 label | 256바이트, 제어 문자와 양방향 서식 문자 불가 |
+| 암호화 평문, AAD | 각각 64KiB |
+| kmsctl 입력 파일과 stdin | 1MiB |
+| 봉투 컨텍스트 | 1KiB |
+| 송신자별 대기 봉투 | 64개 |
+| kmsd 동시 연결 | 64개 |
+
+## 문제 해결
+
+| 메시지 | 원인 및 조치 |
+|----------------------------------------------------------------------------------|------------------------------------------------------------------------------|
+| `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` | 감사 로그가 코어 없이 수정되었습니다. |
+| `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` | 소켓 디렉토리에서 그룹과 기타 사용자의 쓰기 권한을 빼야 합니다. |
diff --git a/conformance/wire/gen_v1.py b/conformance/wire/gen_v1.py
new file mode 100644
index 0000000..e8dab95
--- /dev/null
+++ b/conformance/wire/gen_v1.py
@@ -0,0 +1,126 @@
+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") + 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"))),
+ ("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) + 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"),
+ ("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..b57bd51 100644
--- a/conformance/wire/v1.txt
+++ b/conformance/wire/v1.txt
@@ -27,6 +27,14 @@ 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 4b575031020000110000000e0000002d00010000010300000010000102030405060708090a0b0c0d0e0f0000000b74656e616e742d612f646200000000
+frame key_inspect_response_asym 4b57503102000011000000120000006a00300000008000000010101112131415161718191a1b1c1d1e1f000000076b656d2f626f620000004104808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf
+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 +67,11 @@ 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 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
+reject_body key_import_response_short_id 0x0031 rsp 0000000f00000000000000000000000000000000000028a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c700010000000200000000
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..44d170f 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -3,9 +3,10 @@ resolver = "3"
members = ["crates/*"]
[workspace.package]
-version = "0.1.0"
+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-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..6eac6bd 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(())
+ }
}
}
@@ -56,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) => {
@@ -79,7 +84,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/core/crates/kms-core/src/server.rs b/core/crates/kms-core/src/server.rs
index 831959b..62e7ce3 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,50 @@ 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,
+ public_key: &info.public_key,
+ })
+ }
+
+ 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 +279,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 +519,176 @@ 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");
+ 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);
+
+ 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-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();
+}
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-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/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
new file mode 100644
index 0000000..0a7b565
--- /dev/null
+++ b/core/crates/kms-engine/src/export.rs
@@ -0,0 +1,209 @@
+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::blob;
+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)?;
+ blob::check_label(label)?;
+ 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);
+ }
+ blob::check_label(e.label)?;
+ 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,
+ })
+}
+
+#[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/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..b5a2ea4 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,221 @@ 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;
+#[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);
+pub(crate) type Encapsulated = (Vec, Secret);
+
+pub(crate) fn generate(alg: Alg) -> Result {
match alg {
- Alg::MlKem768 => Ok(&ML_KEM_768),
- Alg::MlKem1024 => Ok(&ML_KEM_1024),
- _ => Err(Error::InvalidArgument),
+ 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 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);
+ }
+ match alg {
+ 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 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))
}
-pub(crate) fn generate(alg: Alg) -> Result<(Zeroizing>, Vec), Error> {
- let a = kem_alg(alg)?;
+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 +236,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..19715a8 100644
--- a/core/crates/kms-engine/src/lib.rs
+++ b/core/crates/kms-engine/src/lib.rs
@@ -4,14 +4,14 @@ mod aead;
mod alg;
mod blob;
mod error;
+mod export;
mod kdf;
-#[cfg(feature = "pqc")]
mod kem;
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};
@@ -40,6 +40,16 @@ 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)]
+pub struct ImportedKey {
+ pub key_id: [u8; KEY_ID_LEN],
+ pub blob: Vec,
+ pub alg: Alg,
+ pub usage: Usage,
+ pub label: String,
}
#[derive(Debug)]
@@ -86,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)?;
@@ -99,10 +107,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 {
@@ -121,11 +126,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,
})
}
@@ -198,7 +210,6 @@ impl Engine {
Ok(out)
}
- #[cfg(feature = "pqc")]
pub fn encapsulate(
&self,
alg: Alg,
@@ -211,7 +222,68 @@ impl Engine {
kem::encapsulate(alg, encapsulation_key)
}
- #[cfg(feature = "pqc")]
+ 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)
@@ -219,7 +291,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;
@@ -253,6 +331,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");
@@ -336,6 +415,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());
@@ -369,29 +449,244 @@ 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());
+ 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
+ .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)
+ 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)
+ );
+ }
+
+ 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_eq!(e.sign(&k.blob, b"m"), Err(Error::InvalidArgument));
}
+ 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]
diff --git a/core/crates/kms-engine/src/sign.rs b/core/crates/kms-engine/src/sign.rs
index e85aa16..47bbda2 100644
--- a/core/crates/kms-engine/src/sign.rs
+++ b/core/crates/kms-engine/src/sign.rs
@@ -54,7 +54,46 @@ 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),
+ }
+}
+
+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),
}
}
@@ -94,7 +133,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 +160,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-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/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/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/core/crates/kms-wire/src/msg.rs b/core/crates/kms-wire/src/msg.rs
index ba7968f..284d127 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;
@@ -380,3 +382,159 @@ 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,
+ pub public_key: &'a [u8],
+}
+
+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)?;
+ w.write_bytes(self.public_key)
+ }
+
+ 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)?,
+ public_key: r.read_bytes(MAX_PUBLIC_KEY)?,
+ })
+ }
+}
+
+#[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/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}");
+}
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/host/cmd/kmsctl/main.go b/host/cmd/kmsctl/main.go
index 38dd16d..d57e7d8 100644
--- a/host/cmd/kmsctl/main.go
+++ b/host/cmd/kmsctl/main.go
@@ -1,3 +1,720 @@
package main
-func main() {}
+import (
+ "context"
+ "crypto/fips140"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "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"
+)
+
+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
+ 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
+ audit verify [--anchor SEQ:HASH]
+ 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
+ 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 {
+ 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
+ }
+ rest := fs.Args()
+ if len(rest) == 0 {
+ fs.Usage()
+ return 2
+ }
+ if rest[0] == "version" {
+ fmt.Fprintf(stdout, "kmsctl %s (go %s, go_fips=%v)\n", version, runtime.Version(), fips140.Enabled())
+ return 0
+ }
+ 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
+ }
+ }
+ 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:])
+ 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 "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)")
+ }
+ return c.cmdPeerHealth(ctx, args[2:])
+ 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)
+ 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) {
+ 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) {
+ 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")
+ }
+ opts, err := c.options()
+ if err != nil {
+ return 2, err
+ }
+ n, err := node.Init(ctx, 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\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
+}
+
+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) {
+ 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)
+ 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) {
+ 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)
+ 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) 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) {
+ 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)
+ 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) 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: 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, 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
+}
+
+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
+}
+
+const maxInput = 1 << 20
+
+func (c *cli) readIn(path string) ([]byte, error) {
+ 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 b, nil
+}
+
+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..9c348d8
--- /dev/null
+++ b/host/cmd/kmsctl/main_test.go
@@ -0,0 +1,236 @@
+package main
+
+import (
+ "bytes"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+)
+
+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 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 == "" {
+ 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") != 5 {
+ t.Fatalf("bob key list %q", list)
+ }
+ 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)
+ }
+ wd, err := os.Getwd()
+ if err != nil {
+ t.Fatal(err)
+ }
+ rel, err := filepath.Rel(wd, abs)
+ if err != nil {
+ t.Fatal(err)
+ }
+ 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/cmd/kmsd/main.go b/host/cmd/kmsd/main.go
index 38dd16d..9fe22fe 100644
--- a/host/cmd/kmsd/main.go
+++ b/host/cmd/kmsd/main.go
@@ -1,3 +1,214 @@
package main
-func main() {}
+import (
+ "context"
+ "crypto/fips140"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "os"
+ "os/signal"
+ "path/filepath"
+ "runtime"
+ "sync"
+ "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
+ 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
+}
+
+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 (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")
+ 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 (go %s, go_fips=%v)\n", version, runtime.Version(), fips140.Enabled())
+ 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 = *dir
+ }
+ 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
+ }
+ 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, config dir or $KMS_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 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())
+ }
+ errc := make(chan error, 1)
+ go func() { errc <- hs.ServeTLS(limit(ln, maxConns), "", "") }()
+ 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..287781c
--- /dev/null
+++ b/host/cmd/kmsd/main_test.go
@@ -0,0 +1,202 @@
+package main
+
+import (
+ "bytes"
+ "context"
+ "net"
+ "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 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 == "" {
+ 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)
+ }
+}
+
+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/api/api.go b/host/internal/api/api.go
index 778f64e..2060003 100644
--- a/host/internal/api/api.go
+++ b/host/internal/api/api.go
@@ -1 +1,207 @@
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.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),
+ 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..2e18d45
--- /dev/null
+++ b/host/internal/api/api_test.go
@@ -0,0 +1,193 @@
+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)
+ }
+ 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/audit/audit.go b/host/internal/audit/audit.go
index 6fd56fb..4b4a923 100644
--- a/host/internal/audit/audit.go
+++ b/host/internal/audit/audit.go
@@ -1 +1,410 @@
package audit
+
+import (
+ "bufio"
+ "bytes"
+ "crypto/sha256"
+ "encoding/binary"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "io/fs"
+ "os"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "syscall"
+ "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")
+ 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 {
+ 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"`
+ Event string `json:"event"`
+ 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
+ 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, 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
+ }
+ 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)
+ }
+ 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
+ }
+ err = l.catchUp()
+ l.unlock()
+ if err != nil {
+ _ = f.Close()
+ return nil, err
+ }
+ 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, 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()
+ 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()
+ 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 {
+ 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
+ }
+ line = append(line, '\n')
+ n, err := l.f.Write(line)
+ if err != nil {
+ return err
+ }
+ if err := l.f.Sync(); err != nil {
+ return err
+ }
+ l.seq = e.Seq
+ l.prev = e.Hash
+ l.head = e
+ l.offset += int64(n)
+ 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 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, "", 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, "", nil, fmt.Errorf("%w: entry %d is not valid json", ErrCorrupt, seq+1)
+ }
+ if e.Seq != seq+1 || e.Prev != prev {
+ 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, "", 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, "", nil, fmt.Errorf("%w: %v", ErrCorrupt, err)
+ }
+ return seq, prev, head, nil
+}
+
+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, Anchor{}, err
+ }
+ if v != nil {
+ for i := range entries {
+ if err := verifyEntry(v, &entries[i]); err != nil {
+ return 0, Anchor{}, err
+ }
+ }
+ }
+ 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) {
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ var out []Entry
+ seq, prev := uint64(0), genesis
+ sc := bufio.NewScanner(f)
+ sc.Buffer(make([]byte, 0, maxLine), maxLine)
+ for sc.Scan() {
+ s, p, head, err := replayFrom(bytes.NewReader(append(sc.Bytes(), '\n')), seq, prev, false)
+ if err != nil {
+ return nil, err
+ }
+ 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, nil
+}
diff --git a/host/internal/audit/audit_test.go b/host/internal/audit/audit_test.go
new file mode 100644
index 0000000..664384d
--- /dev/null
+++ b/host/internal/audit/audit_test.go
@@ -0,0 +1,304 @@
+package audit
+
+import (
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/sha256"
+ "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, Options{Now: 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, nil)
+ if err != nil || n != 2 {
+ t.Fatalf("verify %d %v", n, err)
+ }
+ l2, err := Open(path, Options{Now: 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, nil); !errors.Is(err, ErrCorrupt) {
+ t.Fatalf("tamper not detected: %v", err)
+ }
+ 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, 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, 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, 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, Options{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer a.Close()
+ b, err := Open(path, Options{})
+ 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, nil); 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)
+ }
+}
+
+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()
+}
+
+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/auth/auth.go b/host/internal/auth/auth.go
index 8832b06..0dfaccf 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 {
+ MemberByTLS(ctx context.Context, memberID string, 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.MemberByTLS(ctx, c.Subject.CommonName, 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.MemberByTLS(ctx, c.Subject.CommonName, 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/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)
+ }
+}
diff --git a/host/internal/core/client.go b/host/internal/core/client.go
index ab7e796..4b2b2c9 100644
--- a/host/internal/core/client.go
+++ b/host/internal/core/client.go
@@ -36,6 +36,22 @@ type GeneratedKey struct {
PublicKey []byte
}
+type KeyInfo struct {
+ Alg uint16
+ Usage uint32
+ KeyID [wire.KeyIDLen]byte
+ Label string
+ PublicKey []byte
+}
+
+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 +148,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, PublicKey: rsp.PublicKey}, 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..bd1aedf 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,77 @@ 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 || 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 {
+ 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()
diff --git a/host/internal/exchange/card.go b/host/internal/exchange/card.go
new file mode 100644
index 0000000..d7ba99b
--- /dev/null
+++ b/host/internal/exchange/card.go
@@ -0,0 +1,189 @@
+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 + 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
+ TLSPublicKey []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.TLSPublicKey) == 0 || len(c.TLSPublicKey) > 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.Bytes(c.TLSPublicKey)
+ 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.TLSPublicKey, 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..78bc0eb
--- /dev/null
+++ b/host/internal/exchange/exchange_test.go
@@ -0,0 +1,217 @@
+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), TLSPublicKey: make([]byte, 91), 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")
+ }
+ 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")
+ }
+ 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, TLSPublicKey: []byte("spki"), 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)
+ }
+}
diff --git a/host/internal/node/node.go b/host/internal/node/node.go
new file mode 100644
index 0000000..7fee344
--- /dev/null
+++ b/host/internal/node/node.go
@@ -0,0 +1,679 @@
+package node
+
+import (
+ "bytes"
+ "context"
+ "crypto/tls"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "sync"
+ "time"
+ "unicode"
+ "unicode/utf8"
+
+ "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"
+ "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")
+ 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
+
+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
+}
+
+const MaxPendingPerSender = 64
+
+type Node struct {
+ inboxMu sync.Mutex
+ opts Options
+ st *store.Store
+ core *core.Client
+ log *audit.Log
+ 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(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 {
+ 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()
+ }
+ 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 (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 err := o.resolveCore(); err != nil {
+ return nil, err
+ }
+ st, err := store.Init(o.Dir)
+ 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
+ }
+ 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 {
+ _ = n.Close()
+ return nil, err
+ }
+ if err := n.createTLS(ctx); err != nil {
+ _ = n.Close()
+ return nil, err
+ }
+ 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
+ }
+ 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
+ }
+ 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 {
+ return err
+ }
+ if err := n.st.PutKey(n.kem); err != nil {
+ return err
+ }
+ 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)
+}
+
+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
+ }
+ 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() {
+ _ = n.Close()
+ return nil, ErrPQCUnavailable
+ }
+ if n.identity, err = n.Key(ctx, info.IdentityKeyID); err != nil {
+ _ = n.Close()
+ return nil, fmt.Errorf("node: identity key: %w", err)
+ }
+ if n.kem, err = n.Key(ctx, info.KemKeyID); err != nil {
+ _ = n.Close()
+ return nil, fmt.Errorf("node: kem key: %w", err)
+ }
+ 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
+ }
+ if err := n.loadTLS(ctx); err != nil {
+ _ = 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
+ }
+ return n, nil
+}
+
+func (n *Node) Close() error {
+ var lerr error
+ if n.log != nil {
+ lerr = n.log.Close()
+ }
+ cerr := n.core.Close()
+ return errors.Join(lerr, cerr)
+}
+
+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
+}
+
+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 ||
+ !bytes.Equal(info.PublicKey, rec.PublicKey) {
+ return nil, ErrMetadata
+ }
+ return rec, nil
+}
+
+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
+ }
+ 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)
+ }
+ 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
+}
+
+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 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 pt, nil
+}
+
+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,
+ TLSPublicKey: n.TLSPublicKey(),
+ 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) {
+ 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
+ }
+ 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) {
+ 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
+ }
+ 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) {
+ 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, ectx, err := n.verifyEnvelope(ctx, raw)
+ 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
+}
+
+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, nil, ErrNotRecipient
+ }
+ card, err := n.Member(ctx, peek.Sender)
+ if err != nil {
+ return nil, nil, err
+ }
+ if env.SigAlg != card.IdentityAlg {
+ 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
+ }
+ return rec, nil
+}
+
+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
+ }
+ 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
+ }
+ 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)
+}
diff --git a/host/internal/node/node_test.go b/host/internal/node/node_test.go
new file mode 100644
index 0000000..db04f9e
--- /dev/null
+++ b/host/internal/node/node_test.go
@@ -0,0 +1,542 @@
+package node
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "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"
+ "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 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 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)
+ 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()
+ 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) != 5 {
+ 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)
+ }
+
+ 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 := 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) {
+ 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(nil); 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) {
+ 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)
+ }
+ 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)
+ }
+
+ 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)
+ }
+}
+
+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)
+ 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.MemberByTLS(ctx, "bob", bob.TLSPublicKey())
+ if err != nil || found.MemberID != "bob" {
+ t.Fatalf("lookup: %+v %v", found, 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 {
+ 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) != 4 {
+ 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)
+ }
+}
+
+func TestLabelsRejectControlCharacters(t *testing.T) {
+ ctx := context.Background()
+ alice, _ := initNode(t, "alice", false)
+ for _, l := range []string{"a\nb", "\x1b[2J", "ab", "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")
+ }
+}
diff --git a/host/internal/node/tls.go b/host/internal/node/tls.go
new file mode 100644
index 0000000..55779be
--- /dev/null
+++ b/host/internal/node/tls.go
@@ -0,0 +1,139 @@
+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) 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
+ }
+ 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
+ }
+ 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 card, nil
+}
diff --git a/host/internal/store/store.go b/host/internal/store/store.go
new file mode 100644
index 0000000..27a58e5
--- /dev/null
+++ b/host/internal/store/store.go
@@ -0,0 +1,508 @@
+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"
+ tlsFile = "tls.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
+ created bool
+}
+
+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"`
+ 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"`
+ KeyCiphertext []byte `json:"key_ciphertext"`
+ 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) {
+ 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)
+ }
+ 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
+ }
+ 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) {
+ 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 !n.valid() {
+ 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 !n.valid() {
+ return nil, ErrCorrupt
+ }
+ 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
+ }
+ 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) || strings.HasPrefix(name, ".") {
+ 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
+ }
+ 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)
+ 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..9850c48
--- /dev/null
+++ b/host/internal/store/store_test.go
@@ -0,0 +1,209 @@
+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)
+ }
+ 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)
+ }
+ 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)
+ }
+ 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 {
+ 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)
+ }
+ 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)
+ }
+}
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)
+ }
+ })
+}
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 86ec32b..0ca989c 100644
--- a/host/internal/wire/msg.go
+++ b/host/internal/wire/msg.go
@@ -12,17 +12,21 @@ const (
MaxCiphertext = MaxPlaintext + 64
MaxMessage = 64 * 1024
MaxRandom = 1024
+ MaxContext = 1024
+ MaxPackage = 8 * 1024
)
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 +38,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 (
@@ -352,6 +357,166 @@ 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
+ PublicKey []byte
+}
+
+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)
+ w.Bytes(m.PublicKey)
+}
+
+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)
+ if m.Label, err = r.String(MaxLabel); err != nil {
+ return err
+ }
+ m.PublicKey, err = r.Bytes(MaxPublicKey)
+ 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:
@@ -362,6 +527,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:
@@ -381,6 +552,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:
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
diff --git a/scripts/dist.sh b/scripts/dist.sh
new file mode 100755
index 0000000..184a5fc
--- /dev/null
+++ b/scripts/dist.sh
@@ -0,0 +1,63 @@
+#!/bin/sh
+set -eu
+
+version="$1"
+core_bin="$2"
+kmsd_bin="$3"
+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)
+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"
+cp "$sbom" "$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"
+ 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"
+ cp "$name/sbom.cdx.json" "$name.cdx.json"
+ rm -rf "$name"
+ if command -v sha256sum >/dev/null 2>&1; then
+ sum="sha256sum"
+ else
+ sum="shasum -a 256"
+ fi
+ $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..391bfc8
--- /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", "--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"]}
+ 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, "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"]),
+ "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())
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..4666480
--- /dev/null
+++ b/scripts/verify.sh
@@ -0,0 +1,72 @@
+#!/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 [ ! -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 --strict -c SHA256SUMS
+else
+ shasum -a 256 --strict -c SHA256SUMS
+fi