Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ result = await crawler.execute_tool_call("search_poi", {"query": "Beijing"})

- **`authentication/`**: DID WBA (Web-based Decentralized Identifiers) authentication — DID document creation (`create_did_wba_document`), auth header generation (`DIDWbaAuthHeader`), RS256 JWT signature verification (`DidWbaVerifier`)
- **`direct_e2ee/`**: ANP-P5 private-chat E2EE SDK surface for Go/Rust parity. It owns P5 wire models, JCS AAD builders, X3DH-like initial material, HKDF `kdf_rk`/`kdf_ck`, pending-confirmation bootstrap, Double Ratchet-like session state, and top-level OPK publish/get sidecar handling for product clients/services to consume.
- **`group_e2ee/`**: ANP-P6 group E2EE SDK surface. It owns P6 wire models, recovery KeyPackage/recover-member shapes, group send AAD/JCS helpers, hidden leave-request/process control-plane structs, and shared contract fixtures; real OpenMLS one-shot operations live in `rust/src/bin/anp-mls.rs` and include create/add, recover-member prepare/finalize/abort, remove-member, commit process/finalize/abort, and local leave terminal-state handling, while non-cryptographic contract artifacts remain available only behind explicit test flags.
- Shared P5 direct E2EE vector fixtures live under `testdata/direct_e2ee/`; Go (`golang/direct_e2ee/shared_vectors_test.go`) and Rust (`rust/tests/direct_e2ee_shared_vectors.rs`) must pass the same JSON vectors before product integrations claim cross-language parity.
- Shared P6 group E2EE proof fixtures live under `testdata/group_e2ee/`; Go (`golang/proof/proof_test.go`) and Rust (`rust/tests/proof_tests.rs`) verify the same `did_wba_binding` golden vector and tamper-negative before public discovery work can claim proof parity.
- P5 key-service calls (`direct.e2ee.publish_prekey_bundle` / `direct.e2ee.get_prekey_bundle`) must bind `meta.target.kind="service"` and use the caller DID document's advertised `ANPMessageService.serviceDid`; do not send legacy target-less control-plane requests.
- **`e2e_encryption_v2/`**: Legacy transport-agnostic E2E encryption v2 using HTTP RESTful dict-based messages, ECDHE key exchange, AES-GCM encryption. Session state machine: IDLE → HANDSHAKE_INITIATED → HANDSHAKE_COMPLETING → ACTIVE. Uses `did:wba:` format and snake_case fields (unlike the older `e2e_encryption/` which is WebSocket-coupled with camelCase). Do not use it for new ANP-P5 direct E2EE service behavior.
- **`e2e_encryption/`**: Legacy WebSocket-based E2E encryption (forward compatibility, uses `did:anp:` format)
Expand Down Expand Up @@ -141,6 +143,7 @@ Google Python Style Guide: 4-space indentation, type hints on function signature

- Always use `uv run` prefix when running scripts to ensure correct environment
- Go SDK work under `golang/` must remain pure Go and must not use cgo; prefer cross-platform standard-library or pure-Go dependencies only
- `rust/src/bin/anp-mls.rs` is a one-shot group E2EE binary: JSON request via stdin, JSON response via stdout, logs/errors via stderr. `system version --json-in -` is the stable no-state compatibility probe for packaging/doctor checks. Default real mode requires `--data-dir`, persists OpenMLS private state plus app metadata/idempotency/pending membership commits in local SQLite (`state.db`), validates group-state/cipher claims against local bindings before encrypt/decrypt, redacts decrypted plaintext from operation records, and guards mutations with `state.lock`; contract-test artifacts are only allowed when explicitly enabled and must remain marked `non_cryptographic=true` / `artifact_mode=contract-test`. Remove-member prepares an OpenMLS pending commit that must be finalized after service acceptance or aborted after deterministic rejection; leave is a documented OpenMLS-0.8 local terminal-state path until another active member/service flow advances the MLS epoch for remaining members. `group status` reports pending commit summaries plus `local_epoch`; inactive bindings return `left`/`removed` instead of misleading `active` so one-shot clients can distinguish accepted-but-unfinalized commits, local terminal states, and stale epochs before sending.
- OpenANP `@interface` method names must be unique within a class (tracked by function reference)
- OpenANP `Context` parameter (`ctx: Context`) is auto-injected and excluded from OpenRPC schemas; detected by parameter name `ctx`/`context` or type annotation
- OpenANP `router()` works as both class method (tries no-arg instantiation) and instance method (recommended for constructors with arguments)
Expand Down
36 changes: 36 additions & 0 deletions docs/pr-notes/group-e2ee-p6-conformance-before-discovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Group E2EE Step B P6 Conformance Notes — ANP SDK / anp-mls

