vcvalidator

package
v1.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 13, 2026 License: MIT Imports: 24 Imported by: 0

README

validateVC — Verifiable Credential validator step

A beckn-onix processing Step plugin that verifies the W3C Verifiable Credentials embedded in a request body. When enabled it gates the configured beckn action(s) and rejects the request with a NACK if any embedded credential fails verification — the request never reaches routing.

It implements the definition.StepProvider contract and is built as validateVC.so (the .so basename is the plugin id, so pipelines wire it as the validateVC step — the Go package remains vcvalidator). Running inside the handler's step pipeline means rejections go through the same signed-NACK path as every other step (validateSign, validateSchema): the handler builds the NACK envelope and signs it before it is written to the wire.

A credential is any JSON object in the body carrying both a proof and a credentialSubject. This is the combination beckn uses for an embedded VC (for example a credential nested under message.contract.participants[].participantAttributes), so the plugin needs no knowledge of the surrounding message shape.

What it checks

For every embedded credential:

  1. Proof signature — verifies a VC-JWT (proof.jwt) signature against the issuer's public key, resolved from the issuer DID. Supported DID methods:
    • did:keyEd25519 (z6Mk…), P-256 (zDn…), secp256k1 (zQ3…)
    • did:jwk — embedded JWK
    • did:web — fetches https://<host>/[path/]did.json and reads the verification method's publicKeyJwk / publicKeyMultibase / publicKeyBase58
  2. Issuer binding — the JWT signer (the kid controller DID) must equal the credential's declared issuer. A credential signed by anyone other than its issuer is rejected (ISSUER_MISMATCH). The signing algorithm in the JWT header must also match the resolved key's algorithm (alg-confusion protection).
  3. Validity windowvalidFrom / validUntil and the JWT nbf / exp.
  4. Revocation — when credentialStatus is present: StatusList2021 / BitstringStatusList bitstring lookup, a DEDI registry lookup, or a generic revoked indicator.
A note on JSON-LD Data Integrity proofs

Proofs of type Ed25519Signature2020 / DataIntegrityProof (carrying a proofValue rather than a jwt) require RDF canonicalisation (URDNA2015), which this plugin does not implement. With requireProof: true (default) such credentials are rejected; with requireProof: false the signature step is skipped and only the validity window, revocation, and verification-method resolvability are checked.

Outbound fetch hardening

