Skip to content

fix(dva): drop redundant attestation fields and decode issuer did:key from JWS - #90

Draft
MYRhouma wants to merge 11 commits into
Prometheus-X-association:mainfrom
MYRhouma:fix/dva-response-cleanup
Draft

MYRhouma wants to merge 11 commits into
Prometheus-X-association:mainfrom
MYRhouma:fix/dva-response-cleanup

Conversation

@MYRhouma

Copy link
Copy Markdown

Cleans up the dataspace-connector attestation branch to match the slimmed dva-api contract:

  • forwards the JWS issued by the dva-api back through provider/consumer flows (replaces previously sent vcId, vcIssuedDate, attesterDidKey and payload fields, which are all derivable from the JWS)
  • adds decodeJwsIssuer(jws) to extract the issuer did:key from the JWS payload so downstream attesterDid can be derived without a separate header
  • removes attesterDid from the verifyAttestation call site in consumer.public.service.ts (the field is no longer part of VerifyAttestationRequest)
  • drops now-unused aovAttesterDid header read in the consumer import branch
  • fixes the decodeJwsIssuer unit test fixture (z6Mkk -> z6Mk to match the base64url-decoded payload)
  • pnpm run build clean; npx ts-mocha src/tests/api/dvaClient.spec.ts -> 5 passing

Lockstep with Prometheus-X-association/data-veracity#76 (synchronous attestation endpoints).

Depends on: none (PDC-side)

@bzp99

MYRhouma and others added 11 commits June 27, 2026 14:00
The contract manager now has an optional vlaId field that points to a
VLA (Verifiable Legal Agreement) in the DVA. To let the PDC read this
field when it fetches a contract, add vlaId?: string to both response
types.

contract.response.ts:
- Add vlaId?: string to ContractResponseType (after useDVCT).

bilateral.response.ts:
- Add vlaId?: string to BilateralResponseType.

contractVlaId.spec.ts (4 tests):
- should parse an ecosystem contract WITH vlaId (field present)
- should parse an ecosystem contract WITHOUT vlaId (optional)
- should parse a bilateral contract WITH vlaId
- should parse a bilateral contract WITHOUT vlaId (optional)

The field is optional on both sides. Existing contracts without a VLA
parse correctly — vlaId is just undefined.
Wire the DVA (Data Veracity Assurance) service URL into the PDC
configuration system, following exactly the existing consentUri /
dvctUri pattern so operators configure it the same way.

config.sample.json:
- Add "dvaUri": "" and "dvaApiKey": "" next to the existing
  consentUri / dvctUri block.

configuration.ts (types):
- Add dvaUri?: string and dvaApiKey?: string to the IConfiguration
  TypeScript interface.
- Add dvaUri: String and dvaApiKey: String to the Mongoose schema so
  the values can be overridden via DB.

configuration.ts (loaders):
- Add getDvaUri() and getDvaApiKey() accessors. Both follow the existing
  getConsentUri() pattern: read from DB first, fall back to the JSON
  config file, return undefined if neither is set.
- Wire both into setUpConfig() and reloadConfigurationFromFile().
- Add to the module export list.

configuration.private.router.ts:
- Add body('dvaUri').optional().isString().custom(urlValidation) and
  body('dvaApiKey').optional().isString() validators on
  PUT /private/configuration/, alongside the existing consentUri
  validator.
- Add matching @Swagger requestBody schema entries.
- No controller or service change needed: the controller forwards
  req.body verbatim, and the service's findOneAndUpdate({}, {...data})
  persists any new field on the IConfiguration interface.
dataExchangeStatusEnum.ts:
- Add VERACITY_ERROR = 'VERACITY_ERROR' to the DataExchangeStatusEnum
  (single-line addition next to NODE_CALLBACK_ERROR / PENDING). Used by
  the provider and consumer hooks when the DVA reports failed veracity
  or the JWS verification fails.

libs/third-party/dva.ts (new):
- axios-based DVA client matching the existing libs/third-party/
  contract.ts / consumer.ts / provider.ts style.
- requestAttestation({dvaUri, vlaId, exchangeId, contract, data,
  attesterDid, apiKey?}): POSTs to
  ${dvaUri}/attestation with {exchangeID, contract, vlaId, data,
  attesterID}. Sends optional Authorization: Bearer <apiKey> header.
  Returns AttestationResponse {requestId, issuerDidKey, jws, vcId,
  evaluationPassing, evaluationResults, vcIssuedDate}.
- verifyAttestation({dvaUri, jws, attesterDid, apiKey?}): POSTs to
  ${dvaUri}/attestation/verify with {jws, attesterDidKey}. Returns
  {verified, reason?, payload?}.
- Each function shares a buildHeaders(apiKey?) helper.

dvaClient.spec.ts (3 tests):
- requestAttestation POSTs to the DVA /attestation endpoint and returns
  the JWS on success (uses axios-mock-adapter, no Express app import so
  it dodges the Node 25 buffer-equal-constant-time crash).
- requestAttestation returns null JWS when evaluation fails.
- verifyAttestation POSTs to the DVA /attestation/verify endpoint and
  returns {verified: true}.