## Scope

- Align `anp-mls` real OpenMLS path with P6 before any public discovery claim.
- Add required `ratchet_tree_b64u` export/import for add/welcome processing.
- Bind MLS encrypt/decrypt authenticated data to canonical P6 send AAD and keep a stable golden vector in the Rust SDK helper.
- Add hidden PR-B3 recover-member contract shapes and `anp-mls` contract checks for prepare/finalize/abort so downstream CLI/service integration can share stable pending-commit fields.
- Deepen local DID WBA binding checks against member DID, MLS BasicCredential identity, leaf signature key, verification method, validity window, and optional proof shape.
- Treat `group_state_ref.group_state_version` as service state for AAD only; MLS epoch validation remains bound to explicit `epoch` so P4 state versions and MLS epochs are not conflated.

## Public discovery stance

- This is still hidden/test-only integration work.
- Do not advertise `anp.group.e2ee.v1` / `group-e2ee` from ANP SDK release notes until message-service discovery gate and security review pass in a separate enablement PR.

## Config / migration impact

- No ANP database migration.
- No Go/CLI CGO dependency is introduced; product clients still invoke `anp-mls` via JSON stdin/stdout.
- Contract-test artifacts remain available only when explicitly enabled and remain marked non-cryptographic.

## Fresh validation evidence

- `cargo fmt --manifest-path rust/Cargo.toml --check`
- `cargo test --manifest-path rust/Cargo.toml group_e2ee --all-targets` → passed: Rust P6 helper tests 3 passed; real `anp-mls` group E2EE tests 12 passed.
- PR-B3 recovery sync adds focused evidence for `recover-member-prepare/finalize/abort` contract paths plus Rust/Go recover-member wire models; keep this evidence fresh in the implementing PR.

## Rollback

- Revert the Step B `anp-mls` changes if the service/CLI side must return to the previous hidden minimal loop. This would remove ratchet-tree-required welcome processing and MLS AAD tamper rejection, so public discovery must remain disabled.

## Caveats

- Still no External Commit, multi-device membership sync, group attachment E2EE, cloud snapshot, public recover-member discovery, or HTTP `anp-mls serve` requirement.
- No k1 DID compatibility is included.
34 changes: 34 additions & 0 deletions docs/pr-notes/group-e2ee-v1-pr-closeout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Group E2EE v1 PR Closeout Notes — ANP SDK / anp-mls

## Scope

- Close stale wording in the Rust P6 group E2EE helper so it reflects the current split: P6 wire/canonicalization helpers in `rust/src/group_e2ee/`, real OpenMLS operations in the `anp-mls` binary.
- Keep PR-B3 recover-member contract surfaces synchronized for `recover-member-prepare`, `recover-member-finalize`, and `recover-member-abort`; this remains hidden/test-only and is not public discovery.
- Keep contract-test artifacts explicitly non-cryptographic; do not claim public discovery readiness.

## Commits / branch context

- Current branch is ahead of origin with recent Group E2EE work through `25cdb83 Harden anp-mls release probes and local bindings`.
- This closeout only adjusts wording/docs for the existing hidden/test-only v1 minimal loop.

## Config / migration impact

- No ANP SDK database migration or config default changes.
- No k1 DID compatibility work.

## Validation