did:web resolution and revocation checks issue HTTP GETs to URLs taken from the request body (the credential's issuer DID and credentialStatus), which is an SSRF surface. The shared HTTP client therefore:

  • blocks non-public destinations — loopback, RFC1918/ULA private, link-local (including the cloud metadata endpoint 169.254.169.254), multicast, unspecified and broadcast addresses. The check runs at the dial layer on the resolved IP, so it also defeats DNS rebinding.
  • caps redirects at 3 hops, each hop passing through the same dial guard.
  • caps the per-request credential count (maxCredentials, default 10): each credential can cost up to two fetches of httpTimeout each, so the cap bounds how long a single request can hold a handler goroutine. Excess is rejected with a Bad Request NACK before any network I/O.

Deployments whose issuers or registries live on a private network (e.g. the DEG devkit's docker network) must opt in explicitly with allowPrivateNetworks: "true" — never do this in production.

NACK failure classes

A rejection is returned to the handler as one of the standard model error types, so the NACK carries the usual error.code plus a message that starts with the machine-readable failure class:

failure class meaning model error → HTTP
INVALID_CREDENTIAL malformed credential / missing issuer BadReqErr → 400
INVALID_PROOF signature invalid, missing, or alg mismatch SignValidationErr → 401
ISSUER_MISMATCH proof signer ≠ declared issuer SignValidationErr → 401
CREDENTIAL_EXPIRED outside validity window SignValidationErr → 401
DID_RESOLUTION_FAILED could not resolve issuer / verification-method DID SignValidationErr → 401
CREDENTIAL_REVOKED revoked per credentialStatus SignValidationErr → 401

The NACK body matches beckn-onix's v2 shape and is signed by the handler (Signature response header) like every other pipeline NACK:

{"message":{"status":"NACK","messageId":"…","error":{"code":"Unauthorized","message":"Signature Validation Error: CREDENTIAL_REVOKED: …"}}}

Configuration

Wired as a steps plugin on a module handler, then referenced by id in the pipeline's steps list (typically after validateSign, before validateSchema):

modules:
  - name: bppTxnReceiver
    path: /bpp/receiver/
    handler:
      type: std
      role: bpp
      plugins:
        steps:
          - id: validateVC
            config:
              enabled: "true"           # master switch
              actions: "confirm"        # REQUIRED — comma list of gated beckn actions
              allowedDidMethods: "key,jwk,web"
              checkExpiry: "true"
              checkRevocation: "true"
              requireProof: "true"      # reject proofs this plugin cannot verify
              failOpen: "false"         # on did:web/revocation network errors: false = reject
              httpTimeout: "10"         # seconds
              maxCredentials: "10"      # cap on embedded credentials per request
              allowPrivateNetworks: "false"  # SSRF guard escape hatch — local/devkit only
              debugLogging: "false"
      steps:
        - validateSign
        - validateVC
        - validateSchema
        - addRoute
        - signAck
key required default meaning
enabled no true when false, every request passes through untouched
actions yes (when enabled) comma list of gated beckn actions, e.g. confirm,init
allowedDidMethods no key,jwk,web permitted issuer / verification-method DID methods
checkExpiry no true enforce validFrom/validUntil and nbf/exp
checkRevocation no true check credentialStatus
requireProof no true reject credentials whose proof this plugin cannot verify
failOpen no false on transient network errors, true allows / false rejects
httpTimeout no 10 seconds; bounds did:web and revocation-list fetches
maxCredentials no 10 max embedded credentials per request; excess → Bad Request NACK
allowPrivateNetworks no false permit fetches to private/loopback/link-local addresses (local deployments only)
debugLogging no false verbose per-credential logging

actions has no hidden code default — it must be declared in the YAML so the gated messages are always visible from the config alone.

Testing

# from the repo root
go test ./pkg/plugin/implementation/vcvalidator/...

The suite runs fully offline. It includes:

  • TestVectors — the committed mock credentials under testdata/vectors/ covering did:key, did:jwk and did:web in both a not-revoked and a revoked state; the referenced DID document and StatusList2021 credential are served from an in-memory fetcher.
  • TestRealDIDKeyVC — a real, externally-issued did:key (P-256) VC-JWT (testdata/flockenergy_vc.json).
  • Negative tests for tampered signatures, expired / not-yet-valid windows, issuer mismatch, did:web unreachable (fail-closed and fail-open), DEDI revocation, and Data Integrity proof rejection.
  • Step-level tests (TestStepPassThrough, TestStepNackErrorTypes) — the pass-through cases (disabled, non-gated action, no credentials) and the mapping of rejections to the model error types the handler NACKs with.

See testdata/README.md for the fixtures and how to regenerate them.

Building

The plugin is listed in install/build-plugins.sh (entry vcvalidator:validateVC — source dir : output name) and is built like any other plugin:

go build -buildmode=plugin -o plugins/validateVC.so \
    ./pkg/plugin/implementation/vcvalidator/cmd/plugin.go

Documentation

Overview

Package vcvalidator provides a processing Step that validates W3C Verifiable Credentials embedded in a beckn request body. It is built as validateVC.so, so pipelines reference it by the step id validateVC — matching the verb naming of the built-in steps (validateSign, validateSchema).

For the configured beckn actions it verifies every embedded credential's proof, validity window and revocation status. On any failure the step returns an error, which the handler pipeline turns into the standard signed beckn NACK — the request never reaches routing.

The package is organised in two files:

  • vcvalidator.go — the plugin surface: the Step, its Config, and credential extraction from the request body.
  • verify.go — the verification engine: proof/JWT checks, DID resolution (did:key / did:jwk / did:web), and revocation.

verify.go is the verification engine behind the vcvalidator Step: proof and validity-window checks, DID resolution (did:key / did:jwk / did:web), and revocation (StatusList2021 / BitstringStatusList, DEDI, generic).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func New

func New(cfg map[string]string) (definition.Step, error)

New builds the validateVC Step from its YAML config map.

Types

type Config

type Config struct {
	// Enabled controls whether the plugin is active. When false the step
	// passes every request through untouched.
	Enabled bool

	// Actions is the list of beckn actions whose payloads are validated.
	// REQUIRED — no code default. Declared explicitly in the devkit YAML so
	// the operator can see exactly which messages are gated (e.g.
	// "confirm,init,select").
	Actions []string

	// AllowedDIDMethods restricts which issuer/verification-method DID
	// methods are accepted. Default: key,jwk,web.
	AllowedDIDMethods []string

	// CheckExpiry toggles validity-window enforcement (validFrom/validUntil
	// and JWT nbf/exp). Default: true.
	CheckExpiry bool

	// CheckRevocation toggles credentialStatus revocation checks.
	// Default: true.
	CheckRevocation bool

	// RequireProof rejects credentials whose proof cannot be cryptographically
	// verified by this plugin (e.g. JSON-LD Data Integrity proofs such as
	// Ed25519Signature2020 that require RDF canonicalization, which this
	// plugin does not perform). When false such proofs are skipped with a
	// warning and the remaining checks (expiry/revocation) still run.
	// Default: true.
	RequireProof bool

	// FailOpen controls behaviour on transient network errors while
	// resolving a did:web document or fetching a revocation list. When true
	// such errors are logged and the credential is allowed through; when
	// false the request is rejected. Default: false (fail closed).
	FailOpen bool

	// HTTPTimeout bounds did:web and revocation-list HTTP fetches.
	// Default: 10s.
	HTTPTimeout time.Duration

	// MaxCredentials caps how many embedded credentials a single request may
	// carry. Each credential can cost up to two HTTP fetches (did:web
	// resolution + revocation), so the cap bounds the per-request work; a
	// request exceeding it is rejected with a Bad Request NACK before any
	// network I/O. Default: 10.
	MaxCredentials int

	// AllowPrivateNetworks permits did:web and revocation fetches to resolve
	// to private, loopback or link-local addresses. The fetched URLs come from
	// the request body, so this MUST stay false in production (SSRF); it
	// exists for local/devkit deployments where issuers and registries live on
	// a private docker network. Default: false.
	AllowPrivateNetworks bool

	// DebugLogging enables verbose per-credential logging.
	DebugLogging bool
}

Config holds configuration for the VC Validator plugin.

The plugin inspects Verifiable Credentials carried in the request body (by default the credential objects nested under message.contract.participants[].participantAttributes) and, for the configured beckn actions, verifies that each credential:

  • has a cryptographically valid proof (did:key / did:jwk / did:web),
  • was signed by the did:web issuer when the issuer id is a did:web that is web accessible,
  • is within its validity window (validFrom / validUntil, nbf / exp), and
  • is not revoked (credentialStatus).

On any failure the request is rejected with a beckn NACK and never reaches routing.

func DefaultConfig

func DefaultConfig() *Config

DefaultConfig returns a Config seeded with sensible defaults for the non-primary fields. The primary behaviour knob (Actions) is intentionally left empty — ParseConfig requires it in the YAML.

func ParseConfig

func ParseConfig(cfg map[string]string) (*Config, error)

ParseConfig parses the plugin configuration map supplied by beckn-onix.

func (*Config) IsActionEnabled

func (c *Config) IsActionEnabled(action string) bool

IsActionEnabled reports whether the given beckn action is gated.

func (*Config) IsMethodAllowed

func (c *Config) IsMethodAllowed(method string) bool

IsMethodAllowed reports whether the given DID method (without the "did:" prefix, e.g. "key", "web") is permitted.

Directories

Path Synopsis
Package main provides the plugin entry point for the VC Validator processing step.
Package main provides the plugin entry point for the VC Validator processing step.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL