Skip to content

Adding device attestation certificate validation#3560

Open
Apollon77 wants to merge 63 commits intomainfrom
attestation-certificate-checks
Open

Adding device attestation certificate validation#3560
Apollon77 wants to merge 63 commits intomainfrom
attestation-certificate-checks

Conversation

@Apollon77
Copy link
Copy Markdown
Collaborator

@Apollon77 Apollon77 commented Apr 9, 2026

Summary

  • Device attestation validation during commissioning per Matter spec 6.2.3.1 — certificate chain verification, attestation signature/nonce, Certification Declaration validation, and certificate revocation via CRL
  • Findings model with error/warning/info levels and configurable onAttestationFailure policy callback
  • Local checks always run (nonce, signature, VendorID, CD fields) even without DCL; DCL-dependent checks (PAA trust, chain, revocation) require DclCertificateService
  • CRL revocation — production DCL fetch, signer chain validation against trusted PAAs, CRL signature & integrity verification, freshness tracking (nextUpdate + 7-day TTL)
  • Shell commandscert revocations, cert check-revoked, config strict-attestation
  • Explicit attestation policies for shell (strict mode config) and chip-testing (always accept)

🤖 Generated with Claude Code

Apollon77 and others added 30 commits February 17, 2026 13:41
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire up DeviceAttestationValidator.validate() in the #deviceAttestation()
commissioning step with a configurable failure policy. The policy supports
four modes: undefined (backward-compatible accept with warning), true
(always accept), false (always reject), or a custom callback. Also forward
the option from CommissioningClient.BaseCommissioningOptions through to
ControllerCommissioningFlowOptions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…dator

Add validation steps 1-4 in DeviceAttestationValidator.validate():
- Parse DAC and PAI certificates from DER (ASN.1)
- PAA trust store lookup via DclCertificateService
- Certificate chain signature verification (PAA->PAI->DAC)
- VendorID matching (DAC vs PAI, and PAA vs PAI if PAA has vendorId)

Also adds:
- Pai.fromAsn1() and Dac.fromAsn1() static methods for DER parsing
- DclCertificateService.getCertificateAsDer() for direct DER retrieval
- Comprehensive tests for valid chains, untrusted PAA, tampered certs,
  wrong signers, and vendorId mismatches

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add validation steps 6-7 to DeviceAttestationValidator.validate():
- Step 6: Decode attestation elements using TlvAttestation and verify the
  nonce matches the one sent to the device
- Step 7: Verify the attestation signature against the DAC public key,
  using the concatenation of attestation elements and attestation challenge
  as the signed data

Update tests to use valid attestation data (TLV-encoded elements, proper
nonce, and real ECDSA signatures) and add new test cases for nonce
mismatch, tampered signature, wrong challenge, and wrong signing key.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a static parse() method that extracts the CD content, signer subject
key identifier, ECDSA signature, and raw eContent bytes from a
DER-encoded CMS/PKCS#7 SignedData structure. This is the inverse of the
existing generate()/asSignedAsn1() flow and is needed by the upcoming CD
validation step during device attestation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add steps 8-9 to DeviceAttestationValidator.validate():
- Step 8: CD signature verification using optional cdSignerPublicKeys map
- Step 9: CD field validation against BasicInformation and DAC/PAI

The cdSignerPublicKeys context field maps SKID hex to public key. When
not provided, CD signature verification is skipped with a warning (DCL
fetching of CD signer certs is a future enhancement).

CD field validation checks per Matter spec Section 6.2.3.1:
- vendor_id matches BasicInformation VendorID
- product_id_array contains BasicInformation ProductID
- dac_origin fields consistency and cross-validation
- DAC/PAI vendorId and productId match against CD fields
- authorizedPaaList contains PAA's subject key identifier

Also exports testCdSignerInfo() from CertificationDeclaration for tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add DclClient methods for fetching revocation distribution points from
the DCL REST API, with proper field name mapping from the DCL response
format (issuerSubjectKeyID, dataURL) to the internal schema format
(issuerSubjectKeyId, dataUrl).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extend DclCertificateService to fetch CRL revocation data from DCL
distribution points, parse DER-encoded CRLs to extract revoked serial
numbers, cache the revocation index in storage, and expose an
isRevoked() lookup method.

Changes:
- Add revocation storage and in-memory index (Map<issuerKeyId, Set<serialHex>>)
- Fetch revocation distribution points during update() cycle
- Download and parse CRL files for each distribution point
- Persist revocation index to storage for restart resilience
- Add isRevoked(authorityKeyId, serialNumber) public API
- Add static parseCrlRevokedSerials() for CRL DER parsing
- Update existing tests to include revocation endpoint mock
- Add 8 new tests covering CRL parsing, isRevoked lookup,
  persistence, Bytes support, non-CRL type skipping, and error handling

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add Step 5 (revocation check) to DeviceAttestationValidator.validate()
between VendorID matching and nonce verification. When revocation data is
available, both DAC and PAI serial numbers are checked against the
revocation index via DclCertificateService.isRevoked(). When no revocation
data exists, a warning is logged and validation continues per spec
Section 6.2.4.2.

Also adds hasRevocationData getter to DclCertificateService so the
validator can detect when revocation data is unavailable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ning

Replaces the placeholder `undefined as any` for dclCertificateService in the
commissioning flow with the real service, obtained from the environment. Adds
CSR attestation signature verification using the DAC public key extracted
during device attestation (step 10).

Key changes:
- Add dclCertificateService as optional field on ControllerCommissioningFlowOptions
- ControllerCommissioner auto-injects DclCertificateService from environment
- Skip attestation validation with warning if service is not available
- Extract and store DAC public key after attestation for CSR verification
- Verify CSR signature against DAC public key in certificates step

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the stub in DeviceCertification.#validateCertification() with
actual validation that parses the DAC and PAI certificates and verifies
their vendorId/productId fields match the configured product description.
This catches configuration errors at device startup rather than during
commissioning.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Verify the onAttestationFailure option flows through the full commissioning
pipeline without errors, covering backward compatibility (no DclCertificateService),
explicit true/false policies, and custom callback acceptance.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- C1: Add certificate validity period checking (Step 3b) per Matter spec
  Section 6.2.3.1, validating PAI/PAA at DAC's notBefore timestamp
- C2: Throw CommissioningError when DclCertificateService is absent but
  strict attestation is requested (onAttestationFailure=false or callback)
- I1: Add TODO comment for CRL signature validation in processRevocationPoint
- I3: Return DAC public key from validate() to avoid redundant DAC parsing
- I4: Strengthen getCertificateAsDer/AsPem empty bytes check
- I5: Split revocation fetching to mirror certificate fetching pattern,
  respecting fetchTestCertificates config for test-net revocation data