- Fresh evidence collected in this Ralph pass:
- `cargo test --manifest-path Cargo.toml group_e2ee -- --nocapture` from `anp/anp/rust` → passed: P6 helper tests 2 passed and real `anp-mls` Group E2EE tests 9 passed.
- Stale wording grep no longer finds the old “no MLS implementation here yet” disclaimer in current Rust P6 helper comments.

## Rollback

- Revert the wording-only commit if needed. No schema or wire behavior rollback is required.

## Caveats

- Group E2EE v1 remains a minimal same-domain/single-device loop.
- Recover-member prepare/finalize/abort is available only as the hidden PR-B3 recovery lifecycle surface; do not route public add-member or discovery behavior through it.
- Step B P6 conformance is tracked in `docs/pr-notes/group-e2ee-p6-conformance-before-discovery.md`; public discovery still requires a separate security-reviewed enablement PR.
- Public discovery remains hidden; do not advertise `anp.group.e2ee.v1` / `group-e2ee` from this PR.
14 changes: 14 additions & 0 deletions golang/docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,20 @@ Common production entry points:
- `MessageServiceDirectE2eeClient.ProcessIncoming`
- `MessageServiceDirectE2eeClient.DecryptHistoryPage`

### `group_e2ee`

Wire models and an `ExecProvider` for `anp.group.e2ee.v1`. Real group E2EE flows are owned by the Rust `anp-mls` one-shot binary, which keeps OpenMLS private state in its local SQLite state directory and receives plaintext through stdin, not argv. PR-B1 safe leave uses hidden/test-only `group.e2ee.leave_request` control-plane objects plus owner/admin processing through an epoch-advancing remove commit; it does not make same-member local-terminal leave a service success. Contract-test artifacts are still available only when explicitly enabled for compatibility tests, and those deterministic artifacts must be marked `non_cryptographic=true` and `artifact_mode=contract-test`.

Key APIs:

- `ExecProvider.Call`
- `GroupKeyPackage`
- `GroupCipherObject`
- `GroupLeaveRequestObject`
- `GroupLeaveRequestProcessObject`
- `GroupStateRef`
- `ApplicationPlaintext`

## Compatibility Notes

- Pure Go implementation only
Expand Down
72 changes: 72 additions & 0 deletions golang/group_e2ee/exec_provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package groupe2ee

import (
"bytes"
"context"
"encoding/json"
"fmt"
"os/exec"
"time"
)

type CommandRunner interface {
Run(ctx context.Context, binary string, args []string, stdin []byte) (stdout []byte, stderr []byte, err error)
}

type OSCommandRunner struct{}

func (OSCommandRunner) Run(ctx context.Context, binary string, args []string, stdin []byte) ([]byte, []byte, error) {
cmd := exec.CommandContext(ctx, binary, args...)
cmd.Stdin = bytes.NewReader(stdin)
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
return stdout.Bytes(), stderr.Bytes(), err
}

type ExecProvider struct {
BinaryPath string
DataDir string
Timeout time.Duration
Runner CommandRunner
}

func (p ExecProvider) Call(ctx context.Context, domain string, action string, req Request) (*Response, error) {
if p.BinaryPath == "" {
p.BinaryPath = "anp-mls"
}
if p.Timeout <= 0 {
p.Timeout = 15 * time.Second
}
runner := p.Runner
if runner == nil {
runner = OSCommandRunner{}
}
body, err := json.Marshal(req)
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(ctx, p.Timeout)
defer cancel()
args := []string{domain, action, "--json-in", "-"}
if p.DataDir != "" {
args = append(args, "--data-dir", p.DataDir)
}
stdout, stderr, err := runner.Run(ctx, p.BinaryPath, args, body)
if err != nil && len(stdout) == 0 {
return nil, fmt.Errorf("anp-mls exec failed: %w: %s", err, string(stderr))
}
var resp Response
if decodeErr := json.Unmarshal(stdout, &resp); decodeErr != nil {
return nil, fmt.Errorf("decode anp-mls response: %w: stderr=%s", decodeErr, string(stderr))
}
if !resp.OK {
if resp.Error != nil {
return &resp, fmt.Errorf("anp-mls error %s: %s", resp.Error.Code, resp.Error.Message)
}
return &resp, fmt.Errorf("anp-mls returned ok=false")
}
return &resp, nil
}
48 changes: 48 additions & 0 deletions golang/group_e2ee/exec_provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package groupe2ee

