hyperledger / hyperledger/fabric-x-sdk
Cryptographic verification for endorsement proposals
- Dominant language
- Go
- Stars
- 3
- Forks
- 5
- Avg merge
- 2d 11h
- Merged PRs (30d)
- 9
Description
## Context
The function `endorsement.Parse` (endorsement/proposal.go)
deserializes a `peer.SignedProposal` and returns an `Invocation` whose `Creator` field is trusted
by every caller, but is never cryptographically verified — the function's own doc comment says so
explicitly (a `TODO: SECURITY WARNING` block). Anyone who can reach an endorser built on this SDK
can submit a proposal with a forged `Creator`, and nothing in the SDK or the downstream commit
pipeline ever re-checks who actually originated the request (the endorsement policy checked at
commit time validates who *endorsed the result*, not who *asked for it*).
The threat model: mTLS authenticates the transport connection, not the application-level identity in
`Creator`. Whether this matters depends on whether the endorser's business logic makes
authorization/attribution decisions based on `Creator` — which is the expected pattern for Fabric-X
custom endorsers (the whole point of this SDK's `endorsement` package). Proposed design, two layers:
1. **Authentication (mandatory when opted in)** — `Creator` must deserialize to an identity whose
certificate chains to a configured, trusted MSP root, and the proposal's signature must verify
against that identity.
2. **Submission policy (optional, permissive by default)** — if configured, the authenticated
identity must additionally satisfy a policy expressed in the same Fabric policy-string DSL
already used elsewhere in this ecosystem (e.g. `fxconfig namespace create --policy="OR(...)"`
in this repo's own Makefile). If unset, any identity that authenticates against a known MSP may
submit — this is a distinct concept from the *endorsement* policy checked later at commit time.
Research confirmed `github.com/hyperledger/fabric-x-common` (already a direct dependency) ships the
real Fabric MSP and policy machinery needed for this — nothing needs to be hand-rolled:
- `msp.LoadVerifyingMspDir(msp.DirLoadParameters{MspDir, MspName})` loads an org's trust config
(root/intermediate CAs, OU config) from a standard MSP folder — the verify-only counterpart to
what `identity.SignerFromMSP` already does for the signing side. `msp.NewMSPManager().Setup([]MSP{...})`
registers several orgs for multi-org trust.
- `protoutil.UnmarshalIdentity(shdr.Creator)` → `*msppb.Identity` → `mspManager.DeserializeIdentity(...)`
→ `msp.Identity`, which has `.Validate()` (confirmed via `msp/mspimplvalidate.go`: this performs
real certificate-chain-to-trusted-root validation and revocation checking) and `.Verify(msg, sig)`.
- `policydsl.FromString("OR('org1.member', ...)")` + `cauthdsl.NewPolicyProvider(mspManager).NewPolicy(...)`
→ `policies.Policy.EvaluateIdentities([]msp.Identity{id})` — the exact same DSL as namespace policies.
- Confirmed by reading the generated proto structs directly: this SDK's `identity.Signer.Serialize()`
(which marshals `fabric-protos-go-apiv2/msp.SerializedIdentity{Mspid: field 1 string, IdBytes: field 2 bytes}`)
is wire-compatible with `fabric-x-common/api/msppb.Identity{MspId: field 1 string, Certificate: field 2 bytes (oneof)}`
— both use the same field numbers/wire types, so `protoutil.UnmarshalIdentity` correctly parses
`Creator` bytes produced by this SDK's own signer.
Blast radius is small: `endorsement.Parse` has exactly one call site in the whole repo
(`fabrictest/peer.go`'s `testPeer.ProcessProposal`), and this change is purely additive — `Parse`'s
signature doesn't change, so nothing breaks. `fabrictest` is documented as a minimal in-memory fake
network with non-cryptographic test signers (`testSigner{}`); it deliberately keeps calling the
plain, unverified `Parse` — upgrading its fake identities to real crypto is out of scope here (noted
as future work) since it would touch a large, unrelated swath of the fast unit-test suite for no
real benefit (the fake network isn't simulating endorsement security).
## Approach
### 1. `identity.TrustStore` — new file `identity/truststore.go`
A verification-side sibling to `identity.Signer`, following the same folder-based, value-returning,
`%w`-wrapped-error conventions as `SignerFromMSP` (identity/msp.go):
```go
type TrustStore struct {
mgr msp.MSPManager // github.com/hyperledger/fabric-x-common/msp
}
// NewTrustStore builds a multi-org trust store from MSP folders, keyed by MSP ID. Each folder
// is a standard Fabric MSP layout (cacerts/, intermediatecerts/, admincerts/, config.yaml) —
// verify-only, unlike SignerFromMSP no keystore/signcerts is required.
func NewTrustStore(orgs map[string]string) (TrustStore, error)
// Verify checks that creator (marshaled msppb.Identity bytes, e.g. SignatureHeader.Creator) names
// a valid identity whose certificate chains to a configured MSP root, and that sig is a valid
// signature over msg from that identity. Returns the verified identity on success.
func (t TrustStore) Verify(creator, msg, sig []byte) (msp.Identity, error)
// Manager returns the underlying MSPManager, for building policy evaluators (see endorsement.NewVerifier).
func (t TrustStore) Manager() msp.MSPManager
```
`NewTrustStore` loops the map calling `msp.LoadVerifyingMspDir(msp.DirLoadParameters{MspDir: dir, MspName: mspID})`
per org (this one call already handles BCCSP setup, config loading, and `MSP.Setup` — confirmed at
`msp/factory.go:107-131`), collects the `[]msp.MSP`, and registers them via `msp.NewMSPManager().Setup(...)`.
`Verify` does: `protoutil.UnmarshalIdentity(creator)` → `t.mgr.DeserializeIdentity(...)` → `id.Validate()`
→ `id.Verify(msg, sig)`, wrapping each failure with context (which step failed) using the existing
`fmt.Errorf("...: %w", err)` style from identity/msp.go.
### 2. `endorsement.Verifier` — new file `endorsement/verifier.go`
```go
// Verifier parses SignedProposals with cryptographic verification: the signature is checked
// against a trusted, multi-org identity store, and (optionally) the signer must satisfy a
// submission policy. Prefer this over the plain Parse function whenever proposals may originate
// outside a fully trusted environment.
type Verifier struct {
trust identity.TrustStore
policy policies.Policy // nil = any identity known to the trust store may submit
}
// NewVerifier builds a Verifier. submissionPolicy is the Fabric policy DSL (e.g.
// `OR('org1.member', 'org2.member')`); pass "" to allow any identity in trust to submit.
// Note a submission policy is evaluated against exactly one identity (the submitter), so
// AND-ing across distinct orgs can never be satisfied — that combinator only makes sense for
// endorsement policies, which aggregate independently-collected signatures.
func NewVerifier(trust identity.TrustStore, submissionPolicy string) (Verifier, error)
// Parse extracts and verifies a SignedProposal: structural integrity and TxID correctness (via
// the plain Parse function), then that the proposal is genuinely signed by the identity named in
// Creator and that identity is trusted, then (if configured) the submission policy.
func (v Verifier) Parse(signedProp *peer.SignedProposal, expectedTime time.Time) (Invocation, error)
```
`Verifier.Parse` calls the existing free `Parse` function first (reused as-is for structural
parsing/TxID/timestamp checks — no duplication), then verifies using **`signedProp.ProposalBytes`
directly** (not a re-marshal of `inv.Proposal` — proto re-encoding isn't guaranteed byte-stable, so
verification must use the exact bytes that were originally signed), then evaluates the policy if
one is configured.
### 3. `Invocation.VerifiedIdentity` — new field in `endorsement/proposal.go`
```go
// VerifiedIdentity is the cryptographically verified identity behind Creator. Only set when the
// Invocation came from Verifier.Parse; nil from the plain Parse function or NewInvocation. Callers
// can use it (e.g. GetMSPIdentifier, GetOrganizationalUnits, SatisfiesPrincipal) to make their own
// fine-grained authorization decisions — the SDK verifies identity, application logic decides access.
VerifiedIdentity msp.Identity
```
Purely additive — existing `Invocation{}` literals and `NewInvocation` are unaffected (zero value is nil).
### 4. Update `Parse`'s doc comment (endorsement/proposal.go)
Replace the `TODO: SECURITY WARNING` block: it currently frames missing verification as an
unfinished implementation. Rewrite it as an intentional, documented scope boundary — `Parse` does
structural validation only and never touches trust; point readers at `Verifier.Parse` for anything
that isn't a fully trusted environment.
### 5. `fabrictest/peer.go`
One-line comment update at the `endorsement.Parse` call site in `ProcessProposal` clarifying that
using the unverified path is intentional (fake in-memory network, non-cryptographic test signers),
not a leftover gap — avoids future confusion once `Verifier` exists elsewhere in the codebase. No
behavioral change.
### 6. Shared test fixtures — new file `internal/msptest/msptest.go`
Neither `identity` nor `endorsement` currently has any test file, and the existing `fixedSigner`/
`testSigner` fakes elsewhere in the repo aren't real crypto (arbitrary bytes, not valid MSP identities)
— nothing reusable exists for testing real signature verification. Add a small internal-only helper:
```go
// GenerateOrg creates a self-signed CA and one signing identity under dir, in the standard Fabric
// MSP folder layout (cacerts/, keystore/, signcerts/) — for tests that need a real ECDSA identity
// chaining to a real trust root, without Docker or committed crypto material.
func GenerateOrg(t *testing.T, dir, mspID string) (caDir, userDir string)
```
Uses `crypto/x509`/`crypto/ecdsa` to build an ephemeral root CA + one leaf cert per call, writes
PEM files in the exact layout `identity.SignerFromMSP` (signing side) and `identity.NewTrustStore`
(verify side, via `msp.LoadVerifyingMspDir`) both already expect — so tests exercise the real
folder-loading code path end-to-end rather than hand-built protos.
### 7. Tests
- `identity/truststore_test.go`: valid identity from a known org verifies successfully; identity
from an org not in the trust store is rejected; tampered message/signature is rejected;
self-signed cert not chaining to any configured root is rejected.
- `endorsement/verifier_test.go`: `Verifier.Parse` happy path populates `VerifiedIdentity`; rejects
a proposal from an untrusted org; with a submission policy configured, rejects an authenticated
identity that doesn't satisfy it while accepting one that does; with no policy configured, accepts
any trusted-org identity (permissive default).
- `endorsement/proposal_test.go` (new — closes the existing zero-coverage gap on `Parse` itself):
regression test for the timestamp-window bug fixed earlier in this session (proposal outside the
±5 minute window relative to `expectedTime` must now actually be rejected).
Both new `_test.go` files use `internal/msptest.GenerateOrg` plus the existing
`identity.SignerFromMSP` to build a real signer, sign a proposal with it via the existing
`endorsement.NewInvocation`/`protoutil.GetSignedProposal`-style helpers, and feed the result through
`Verifier.Parse`.
## Out of scope (explicitly, for a follow-up)
- Upgrading `fabrictest`'s fake network to real ECDSA/MSP identities so its `ProcessProposal` can
exercise `Verifier.Parse` in the existing fast test suite.
- Dynamically sourcing trust roots from a live channel config block instead of static MSP folders.
- Wiring `Verifier` into any reference/sample endorser (lives in the separate `fabric-x-samples` repo).
## Verification
- `go build ./...` and `go vet ./...` — new code compiles cleanly, no new dependencies beyond
`fabric-x-common` sub-packages already in `go.sum` (`msp`, `common/cauthdsl`, `common/policydsl`,
`common/policies`), no `go.mod` changes expected.
- `go test ./identity/... ./endorsement/... -race -v` — run the new tests directly, confirm both
positive and negative (rejection) cases pass, with `-race` since this session already found and
fixed one real data race by enabling it.
- `go test ./... -short -race` — full existing suite must remain green (in particular
`fabrictest`/`integration` tests, since `Parse`'s signature and behavior are unchanged and
`Verifier` is new/additive).
- `make checks` — gofmt/vet/license-header checks on the new files.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with endorsement/proposal.go and identity/msp.go, then trace the sole Parse call in fabrictest/peer.go. Read the fabric-x-common MSP and policy APIs before running the proposed identity and endorsement tests. Done means trusted identities and signatures are accepted, untrusted or tampered proposals are rejected, optional policies are enforced, and existing unverified test behavior remains intentional.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- backend-api-design, cryptography, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 25/100