- I6: Add firmware info warning when present but validation unsupported
- I7: Remove unused ProductIdMismatch enum value
- S2-S3: Extract buildTestCrl and pemEncode into shared TestHelpers.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts:
#	packages/node/test/node/ClientNodeTest.ts
#	packages/protocol/src/certificate/kinds/CertificationDeclaration.ts
#	packages/protocol/src/dcl/DclCertificateService.ts
#	packages/protocol/src/dcl/DclClient.ts
#	packages/protocol/test/dcl/DclCertificateServiceTest.ts
Import paths (#general → @matter/general, #types → @matter/types),
DclClient config API (boolean → DclConfig object), paseSession injection
via options, formatting, and unused import cleanup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# Conflicts:
#	packages/node/src/behavior/system/commissioning/CommissioningClient.ts
#	packages/node/test/node/ClientNodeTest.ts
#	packages/protocol/src/dcl/DclCertificateService.ts
#	packages/protocol/test/dcl/DclCertificateServiceTest.ts
- Refactor DeviceAttestationValidator to collect warnings/info as findings
  (errors still throw immediately). OnAttestationFailure callback now
  receives AttestationFinding[] instead of single failure+reason.
- Rename DeviceAttestationFailure → DeviceAttestationCheck (enum covers
  both errors and informational observations).
- Add CertificationType enum (Test/Provisional/Official) for CD type field.
- Add certification_type inspection: provisional → warning, test → info.
- Add certificate_id DCL validation TODO placeholder.
- Add issuer DN (DER hex) to revocation index for spec 6.2.4.2 composite
  key matching.
- Implement CRL signer chain validation (#validateCrlSigner): parse
  CRLSignerCertificate/Delegator, VID/PID matching, chain verification
  against PAA trust store, CRL signature verification.
- Add time-based PAA trust store check (fetchedAt vs DAC notBefore).
- Validate paseSession presence at start of attestation step.
- Replace magic epoch number with MATTER_EPOCH_OFFSET_S.
- Named CrlParseResult interface for parseCrl return type.
- 10 new tests covering findings model, certification_type, PAA time
  check, and CRL parsing.
- Address greg review findings (C1, C3, H2, H3, H5, M1, M2, M4, M5).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When DclCertificateService is unavailable, surface it as a
DclServiceUnavailable error finding through resolveFindings instead of
bypassing the callback. This lets custom onAttestationFailure callbacks
decide whether to proceed without DCL validation.

Also add docs/plans/ to .gitignore (plan docs are never committed).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Enhance ClientNodeTest attestation tests to capture and assert on the
actual findings passed to custom onAttestationFailure callbacks. Verify
the callback is invoked, receives exactly one DclServiceUnavailable error
finding, and that both accept/reject paths work correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move attestation tests from ClientNodeTest to dedicated
ClientAttestationTest.ts. Add new integration tests that set up
DclCertificateService with mocked DCL API responses and verify:
- Full commissioning with DCL trust store containing test PAA
- Callback receives correct findings (CertificationTypeTest, etc.)
- onAttestationFailure=false rejects even with valid chain
- PAA not in trust store produces PaaNotTrusted error finding
- Callback can accept despite PaaNotTrusted error

Also refactor CertificationType: import directly from definitions
file instead of re-exporting. Move testCdSignerInfo() into
CertificationDeclaration.testSignerInfo() namespace.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extract setupDclFetchMock and formatSkidWithColons into shared
  TestHelpers.ts, used by DeviceAttestationValidatorTest
- Replace inlined pemEncode with Pem.encode from @matter/general
- Use new Array<T>() instead of T[] = [] per project conventions
- Restore CertificationDeclaration.ts from committed version (was
  accidentally truncated by IDE edit)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…late

Each test now calls runAttestationTest() with just the varying parts:
dclPaaCert, onAttestationFailure, expectRejection, assertFindings,
assertResult. The helper handles MockSite lifecycle, entropy management,
DCL service setup/teardown, and findings capture.

Reduces the file from 470 to 275 lines while keeping all 10 test cases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add revocation test group to ClientAttestationTest:
- PAI serial in revocation list → CertificateRevoked error, rejected
- Different serials in revocation list → passes, no revocation finding

PAI revocation is testable because PAI AKID = well-known PAA SKID
and PAI serial is deterministic (01). DAC revocation requires the
random PAI SKID, so it's covered by unit tests in
DeviceAttestationValidatorTest.

Also restore DeviceAttestationValidator.js export in certificate/index.ts
(lost during merge from main).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove FirmwareInfoMismatch info finding that just said "not yet
validated" — better to leave a code TODO than emit noise findings
for optional MAY-level spec features. The enum values remain reserved
for when DCL DeviceSoftwareCompliance API is implemented.

Also verify no files were lost during recent merges (all present).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Revocation from test DCL is not meaningful — test certificates are for
development only and their revocation status has no bearing on real
device attestation decisions. Remove the test DCL revocation fetch
and simplify the method.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove the options.dclCertificateService fallback from
ControllerCommissioner — the service is always looked up from
the environment. Nobody passes it explicitly via options.

Keep paseSession in ControllerCommissioningFlowOptions as an internal
(not user-facing) option since ClientInteraction doesn't expose the
underlying session, so the commissioner must inject it explicitly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Apollon77 Apollon77 changed the title feat: device attestation certificate validation Adding device attestation certificate validation Apr 9, 2026
Apollon77 and others added 3 commits April 9, 2026 19:06
#validateCrlSigner runs during DclCertificateService construction
(inside #fetchCertificates → #fetchRevocationData). It called the
public getCertificateAsDer() which asserts construction is complete,
causing "DclCertificateService is still initializing" errors.

Extract #getCertificateDer() without the construction assert for
internal use during init. Public getCertificateAsDer() delegates
to it after the assert.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Test CRLs use empty signatures to avoid fake signature verification
- Add signature length check before attempting CRL signature verify
- Use raw TBS DER for signer chain verification (not re-encoded)
- Extract getCertificateDer for internal use during init
- Downgrade CRL signer validation to info level per spec

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace bulk "fetch all CRLs at startup" with on-demand per-AKID lookup.
Revocation data is fetched from DCL during commissioning and cached for 1h.
Also retains raw issuer DER in parsed certificates for composite key matching,
and supports all IANA hash algorithms for CRL digest verification.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 26 out of 27 changed files in this pull request and generated 5 comments.

Comment thread packages/protocol/test/certificate/CertificatesTest.ts Outdated
Comment thread packages/protocol/test/certificate/CertificatesTest.ts Outdated
Comment thread packages/protocol/test/certificate/CertificatesTest.ts Outdated
Comment thread packages/protocol/src/peer/ControllerCommissioningFlow.ts
Comment thread packages/protocol/src/dcl/DclCertificateService.ts
Apollon77 and others added 20 commits April 15, 2026 10:49
Matter spec 6.2.3.1 requires SHALL-verify of the Certification Declaration
signature. Replace the user-provided cdSignerPublicKeys map with DCL-backed
lookup in DclCertificateService.

- Add kind: "PAA" | "CDSigner" to CertificateMetadata
- Add addCertificate(der, kind, {isProduction?}) returning true/false
- Add getOrFetchCdSigner: local trust store first, DCL by-SKID fallback
  (GET /dcl/pki/all-certificates?subjectKeyId=<skid>)
- Extend GitHub bootstrap to fetch credentials/development/cd-certs
- Bundle well-known Matter test CD signer cert (Appendix F)
- Remove Context.cdSignerPublicKeys from DeviceAttestationValidator

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ControllerCommissioner probes env.has(DclCertificateService) synchronously
when assembling attestation context. Without prior access, the service is
never registered and DeviceAttestationValidator receives undefined,
silently skipping PAA trust store, chain verification, and CRL revocation
checks (only emitting a DclServiceUnavailable warning that strict mode
currently accepts).

Awaiting theNode.certificateService() before commissionNode() registers
the service in the environment and awaits its construction so DCL-backed
attestation runs as intended. CRL fetching remains lazy per-AKID, so the
warmup is cheap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…licy

Replace the polymorphic onAttestationFailure value (function in strict mode,
literal true in lax mode) with a single callback that always logs each
finding at info level — including type, level, and message — before
returning the accept/reject decision. Previously, strict mode passed a
function that returned a boolean without any logging, so accepted findings
disappeared silently. Lax mode passed `true` and relied on the framework's
shorter built-in log line. Both paths now emit consistent, detailed info
about what was accepted or rejected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`Certificate.asUnsignedDer()` re-encodes the TBS portion from parsed
fields, normalizing string-typed Distinguished Name attributes to
UTF8String. Real-world Matter PAI/DAC certificates from several major
vendors encode VendorID and ProductID DN attributes
(`1.3.6.1.4.1.37244.2.1` / `.2.2`) as PrintableString — the spec accepts
both — so the byte mismatch invalidates the issuer signature and breaks
PAI/DAC chain verification during attestation as well as CRL signer
chain validation during revocation checks.

Capture the raw TBS DER bytes inside `parseAsn1Certificate` and store
them on `MatterCertificate.tbsDer`. `asUnsignedDer()` returns the
preserved bytes when present, falling back to the existing re-encode
path for certificates built from TLV or constructed in code.
`DerCodec.encode(DerCodec.decode(x))` is byte-identical for canonically
encoded DER input because the encoder re-emits the captured `_tag` and
raw `_bytes` verbatim and uses canonical short/long-form length
encoding, which all spec-compliant certificates already use.

Removes the manual `DerCodec.decode/encode` workaround in
`DclCertificateService.#validateCrlSigner` since `asUnsignedDer()` now
returns the correct bytes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… noise

The DCL REST API returns gRPC `code: 5` (NOT_FOUND) when an issuer has no
revocation distribution point registered. The previous check only matched
HTTP 404, so every commissioning of a device whose issuer has no CRL on
DCL produced a warning and an info-level stack trace via two separate
log paths instead of a quiet success.

Match `code === 5` and emit a single debug line ("No CRL registered in
DCL for AKID …"). Drop the duplicate `isRevoked` outer log because the
inner fetcher already records the cause when a real failure occurs.

Replace `logger.<level>(prefix, error)` calls with
`Diagnostic.errorMessage(asError(error))` across DclCertificateService
so DCL/GitHub fetch errors render as one short line instead of dumping
a multi-line stack trace.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a single info-level confirmation when device attestation passes,
distinguishing a clean pass from a pass with accepted findings. Hard
errors that are overridden by policy intentionally do not log as
"successfully verified" because they are a policy bypass, not a
verification result.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants