Adding device attestation certificate validation#3560
Open
Adding device attestation certificate validation#3560
Conversation
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>
#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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
onAttestationFailurepolicy callbackDclCertificateServicecert revocations,cert check-revoked,config strict-attestation🤖 Generated with Claude Code