import (
"context"
"encoding/json"
"strings"
"testing"
)

type recordingRunner struct {
args []string
stdin []byte
}

func (r *recordingRunner) Run(_ context.Context, _ string, args []string, stdin []byte) ([]byte, []byte, error) {
r.args = append([]string(nil), args...)
r.stdin = append([]byte(nil), stdin...)
return []byte(`{"ok":true,"api_version":"anp-mls/v1","request_id":"req-1","result":{"non_cryptographic":true}}`), nil, nil
}

func TestExecProviderPassesPlaintextOnStdinNotArgv(t *testing.T) {
runner := &recordingRunner{}
provider := ExecProvider{BinaryPath: "anp-mls", Runner: runner}
_, err := provider.Call(context.Background(), "message", "encrypt", Request{
APIVersion: "anp-mls/v1",
RequestID: "req-1",
ContractTestEnabled: true,
Params: map[string]any{
"application_plaintext": map[string]any{"text": "super secret"},
},
})
if err != nil {
t.Fatal(err)
}
if strings.Contains(strings.Join(runner.args, " "), "super secret") {
t.Fatalf("plaintext leaked into argv: %#v", runner.args)
}
if !strings.Contains(string(runner.stdin), "super secret") {
t.Fatalf("plaintext request was not sent via stdin: %s", string(runner.stdin))
}
var req Request
if err := json.Unmarshal(runner.stdin, &req); err != nil {
t.Fatal(err)
}
if !req.ContractTestEnabled {
t.Fatal("contract test flag not preserved")
}
}
149 changes: 149 additions & 0 deletions golang/group_e2ee/models.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package groupe2ee

const (
Profile = "anp.group.e2ee.v1"
SecurityProfile = "group-e2ee"
TransportSecurityProfile = "transport-protected"
ContractArtifactMode = "contract-test"
MTISuite = "MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519"
MethodLeaveRequest = "group.e2ee.leave_request"
MethodLeaveRequestProcess = "group.e2ee.process_leave_request"
MethodRecoverMember = "group.e2ee.recover_member"
)

type TargetRef struct {
Kind string `json:"kind"`
DID string `json:"did"`
}

type GroupStateRef struct {
GroupDID string `json:"group_did"`
GroupStateVersion string `json:"group_state_version"`
PolicyHash string `json:"policy_hash,omitempty"`
}

type EnvelopeMeta struct {
ANPVersion string `json:"anp_version"`
Profile string `json:"profile"`
SecurityProfile string `json:"security_profile"`
SenderDID string `json:"sender_did,omitempty"`
Target *TargetRef `json:"target,omitempty"`
OperationID string `json:"operation_id,omitempty"`
MessageID string `json:"message_id,omitempty"`
ContentType string `json:"content_type,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
}

type GroupKeyPackage struct {
KeyPackageID string `json:"key_package_id"`
OwnerDID string `json:"owner_did"`
DeviceID string `json:"device_id,omitempty"`
Purpose string `json:"purpose,omitempty"`
GroupDID string `json:"group_did,omitempty"`
Suite string `json:"suite"`
MLSKeyPackageB64U string `json:"mls_key_package_b64u"`
DIDWBABinding map[string]any `json:"did_wba_binding"`
ExpiresAt string `json:"expires_at,omitempty"`
NonCryptographic bool `json:"non_cryptographic,omitempty"`
ArtifactMode string `json:"artifact_mode,omitempty"`
}

type RecoverMemberTarget struct {
AgentDID string `json:"agent_did"`
DeviceID string `json:"device_id"`
}

type RecoverMemberRequestObject struct {
OperationID string `json:"operation_id"`
GroupDID string `json:"group_did"`
ActorDID string `json:"actor_did"`
Target RecoverMemberTarget `json:"target"`
GroupStateRef GroupStateRef `json:"group_state_ref"`
RecoveryKeyPackageID string `json:"recovery_key_package_id,omitempty"`
GroupKeyPackage *GroupKeyPackage `json:"group_key_package,omitempty"`
CommitB64U string `json:"commit_b64u"`
WelcomeB64U string `json:"welcome_b64u"`
RatchetTreeB64U string `json:"ratchet_tree_b64u,omitempty"`
Epoch string `json:"epoch"`
EpochAuthenticator string `json:"epoch_authenticator,omitempty"`
NonCryptographic bool `json:"non_cryptographic,omitempty"`
ArtifactMode string `json:"artifact_mode,omitempty"`
}

type RecoverMemberFinalizeRequestObject struct {
OperationID string `json:"operation_id,omitempty"`
PendingCommitID string `json:"pending_commit_id"`
}

type RecoverMemberAbortRequestObject struct {
OperationID string `json:"operation_id,omitempty"`
PendingCommitID string `json:"pending_commit_id"`
}

type GroupCipherObject struct {
CryptoGroupIDB64U string `json:"crypto_group_id_b64u"`
Epoch string `json:"epoch"`
PrivateMessageB64U string `json:"private_message_b64u"`
GroupStateRef GroupStateRef `json:"group_state_ref"`
EpochAuthenticator string `json:"epoch_authenticator,omitempty"`
NonCryptographic bool `json:"non_cryptographic,omitempty"`
ArtifactMode string `json:"artifact_mode,omitempty"`
}

type GroupLeaveRequestObject struct {
LeaveRequestID string `json:"leave_request_id"`
GroupDID string `json:"group_did"`
RequesterDID string `json:"requester_did"`
GroupStateRef GroupStateRef `json:"group_state_ref"`
ReasonText string `json:"reason_text,omitempty"`
RequestedAt string `json:"requested_at,omitempty"`
NonCryptographic bool `json:"non_cryptographic,omitempty"`
ArtifactMode string `json:"artifact_mode,omitempty"`
}

type GroupLeaveRequestProcessObject struct {
LeaveRequestID string `json:"leave_request_id"`
GroupDID string `json:"group_did"`
RequesterDID string `json:"requester_did"`
ProcessorDID string `json:"processor_did"`
GroupStateRef GroupStateRef `json:"group_state_ref"`
CryptoGroupIDB64U string `json:"crypto_group_id_b64u"`
Epoch string `json:"epoch"`
CommitB64U string `json:"commit_b64u"`
EpochAuthenticator string `json:"epoch_authenticator,omitempty"`
ReasonText string `json:"reason_text,omitempty"`
NonCryptographic bool `json:"non_cryptographic,omitempty"`
ArtifactMode string `json:"artifact_mode,omitempty"`
}

type ApplicationPlaintext struct {
ApplicationContentType string `json:"application_content_type"`
ThreadID string `json:"thread_id,omitempty"`
ReplyToMessageID string `json:"reply_to_message_id,omitempty"`
Annotations map[string]any `json:"annotations,omitempty"`
Text string `json:"text,omitempty"`
Payload map[string]any `json:"payload,omitempty"`
PayloadB64U string `json:"payload_b64u,omitempty"`
}

type Request struct {
APIVersion string `json:"api_version"`
RequestID string `json:"request_id"`
AgentDID string `json:"agent_did,omitempty"`
DeviceID string `json:"device_id,omitempty"`
ContractTestEnabled bool `json:"contract_test_enabled,omitempty"`
Params map[string]any `json:"params"`
}

type Response struct {
OK bool `json:"ok"`
APIVersion string `json:"api_version"`
RequestID string `json:"request_id"`
Result map[string]any `json:"result,omitempty"`
Error *ErrorObject `json:"error,omitempty"`
}

type ErrorObject struct {
Code string `json:"code"`
Message string `json:"message"`
}
Loading
Loading