Wire the PDC's data provider to call the DVA before pushing data to a
consumer. When a contract has a vlaId AND dvaUri is configured, the
PDC asks the DVA to attest the data; if the DVA says the data failed
quality checks, the push is skipped and the exchange is marked
VERACITY_ERROR.

provider.public.service.ts (ProviderExportService):
- After fetching the data and the contract, before pushing to consumer:
  read vlaId from contractResp and dvaUri from getDvaUri().
- If both present, call requestAttestation() with {dvaUri, vlaId,
  exchangeId, contract: contractResp (the full object, not the URL
  string), data, attesterDid: PDC endpoint, apiKey: getDvaApiKey()}.
- On evaluationPassing === false: updateStatus(VERACITY_ERROR, {reason,
  aov}) and continue (skip this resource's push).
- On thrown error: updateStatus(VERACITY_ERROR, {reason: 'DVA request
  failed: ' + e.message}) and continue.
- On success: capture aov.jws and aov.issuerDidKey into aovJws /
  aovAttesterDid locals and pass them to triggerGenericFlow().
- Hook placed in both the service-chain branch and the generic branch,
  inside the per-resource loop so multi-resource exchanges check each
  resource independently.

triggerGenericFlow():
- Add aovJws? and aovAttesterDid? to the props interface.
- Forward them to consumerImport() (new last two params).

Critical design: when vlaId is absent OR dvaUri is empty, the hook is a
no-op. The existing flow runs exactly as before, so deployments that
don't use the DVA are unaffected.
Thread the JWS attestation from the provider through to the consumer so
the consumer's PDC can verify it on receipt. This closes the end-to-end
veracity loop: provider attests → consumer verifies.

libs/third-party/consumer.ts:
- Add two optional params to consumerImport(): aovJws? and
  aovAttesterDid?. When present, they're added as x-ptx-aov-jws and
  x-ptx-aov-attester-did HTTP headers on the axios POST to the
  consumer's /consumer/import endpoint (both the JSON and non-JSON
  branches). When absent, headers are not set — zero impact on existing
  callers.

controllers/public/v1/consumer.public.controller.ts:
- Pass headers: req.headers into consumerImportService({...}) so the
  service can read x-ptx-aov-jws / x-ptx-aov-attester-did.

services/public/v1/consumer.public.service.ts (consumerImportService):
- Accept a new optional headers param.
- If x-ptx-aov-jws is present (case-insensitive):
  - If getDvaUri() is empty: Logger.warn('DVA not configured for
    verification; skipping AoV check') and continue (best-effort).
  - Otherwise call verifyAttestation({dvaUri, jws, attesterDid,
    apiKey: getDvaApiKey()}).
  - If verified === false: updateStatus(VERACITY_ERROR, {reason}) and
    return (block the exchange).
  - If the request throws: updateStatus(VERACITY_ERROR, {reason:
    'DVA verification request failed: ' + e.message}) and return.
  - If verified === true: continue normally.
- If no JWS present: continue normally (the provider didn't attest,
  which is fine for contracts without a VLA).

Design: consumer verification is best-effort — if the consumer hasn't
configured a DVA, it logs a warning and continues instead of blocking.
This is because not every participant will run a DVA, and blocking
data flows would be too aggressive for an opt-in feature.
If the dvaUri configuration value ends with a slash (e.g. 'http://dva/'),
the generated URL becomes 'http://dva//attestation' (double slash). Many
HTTP servers and proxies treat this as a different path and return 404,
causing silent attestation failures with no obvious cause in the logs.

Applied a .replace(/\/+$/, '') to dvaUri in both requestAttestation() and
verifyAttestation() before string-concatenating the path segments.
…romFile()

dvaApiKey was omitted from two configuration functions:

1. setUpConfig(): run once on first startup to persist configuration to the
   database. Because dvaApiKey was not included, getDvaApiKey() would always
   fall back to reading from config.json at runtime instead of from the DB.
   This breaks in container deployments where config.json is not mounted after
   startup, causing all DVA attestation requests to be sent without auth.

2. reloadConfigurationFromFile(): triggered by PUT /config/reload. Because
   dvaApiKey was not in the update payload, a config reload would effectively
   clear any previously stored key from the database, causing unauthenticated
   DVA calls silently from that point on.

Both functions now include dvaApiKey from the config file / DB getter.
…ranch

In ProviderExportService the service-chain branch called requestAttestation()
to obtain a signed AoV token, but immediately discarded aov.jws and
aov.issuerDidKey. The JWS was computed, signed with the DVA server's Ed25519
key, and then silently dropped — the consumer node in a service-chain exchange
had no way to verify data integrity.

Changes:
- Capture aovJwsChain / aovAttesterDidChain when attestation succeeds.
- Persist them on the exchange record via updateStatus(VERACITY_ATTESTED, {…})
  so downstream infrastructure nodes and the consumer can retrieve them.
- Add VERACITY_ATTESTED to DataExchangeStatusEnum (previously only VERACITY_ERROR
  existed, so a successful attestation had no typed status value).

The triggerInfrastructureFlowService() signature is unchanged — adding AoV
to signedConsent/encrypted would have been semantically wrong.
Copilot AI review requested due to automatic review settings July 27, 2026 12:29
@MYRhouma
MYRhouma marked this pull request as draft July 27, 2026 12:31

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants