/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import {
  asError,
  AsyncCache,
  Bytes,
  Construction,
  Crypto,
  Days,
  DerCodec,
  DerTag,
  DerType,
  Diagnostic,
  EcdsaSignature,
  Github,
  hashAlgorithmForId,
  Hours,
  Logger,
  LogLevel,
  Pem,
  PublicKey,
  Seconds,
  StorageService,
  Time
} from "@matter/general";
import { RevocationTypeEnum } from "@matter/types";
import { Paa, Pai } from "../certificate/kinds/AttestationCertificates.js";
import { Certificate } from "../certificate/kinds/Certificate.js";
import { DclClient, MatterDclError, MatterDclResponseError } from "./DclClient.js";
import { DclConfig, DclGithubConfig } from "./DclConfig.js";
const logger = Logger.get("DclCertificateService");
class DclCertificateService {
  #construction;
  #crypto;
  #storageManager;
  #storage;
  #certificateIndex = /* @__PURE__ */ new Map();
  #updateTimer;
  #githubRateLimited = false;
  #closed = false;
  #options;
  #fetchPromise;
  /** Lazy CRL revocation cache: keyed by normalized AKID, fetches on-demand from DCL */
  #revocationCache;
  constructor(environment, options = {}) {
    environment.root.set(DclCertificateService, this);
    this.#crypto = environment.get(Crypto);
    this.#options = options;
    logger.info(
      "Initialize CertificateService",
      Diagnostic.dict({
        prod: options.dclConfig?.url ?? DclConfig.production.url,
        test: options.fetchTestCertificates ? options.testDclConfig?.url ?? DclConfig.test.url : void 0,
        github: options.fetchTestCertificates && options.fetchGithubCertificates ? "yes" : void 0,
        flags: Diagnostic.asFlags({ acceptTest: this.acceptsTestCertificates })
      })
    );
    this.#revocationCache = new AsyncCache(
      "CRL revocation",
      (akid) => this.#fetchAndValidateRevocation(akid),
      Hours(1)
    );
    this.#construction = Construction(this, async () => {
      this.#storageManager = await environment.get(StorageService).open("certificates");
      this.#storage = this.#storageManager.createContext("root");
      await this.#loadIndex(this.#storage);
      if (options.seed?.paaRoots) {
        await this.#consumeCertSeed(options.seed.paaRoots, "PAA");
      }
      if (options.seed?.cdSigners) {
        await this.#consumeCertSeed(options.seed.cdSigners, "CDSigner");
      }
      await this.update();
      if (options.updateInterval !== null) {
        const updateInterval = options.updateInterval ?? Days.one;
        this.#updateTimer = Time.getPeriodicTimer(
          "DCL Certificate Update",
          updateInterval,
          () => this.update()
        ).start();
      }
    });
  }
  get construction() {
    return this.#construction;
  }
  /**
   * Normalize the subject key identifier to a consistent string format.
   * Accepts either Bytes or string (with or without colons).
   */
  #normalizeSubjectKeyId(subjectKeyId) {
    if (typeof subjectKeyId === "string") {
      return subjectKeyId.replace(/:/g, "").toUpperCase();
    }
    return Bytes.toHex(subjectKeyId).toUpperCase();
  }
  /**
   * Whether a stored entry is relevant under the current trust policy. Production certificates
   * are always relevant. Test PAAs are only relevant when the test-certificate policy allows
   * them. CD signer certificates are managed separately and are always relevant.
   */
  #isRelevant(metadata, options) {
    const kind = metadata.kind ?? "PAA";
    const considerTestCertificates = options?.considerTestCertificates ?? this.acceptsTestCertificates;
    return kind !== "PAA" || metadata.isProduction || considerTestCertificates;
  }
  /**
   * Get certificate metadata by subject key identifier. Returns undefined if not found or if the
   * entry is filtered by the effective trust policy.
   */
  getCertificate(subjectKeyId, options) {
    this.construction.assert();
    const metadata = this.#certificateIndex.get(this.#normalizeSubjectKeyId(subjectKeyId));
    if (metadata === void 0 || !this.#isRelevant(metadata, options)) {
      return void 0;
    }
    return metadata;
  }
  /**
   * Get all certificate metadata entries (raw enumeration, unaffected by the
   * `fetchTestCertificates` filter so management commands can still inspect cached test certs).
   */
  get certificates() {
    this.construction.assert();
    return Array.from(this.#certificateIndex.values());
  }
  /**
   * Whether the service trusts test (non-production) certificates as valid trust anchors.
   *
   * Independent of {@link Options.fetchTestCertificates} (which only governs whether test certs are
   * downloaded). Defaults to {@link Options.acceptTestCertificates}, falling back to
   * `fetchTestCertificates` when the trust policy is not set explicitly.
   */
  get acceptsTestCertificates() {
    return this.#options.acceptTestCertificates ?? this.#options.fetchTestCertificates ?? false;
  }
  /**
   * Get certificate as PEM string.
   * @throws {MatterDclError} if certificate not found or filtered by trust policy
   */
  async getCertificateAsPem(subjectKeyId, options) {
    this.construction.assert();
    return Pem.encode(await this.#getCertificateDer(subjectKeyId, options));
  }
  /**
   * Get certificate as DER bytes.
   *
   * Self-heals a corrupted local cache: if the stored certificate no longer parses (e.g. a
   * truncated or mangled storage entry), make one best-effort re-fetch from DCL so a broken cache
   * entry cannot permanently break attestation for an otherwise-valid trust anchor. If recovery
   * fails the original bytes are returned unchanged, leaving the caller's normal validation to
   * surface the parse failure rather than masking it with a different error here.
   *
   * @throws {MatterDclError} if certificate not found or filtered by trust policy
   */
  async getCertificateAsDer(subjectKeyId, options) {
    this.construction.assert();
    const der = await this.#getCertificateDer(subjectKeyId, options);
    const normalizedId = this.#normalizeSubjectKeyId(subjectKeyId);
    const metadata = this.#certificateIndex.get(normalizedId);
    const parseError = this.#certificateParseError(der, metadata?.kind);
    if (parseError === void 0) {
      return der;
    }
    logger.notice(
      `Cached certificate failed to parse; attempting one re-fetch from DCL to recover`,
      Diagnostic.dict({ skid: normalizedId }),
      Diagnostic.errorMessage(parseError)
    );
    const refetched = await this.#tryRefetchRootCertificate(normalizedId, metadata?.isProduction ?? true, options);
    if (refetched !== void 0) {
      const refetchParseError = this.#certificateParseError(refetched, metadata?.kind);
      if (refetchParseError === void 0) {
        return refetched;
      }
      logger.warn(
        `Re-fetched certificate also failed to parse`,
        Diagnostic.dict({ skid: normalizedId }),
        Diagnostic.errorMessage(refetchParseError)
      );
    }
    return der;
  }
  /** Parse error of a DER certificate, or undefined if it parses, using the extension set for its kind. */
  #certificateParseError(der, kind) {
    try {
      Certificate.parseAsn1Certificate(
        der,
        (kind ?? "PAA") === "PAA" ? Certificate.REQUIRED_PAA_EXTENSIONS : Certificate.REQUIRED_EXTENSIONS
      );
      return void 0;
    } catch (error) {
      return asError(error);
    }
  }
  /**
   * Best-effort force re-fetch of a single root certificate from DCL, overwriting the cached copy
   * to recover from a corrupted local storage entry. Never throws: returns the freshly stored DER,
   * or undefined if the certificate could not be re-fetched (not a root cert, absent from DCL, or
   * any fetch error).
   */
  async #tryRefetchRootCertificate(normalizedId, isProduction, options) {
    if (this.#fetchPromise !== void 0) {
      await this.#fetchPromise;
    }
    try {
      if (!await this.#fetchAndStoreRootCertificateBySkid(normalizedId, isProduction, true, this.#options)) {
        return void 0;
      }
    } catch (error) {
      logger.warn(
        `Re-fetch of certificate ${normalizedId} from DCL failed`,
        Diagnostic.errorMessage(asError(error))
      );
      return void 0;
    }
    const metadata = this.#certificateIndex.get(normalizedId);
    if (metadata === void 0 || !this.#isRelevant(metadata, options)) {
      return void 0;
    }
    const der = await this.#storage.get(normalizedId);
    return der && der.byteLength > 0 ? Bytes.of(der) : void 0;
  }
  /**
   * Fetch a single root certificate from DCL by SubjectKeyIdentifier and store it. Returns true if
   * a matching root certificate was found and stored, false if DCL has no such root certificate.
   * Throws on DCL/network errors; callers decide how to handle them.
   */
  async #fetchAndStoreRootCertificateBySkid(normalizedId, isProduction, force, options) {
    const config = isProduction ? this.#options.dclConfig ?? DclConfig.production : this.#options.testDclConfig ?? DclConfig.test;
    const dclClient = new DclClient(config);
    const certRefs = await dclClient.fetchRootCertificateList(options);
    const skidWithColons = normalizedId.match(/.{1,2}/g)?.join(":").toUpperCase();
    const certRef = certRefs.find((ref) => ref.subjectKeyId === skidWithColons);
    if (!certRef) {
      return false;
    }
    await this.#fetchAndStoreCertificate(
      this.#storage,
      dclClient,
      certRef,
      isProduction,
      force,
      options ?? this.#options
    );
    await this.#saveIndex();
    return true;
  }
  /** Internal DER retrieval without construction assert (safe during init). */
  async #getCertificateDer(subjectKeyId, options) {
    const normalizedId = this.#normalizeSubjectKeyId(subjectKeyId);
    const metadata = this.#certificateIndex.get(normalizedId);
    if (!metadata || !this.#isRelevant(metadata, options)) {
      throw new MatterDclError(`Certificate not found`, Diagnostic.dict({ skid: normalizedId }));
    }
    const derBytes = await this.#storage.get(normalizedId);
    if (!derBytes || derBytes.byteLength === 0) {
      throw new MatterDclError(`Certificate data not found in storage`, Diagnostic.dict({ skid: normalizedId }));
    }
    return Bytes.of(derBytes);
  }
  /**
   * Check if a certificate is revoked by looking up its serial number in the revocation set
   * for the given authority key identifier.
   *
   * Lazily fetches revocation data from DCL on first access per AKID, then caches for 1 hour.
   * Per spec Section 6.2.4.2, the revocation set is indexed by (AuthorityKeyIdentifier, IssuerDN).
   *
   * Returns true if the certificate is revoked, false otherwise.
   * If revocation data is not available for the given authority, returns false.
   */
  async isRevoked(authorityKeyIdentifier, serialNumber, issuerDnDerHex) {
    this.construction.assert();
    const akid = this.#normalizeSubjectKeyId(authorityKeyIdentifier);
    let entry;
    try {
      entry = await this.#revocationCache.get(akid);
    } catch {
      return false;
    }
    if (entry.serials.size === 0) {
      return false;
    }
    if (issuerDnDerHex !== void 0 && entry.issuerDnDerHex !== void 0 && entry.issuerDnDerHex !== issuerDnDerHex) {
      return false;
    }
    const serialHex = typeof serialNumber === "string" ? serialNumber.replace(/:/g, "").toUpperCase() : Bytes.toHex(serialNumber).toUpperCase();
    return entry.serials.has(serialHex);
  }
  /**
   * Get certificate metadata by subject key identifier, fetching from DCL if not in local storage.
   * Returns undefined if not found or if the entry is filtered by the effective trust policy.
   */
  async getOrFetchCertificate(subjectKeyId, options) {
    this.construction.assert();
    const normalizedId = this.#normalizeSubjectKeyId(subjectKeyId);
    const existing = this.#certificateIndex.get(normalizedId);
    if (existing && this.#isRelevant(existing, options)) {
      return existing;
    }
    if (this.#fetchPromise !== void 0) {
      await this.#fetchPromise;
      const afterFetch = this.#certificateIndex.get(normalizedId);
      return afterFetch && this.#isRelevant(afterFetch, options) ? afterFetch : void 0;
    }
    try {
      const isProduction = options?.isProduction ?? true;
      if (!await this.#fetchAndStoreRootCertificateBySkid(normalizedId, isProduction, false, options)) {
        logger.debug(
          `Certificate not found in DCL`,
          Diagnostic.dict({ skid: normalizedId, prod: isProduction })
        );
        return;
      }
      const fetched = this.#certificateIndex.get(normalizedId);
      if (fetched) {
        logger.info(
          `Fetched and stored certificate from DCL`,
          Diagnostic.dict({ skid: normalizedId, prod: isProduction })
        );
      }
      return fetched && this.#isRelevant(fetched, options) ? fetched : void 0;
    } catch (error) {
      MatterDclError.accept(error);
      logger.debug(`Failed to fetch certificate ${normalizedId} from DCL: ${error.message}`);
    }
  }
  /**
   * Delete a certificate from storage and index.
   */
  async deleteCertificate(subjectKeyId) {
    this.construction.assert();
    const normalizedId = this.#normalizeSubjectKeyId(subjectKeyId);
    await this.#storage.delete(normalizedId);
    this.#certificateIndex.delete(normalizedId);
    await this.#saveIndex();
    logger.debug(`Deleted certificate`, Diagnostic.dict({ skid: normalizedId }));
  }
  /**
   * Inject a certificate (PAA or CD signer) into the trust store.
   *
   * Returns `true` if the certificate was newly added, `false` if a certificate with the same
   * SubjectKeyIdentifier already exists (in which case the store is unchanged).
   *
   * @param derBytes DER-encoded X.509 certificate
   * @param kind "PAA" (self-signed root) or "CDSigner" (CMS Certification Declaration signer)
   * @param options.isProduction  mark as production-trust; defaults to `false`
   */
  async addCertificate(derBytes, kind, options) {
    this.construction.assert();
    const cert = Certificate.parseAsn1Certificate(
      derBytes,
      kind === "PAA" ? Certificate.REQUIRED_PAA_EXTENSIONS : Certificate.REQUIRED_EXTENSIONS
    );
    const skid = this.#normalizeSubjectKeyId(cert.extensions.subjectKeyIdentifier);
    if (this.#certificateIndex.has(skid)) {
      return false;
    }
    const isProduction = options?.isProduction ?? false;
    const vid = cert.subject.vendorId ?? 0;
    await this.#storeCertificate(this.#storage, skid, Bytes.of(derBytes), {
      subject: cert.subject.commonName,
      subjectKeyId: skid,
      serialNumber: Bytes.toHex(cert.serialNumber),
      vid,
      isRoot: kind === "PAA",
      isProduction,
      kind
    });
    await this.#saveIndex();
    logger.info(`Added ${kind} certificate`, Diagnostic.dict({ skid, isProduction }));
    return true;
  }
  /**
   * Look up a CD signer certificate by its SubjectKeyIdentifier.
   *
   * Checks the local trust store first. On miss, queries the production DCL
   * (`/dcl/pki/all-certificates?subjectKeyId=<skid>`) and caches the result as a
   * CDSigner entry with `isProduction: true`.
   *
   * Returns `undefined` if no matching CD signer can be found either locally or on DCL.
   */
  async getOrFetchCdSigner(subjectKeyId) {
    this.construction.assert();
    const normalizedSkid = this.#normalizeSubjectKeyId(subjectKeyId);
    const existing = this.#certificateIndex.get(normalizedSkid);
    if (existing !== void 0 && (existing.kind ?? "PAA") === "CDSigner") {
      const derBytes = await this.#getCertificateDer(normalizedSkid);
      const cert = Certificate.parseAsn1Certificate(derBytes, Certificate.REQUIRED_EXTENSIONS);
      return { publicKey: cert.ellipticCurvePublicKey, isProduction: existing.isProduction };
    }
    try {
      const config = this.#options.dclConfig ?? DclConfig.production;
      const dclClient = new DclClient(config);
      const certs = await dclClient.fetchCertificatesBySubjectKeyId(normalizedSkid, this.#options);
      if (certs.length === 0) {
        return void 0;
      }
      const derBytes = Pem.asDer(certs[0].pemCert);
      await this.addCertificate(derBytes, "CDSigner", { isProduction: true });
      const cert = Certificate.parseAsn1Certificate(derBytes, Certificate.REQUIRED_EXTENSIONS);
      return { publicKey: cert.ellipticCurvePublicKey, isProduction: true };
    } catch (error) {
      logger.info(
        `Failed to fetch CD signer ${normalizedSkid} from DCL:`,
        Diagnostic.errorMessage(asError(error))
      );
      return void 0;
    }
  }
  /**
   * Close the service and stop all timers.
   */
  async close() {
    this.#closed = true;
    this.#updateTimer?.stop();
    await this.#revocationCache.close();
    await this.#construction.close(async () => {
      await this.#fetchPromise;
      await this.#storageManager?.close();
    });
  }
  /**
   * Load the certificate index from storage, verifying each entry exists and cleaning up orphaned data.
   */
  async #loadIndex(storage) {
    const allKeys = new Set(await storage.keys());
    allKeys.delete("index");
    const storedIndex = await storage.get("index", []);
    let validCount = 0;
    let invalidCount = 0;
    for (const metadata of storedIndex) {
      if (allKeys.delete(metadata.subjectKeyId)) {
        this.#certificateIndex.set(metadata.subjectKeyId, metadata);
        validCount++;
      } else {
        logger.warn(
          `Certificate referenced in index but not found in storage`,
          Diagnostic.dict({ skid: metadata.subjectKeyId })
        );
        invalidCount++;
      }
    }
    if (allKeys.size > 0) {
      logger.debug(`Found ${allKeys.size} orphaned certificate(s) in storage, cleaning up`);
      for (const orphanedKey of allKeys) {
        await storage.delete(orphanedKey);
        logger.debug(`Deleted orphaned certificate: ${orphanedKey}`);
      }
    }
    logger.info(
      `Loaded ${validCount} certificates from storage${invalidCount > 0 ? ` (${invalidCount} missing)` : ""}${allKeys.size > 0 ? ` (${allKeys.size} orphaned removed)` : ""}`
    );
  }
  /**
   * Update certificates from DCL and GitHub. Returns true if update succeeded, false if it failed.
   */
  async update(force = false) {
    if (this.#closed || !this.#storage) {
      return;
    }
    if (this.#fetchPromise !== void 0) {
      return this.#fetchPromise;
    }
    logger.debug(`Update certificates${force ? " (force mode)" : ""}`);
    try {
      this.#fetchPromise = this.#fetchCertificates(this.#storage, force).finally(() => {
        this.#fetchPromise = void 0;
      });
      await this.#fetchPromise;
    } catch (error) {
      logger.info("Certificate update failed", Diagnostic.errorMessage(asError(error)));
      return false;
    }
    return true;
  }
  /**
   * Fetch certificates from DCL and GitHub.
   */
  async #fetchCertificates(storage, force = false) {
    if (this.#closed) {
      return;
    }
    const initialSize = this.#certificateIndex.size;
    this.#githubRateLimited = false;
    await this.#fetchDclCertificates(storage, true, force);
    if (this.#closed) {
      return;
    }
    if (this.#options.fetchTestCertificates) {
      await this.#fetchDclCertificates(storage, false, force);
      if (this.#closed) {
        return;
      }
      if (this.#options.fetchGithubCertificates !== false) {
        await this.#fetchGitHubCertificates(storage, force);
        if (this.#closed) {
          return;
        }
        await this.#fetchGitHubCdSignerCertificates(storage, force);
      }
    }
    if (this.#closed) {
      return;
    }
    await this.#saveIndex();
    const newCerts = this.#certificateIndex.size - initialSize;
    if (newCerts > 0) {
      logger.info(`Downloaded and stored ${newCerts} new certificates (total: ${this.#certificateIndex.size})`);
    } else {
      logger.info(`All certificates up to date (${this.#certificateIndex.size} total)`);
    }
  }
  /** Fetch certificates from DCL for the specified environment. */
  async #fetchDclCertificates(storage, isProduction, force) {
    const environment = isProduction ? "production" : "test";
    logger.debug(`Fetching PAA certificates from DCL (${environment})`);
    const config = isProduction ? this.#options.dclConfig ?? DclConfig.production : this.#options.testDclConfig ?? DclConfig.test;
    const dclClient = new DclClient(config);
    const certRefs = await dclClient.fetchRootCertificateList(this.#options);
    logger.debug(`Found ${certRefs.length} ${environment} root certificates in DCL`);
    for (const certRef of certRefs) {
      if (this.#closed) {
        return;
      }
      await this.#fetchAndStoreCertificate(storage, dclClient, certRef, isProduction, force, this.#options);
    }
  }
  /** Save the certificate index to storage. */
  #saveIndex() {
    if (this.#closed) {
      return;
    }
    if (!this.#storage) {
      throw new MatterDclError("Storage context not initialized");
    }
    const indexArray = Array.from(this.#certificateIndex.values());
    return this.#storage.set("index", indexArray);
  }
  /** Fetch and store a single certificate from DCL by its subject reference. */
  async #fetchAndStoreCertificate(storage, dclClient, certRef, isProduction, force, options) {
    try {
      const normalizedSubjectKeyId = this.#normalizeSubjectKeyId(certRef.subjectKeyId);
      if (!force && this.#certificateIndex.has(normalizedSubjectKeyId)) {
        const existing = this.#certificateIndex.get(normalizedSubjectKeyId);
        if (isProduction && !existing.isProduction) {
          existing.isProduction = true;
          logger.debug(
            `Upgraded certificate to production`,
            Diagnostic.dict({ skid: normalizedSubjectKeyId })
          );
        } else {
          logger.debug(
            `Certificate already exists, skipping`,
            Diagnostic.dict({ skid: normalizedSubjectKeyId })
          );
        }
        return;
      }
      const certs = await dclClient.fetchRootCertificateBySubject(certRef, options);
      for (const cert of certs) {
        if (!cert.subjectKeyId) {
          logger.warn(
            `Certificate for subject ${cert.subject} is missing subjectKeyId, skipping`,
            Diagnostic.dict({ subject: cert.subject })
          );
          continue;
        }
        const subjectKeyId = this.#normalizeSubjectKeyId(cert.subjectKeyId);
        const derBytes = Pem.asDer(cert.pemCert);
        const { subject, subjectAsText, serialNumber, vid, isRoot } = cert;
        await this.#storeCertificate(storage, subjectKeyId, derBytes, {
          subject,
          subjectAsText,
          subjectKeyId,
          serialNumber,
          vid,
          isRoot,
          isProduction,
          kind: "PAA"
        });
        logger.debug(`Stored certificate`, Diagnostic.dict({ skid: normalizedSubjectKeyId, vid: cert.vid }));
      }
    } catch (error) {
      logger.info(
        `Failed to fetch certificate ${certRef.subject}/${certRef.subjectKeyId}`,
        Diagnostic.errorMessage(asError(error))
      );
    }
  }
  #hasTestCertificatesOfKind(kind) {
    for (const metadata of this.#certificateIndex.values()) {
      if (!metadata.isProduction && (metadata.kind ?? "PAA") === kind) {
        return true;
      }
    }
    return false;
  }
  /**
   * Log a GitHub fetch failure. GitHub only serves test certificates, so a rate-limit error is only debug-worthy
   * once we already hold test certificates of the affected kind: the cached data remains usable, making the failed
   * refresh not actionable for the operator. A rate-limit response also flags the rest of the current fetch run to
   * skip GitHub rather than repeating the same failing request.
   */
  #logGithubFetchFailure(message, error, kind) {
    const rateLimited = error instanceof Github.HttpError && error.isRateLimit;
    if (rateLimited) {
      this.#githubRateLimited = true;
    }
    const level = rateLimited && this.#hasTestCertificatesOfKind(kind) ? LogLevel.DEBUG : LogLevel.INFO;
    logger.log(level, message, Diagnostic.errorMessage(asError(error)));
  }
  /**
   * Fetch development certificates from GitHub repository.
   */
  async #fetchGitHubCertificates(storage, force) {
    if (this.#githubRateLimited) {
      logger.debug("Skipping GitHub certificate fetch, GitHub is rate-limiting this run");
      return;
    }
    try {
      logger.debug("Fetching development certificates from GitHub");
      const { owner, repo, branch, certPath } = this.#options.githubConfig ?? DclGithubConfig.defaults;
      const repoClient = new Github.Repo(owner, repo, branch, this.#options);
      const certDir = await repoClient.cd(certPath);
      const files = await certDir.ls();
      const certFiles = files.filter((name) => name.endsWith(".der") && !name.startsWith("dcld_mirror_"));
      logger.debug(`Found ${certFiles.length} certificate files on GitHub`);
      for (const filename of certFiles) {
        if (this.#closed) {
          return;
        }
        await this.#fetchGitHubCertificate(storage, certDir, filename, force);
      }
    } catch (error) {
      this.#logGithubFetchFailure("Failed to fetch certificates from GitHub", error, "PAA");
    }
  }
  /**
   * Fetch CD signer certificates from the configured GitHub directory.
   * Stores each as a CDSigner entry with isProduction=false (user may promote via addCertificate).
   */
  async #fetchGitHubCdSignerCertificates(storage, force) {
    if (this.#githubRateLimited) {
      logger.debug("Skipping GitHub CD signer fetch, GitHub is rate-limiting this run");
      return;
    }
    try {
      logger.debug("Fetching CD signer certificates from GitHub");
      const { owner, repo, branch, cdSignerCertPath } = this.#options.githubConfig ?? DclGithubConfig.defaults;
      const repoClient = new Github.Repo(owner, repo, branch, this.#options);
      const certDir = await repoClient.cd(cdSignerCertPath);
      const files = await certDir.ls();
      const certFiles = files.filter((name) => name.endsWith(".der"));
      logger.debug(`Found ${certFiles.length} CD signer certificate files on GitHub`);
      for (const filename of certFiles) {
        if (this.#closed) {
          return;
        }
        await this.#fetchGitHubCdSignerCertificate(storage, certDir, filename, force);
      }
    } catch (error) {
      this.#logGithubFetchFailure("Failed to fetch CD signer certificates from GitHub", error, "CDSigner");
    }
  }
  /**
   * Fetch a single CD signer certificate from GitHub.
   */
  async #fetchGitHubCdSignerCertificate(storage, certDir, filename, force) {
    try {
      const derBytes = await certDir.getBinary(filename);
      const cert = Certificate.parseAsn1Certificate(derBytes, Certificate.REQUIRED_EXTENSIONS);
      const subjectKeyId = this.#normalizeSubjectKeyId(cert.extensions.subjectKeyIdentifier);
      if (!force && this.#certificateIndex.has(subjectKeyId)) {
        logger.debug(`CD signer certificate already exists, skipping`, Diagnostic.dict({ skid: subjectKeyId }));
        return;
      }
      const subject = filename.replace(".der", "");
      const serialNumber = Bytes.toHex(cert.serialNumber);
      const vid = cert.subject.vendorId ?? 0;
      await this.#storeCertificate(storage, subjectKeyId, Bytes.of(derBytes), {
        subject,
        subjectKeyId,
        serialNumber,
        vid,
        isRoot: false,
        isProduction: false,
        kind: "CDSigner"
      });
      logger.debug(`Stored GitHub CD signer certificate`, Diagnostic.dict({ skid: subjectKeyId, filename }));
    } catch (error) {
      logger.info(
        `Failed to fetch GitHub CD signer certificate ${filename}`,
        Diagnostic.errorMessage(asError(error))
      );
    }
  }
  /**
   * Fetch a single certificate from GitHub by filename.
   */
  async #fetchGitHubCertificate(storage, certDir, filename, force) {
    try {
      const derBytes = await certDir.getBinary(filename);
      const paa = Paa.fromAsn1(derBytes);
      const subjectKeyId = this.#normalizeSubjectKeyId(paa.cert.extensions.subjectKeyIdentifier);
      if (!force && this.#certificateIndex.has(subjectKeyId)) {
        logger.debug(`Certificate already exists, skipping`, Diagnostic.dict({ skid: subjectKeyId }));
        return;
      }
      const subject = filename.replace(".der", "");
      const serialNumber = Bytes.toHex(paa.cert.serialNumber);
      const vid = paa.cert.subject.vendorId ?? 0;
      await this.#storeCertificate(storage, subjectKeyId, derBytes, {
        subject,
        subjectKeyId,
        serialNumber,
        vid,
        isRoot: true,
        isProduction: false,
        kind: "PAA"
      });
      logger.debug(`Stored GitHub certificate`, Diagnostic.dict({ skid: subjectKeyId, filename }));
    } catch (error) {
      logger.info(`Failed to fetch GitHub certificate ${filename}`, Diagnostic.errorMessage(asError(error)));
    }
  }
  /**
   * Store a certificate and its metadata.
   */
  async #storeCertificate(storage, subjectKeyId, derBytes, metadata) {
    const existing = this.#certificateIndex.get(subjectKeyId);
    if (existing?.isProduction && !metadata.isProduction) {
      metadata = { ...metadata, isProduction: true };
    }
    if (metadata.fetchedAt === void 0) {
      metadata = { ...metadata, fetchedAt: existing?.fetchedAt ?? Time.nowMs };
    }
    await storage.set(subjectKeyId, derBytes);
    this.#certificateIndex.set(subjectKeyId, metadata);
  }
  async #consumeCertSeed(source, certKind) {
    const inserted = new Array();
    let expiredCount = 0;
    const parsedBuiltAt = Date.parse(source.builtAt);
    const fetchedAt = Number.isNaN(parsedBuiltAt) ? Date.now() : parsedBuiltAt;
    const isPaa = certKind === "PAA";
    try {
      for await (const entry of source.entries) {
        if (this.#closed) break;
        try {
          if (typeof entry.subjectKeyId !== "string" || !entry.subjectKeyId || typeof entry.derHex !== "string" || !entry.derHex || entry.kind !== "production" && entry.kind !== "test") {
            throw new MatterDclError("invalid entry shape");
          }
          if (entry.kind === "test" && !this.#options.fetchTestCertificates) {
            continue;
          }
          if (entry.notAfter !== void 0 && Date.parse(entry.notAfter) < Date.now()) {
            expiredCount++;
            continue;
          }
          const der = Bytes.fromHex(entry.derHex);
          const parsed = Certificate.parseAsn1Certificate(
            der,
            isPaa ? Certificate.REQUIRED_PAA_EXTENSIONS : Certificate.REQUIRED_EXTENSIONS
          );
          const skidFromDer = this.#normalizeSubjectKeyId(parsed.extensions.subjectKeyIdentifier);
          const entrySkid = this.#normalizeSubjectKeyId(entry.subjectKeyId);
          if (skidFromDer !== entrySkid) {
            logger.warn(
              `seed: SKID mismatch entry=${entrySkid} der-derived=${skidFromDer}, skipped`,
              Diagnostic.dict({ certKind })
            );
            continue;
          }
          if (this.#certificateIndex.has(skidFromDer)) {
            continue;
          }
          const isProduction = entry.kind === "production";
          const vid = parsed.subject.vendorId ?? 0;
          await this.#storeCertificate(this.#storage, skidFromDer, Bytes.of(der), {
            subject: parsed.subject.commonName,
            subjectKeyId: skidFromDer,
            serialNumber: Bytes.toHex(parsed.serialNumber),
            vid,
            isRoot: isPaa,
            isProduction,
            kind: certKind,
            fetchedAt
          });
          inserted.push(skidFromDer);
        } catch (err) {
          logger.warn("seed: malformed entry, aborting stream", Diagnostic.errorMessage(asError(err)));
          break;
        }
      }
    } catch (err) {
      logger.warn("seed: stream failed", Diagnostic.errorMessage(asError(err)));
    }
    if (inserted.length > 0) {
      await this.#saveIndex();
    }
    if (expiredCount > 0) {
      logger.info(`seed: skipped ${expiredCount} expired ${certKind} certs from snapshot ${source.builtAt}`);
    }
    logger.info(`seed: consumed ${inserted.length} ${certKind} certs from snapshot ${source.builtAt}`);
  }
  /**
   * Fetch and validate revocation data for a single AKID on demand.
   * Called by AsyncCache on cache miss. Queries DCL by issuer, downloads the CRL,
   * validates the signer chain and CRL signature, then returns the parsed revocation entry.
   *
   * Returns a RevocationEntry on success (cached for 1h). Throws on transient failures
   * (network timeout, server error) so the result is NOT cached and the next call retries.
   */
  async #fetchAndValidateRevocation(akid) {
    const empty = { serials: /* @__PURE__ */ new Set() };
    const revocationTimeout = Seconds(3);
    const config = this.#options.dclConfig ?? DclConfig.production;
    const dclClient = new DclClient(config);
    let points;
    try {
      points = await dclClient.fetchRevocationDistributionPointsByIssuer(akid, {
        timeout: revocationTimeout
      });
    } catch (error) {
      if (error instanceof MatterDclResponseError && error.response.code === 5) {
        logger.debug(`No CRL registered in DCL for AKID ${akid}`);
        return empty;
      }
      logger.warn(
        `DCL revocation lookup failed for AKID ${akid} (will retry on next check):`,
        Diagnostic.errorMessage(asError(error))
      );
      throw error;
    }
    const crlPoints = points.filter((p) => p.revocationType === RevocationTypeEnum.Crl);
    if (crlPoints.length === 0) {
      return empty;
    }
    for (const point of crlPoints) {
      try {
        return await this.#processRevocationPoint(point, revocationTimeout);
      } catch (error) {
        logger.warn(
          `Failed to process revocation point for ${point.issuerSubjectKeyId} (will retry on next check):`,
          Diagnostic.errorMessage(asError(error))
        );
      }
    }
    throw new MatterDclError(`All CRL downloads failed for AKID ${akid}`);
  }
  /**
   * Process a single revocation distribution point: download the CRL, validate the signer chain
   * and CRL signature per spec Section 6.2.4.1, then extract revoked serial numbers.
   */
  async #processRevocationPoint(point, timeout) {
    let signerPublicKey;
    try {
      signerPublicKey = await this.#validateCrlSigner(point);
    } catch (error) {
      logger.info(
        `CRL signer validation failed for ${point.issuerSubjectKeyId}, skipping CRL signature check:`,
        Diagnostic.errorMessage(asError(error))
      );
    }
    const response = await fetch(point.dataUrl, {
      signal: AbortSignal.timeout(timeout)
    });
    if (!response.ok) {
      throw new MatterDclError(`Failed to fetch CRL from ${point.dataUrl}: ${response.status}`);
    }
    const crlBytes = new Uint8Array(await response.arrayBuffer());
    if (point.dataFileSize !== void 0) {
      const expectedSize = typeof point.dataFileSize === "bigint" ? Number(point.dataFileSize) : point.dataFileSize;
      if (expectedSize > 0 && crlBytes.length !== expectedSize) {
        throw new MatterDclError(`CRL size mismatch: expected ${expectedSize} bytes, got ${crlBytes.length}`);
      }
    }
    if (point.dataDigest !== void 0 && point.dataDigestType !== void 0) {
      const algorithm = hashAlgorithmForId(point.dataDigestType);
      if (algorithm !== void 0) {
        const actualDigest = Bytes.toBase64(await this.#crypto.computeHash(crlBytes, algorithm));
        if (actualDigest !== point.dataDigest) {
          throw new MatterDclError(`CRL digest mismatch: expected ${point.dataDigest}, got ${actualDigest}`);
        }
      } else {
        logger.info(`Skipping CRL digest verification: unsupported digest type ${point.dataDigestType}`);
      }
    }
    const crlParsed = DclCertificateService.parseCrl(crlBytes);
    if (signerPublicKey !== void 0 && crlParsed.tbsDer !== void 0 && crlParsed.signatureValue !== void 0 && Bytes.of(crlParsed.signatureValue).length > 0) {
      try {
        await this.#crypto.verifyEcdsa(
          PublicKey(signerPublicKey),
          crlParsed.tbsDer,
          new EcdsaSignature(crlParsed.signatureValue, "der")
        );
      } catch {
        throw new MatterDclError("CRL signature verification failed against CRLSignerCertificate");
      }
    }
    return {
      serials: crlParsed.serials,
      issuerDnDerHex: crlParsed.issuerDnDerHex
    };
  }
  /**
   * Validate the CRL signer certificate chain per spec Section 6.2.4.1 steps 2-5.
   * Returns the signer's public key for CRL signature verification, or throws on failure.
   */
  async #validateCrlSigner(point) {
    const signerDer = Pem.asDer(point.crlSignerCertificate);
    const signerCert = point.isPAA ? Paa.fromAsn1(signerDer) : Pai.fromAsn1(signerDer);
    let delegatorCert;
    if (point.crlSignerDelegator) {
      const delegatorDer = Pem.asDer(point.crlSignerDelegator);
      delegatorCert = Pai.fromAsn1(delegatorDer);
    }
    if (point.isPAA) {
      const signerVid = signerCert.cert.subject.vendorId;
      if (signerVid !== void 0 && signerVid !== point.vid) {
        throw new MatterDclError(
          `CRLSignerCertificate VendorID ${signerVid} does not match entry VendorID ${point.vid}`
        );
      }
    } else {
      const certToCheck = delegatorCert ?? signerCert;
      const checkVid = certToCheck.cert.subject.vendorId;
      if (checkVid !== void 0 && checkVid !== point.vid) {
        throw new MatterDclError(`CRL signer VendorID ${checkVid} does not match entry VendorID ${point.vid}`);
      }
      if (point.pid !== void 0) {
        const checkPid = certToCheck instanceof Pai ? certToCheck.cert.subject.productId : void 0;
        if (checkPid !== void 0 && checkPid !== point.pid) {
          throw new MatterDclError(
            `CRL signer ProductID ${checkPid} does not match entry ProductID ${point.pid}`
          );
        }
      }
    }
    const signerAkid = signerCert.cert.extensions.authorityKeyIdentifier;
    if (signerAkid !== void 0) {
      const signerAkidNorm = this.#normalizeSubjectKeyId(signerAkid);
      const trustAllPaas = { considerTestCertificates: true };
      let issuerPublicKey;
      if (delegatorCert !== void 0) {
        const delegatorAkid = delegatorCert.cert.extensions.authorityKeyIdentifier;
        if (delegatorAkid === void 0 || !this.#certificateIndex.has(this.#normalizeSubjectKeyId(delegatorAkid))) {
          throw new MatterDclError("CRLSignerDelegator chain cannot be anchored to trusted PAA");
        }
        const paaDer = await this.#getCertificateDer(delegatorAkid, trustAllPaas);
        const paa = Paa.fromAsn1(paaDer);
        await this.#crypto.verifyEcdsa(
          PublicKey(paa.cert.ellipticCurvePublicKey),
          delegatorCert.asUnsignedDer(),
          delegatorCert.signature
        );
        issuerPublicKey = delegatorCert.cert.ellipticCurvePublicKey;
      } else if (this.#certificateIndex.has(signerAkidNorm)) {
        const paaDer = await this.#getCertificateDer(signerAkid, trustAllPaas);
        const paa = Paa.fromAsn1(paaDer);
        issuerPublicKey = paa.cert.ellipticCurvePublicKey;
      }
      if (issuerPublicKey === void 0) {
        throw new MatterDclError(
          `CRLSignerCertificate chain cannot be anchored to trusted PAA (AKID: ${signerAkidNorm})`
        );
      }
      await this.#crypto.verifyEcdsa(
        PublicKey(issuerPublicKey),
        signerCert.asUnsignedDer(),
        signerCert.signature
      );
    }
    return signerCert.cert.ellipticCurvePublicKey;
  }
  /** Result of parsing a CRL. */
  static parseCrl(crlDer) {
    const serials = /* @__PURE__ */ new Set();
    const decoded = DerCodec.decode(crlDer);
    const certListElements = decoded._elements;
    if (!certListElements || certListElements.length < 2) {
      return { serials };
    }
    const tbsCertList = certListElements[0];
    const tbsElements = tbsCertList._elements;
    if (!tbsElements) {
      return { serials };
    }
    const tbsDer = DerCodec.encode(tbsCertList);
    let signatureValue;
    if (certListElements.length >= 3 && certListElements[2]._tag === 3) {
      signatureValue = Bytes.of(certListElements[2]._bytes);
    }
    let idx = 0;
    if (tbsElements[idx]?._tag === DerType.Integer) {
      idx++;
    }
    if (tbsElements[idx]?._tag === DerTag.Sequence) {
      idx++;
    }
    let issuerDnDerHex;
    if (tbsElements[idx]?._tag === DerTag.Sequence) {
      const issuerNode = tbsElements[idx];
      issuerDnDerHex = Bytes.toHex(DerCodec.encode(issuerNode)).toUpperCase();
      idx++;
    }
    if (tbsElements[idx]?._tag === DerType.UtcDate || tbsElements[idx]?._tag === DerType.GeneralizedTime) {
      idx++;
    }
    let nextUpdateMs;
    if (tbsElements[idx]?._tag === DerType.UtcDate || tbsElements[idx]?._tag === DerType.GeneralizedTime) {
      try {
        const dateStr = Bytes.toString(tbsElements[idx]._bytes);
        let year;
        let rest;
        if (tbsElements[idx]._tag === DerType.UtcDate) {
          const yy = parseInt(dateStr.slice(0, 2));
          year = yy >= 50 ? 1900 + yy : 2e3 + yy;
          rest = dateStr.slice(2);
        } else {
          year = parseInt(dateStr.slice(0, 4));
          rest = dateStr.slice(4);
        }
        const month = parseInt(rest.slice(0, 2)) - 1;
        const day = parseInt(rest.slice(2, 4));
        const hour = parseInt(rest.slice(4, 6));
        const min = parseInt(rest.slice(6, 8));
        const sec = parseInt(rest.slice(8, 10));
        nextUpdateMs = Date.UTC(year, month, day, hour, min, sec);
      } catch {
      }
    }
    let authorityKeyId;
    for (const element of tbsElements) {
      if (element._tag === 160 && element._bytes) {
        const extSequence = DerCodec.decode(element._bytes);
        if (extSequence._elements) {
          for (const ext of extSequence._elements) {
            if (!ext._elements || ext._elements.length < 2) continue;
            const oid = Bytes.toHex(ext._elements[0]._bytes);
            if (oid === "551d23") {
              const valueIdx = ext._elements.length > 2 ? 2 : 1;
              const akiOctetString = ext._elements[valueIdx];
              if (akiOctetString._bytes) {
                const akiSeq = DerCodec.decode(akiOctetString._bytes);
                if (akiSeq._elements) {
                  for (const akiField of akiSeq._elements) {
                    if (akiField._tag === 128) {
                      authorityKeyId = Bytes.toHex(Bytes.of(akiField._bytes)).toUpperCase();
                      break;
                    }
                  }
                }
              }
              break;
            }
          }
        }
      }
    }
    let revokedCertsNode;
    for (const element of tbsElements) {
      if (element._tag === DerTag.Sequence && element._elements) {
        const firstChild = element._elements[0];
        if (firstChild && firstChild._tag === DerTag.Sequence && firstChild._elements && firstChild._elements[0]?._tag === DerType.Integer) {
          revokedCertsNode = element;
          break;
        }
      }
    }
    if (!revokedCertsNode?._elements) {
      return { serials, issuerDnDerHex, authorityKeyId, tbsDer, signatureValue, nextUpdateMs };
    }
    for (const entry of revokedCertsNode._elements) {
      if (entry._tag !== DerTag.Sequence || !entry._elements || entry._elements.length < 1) continue;
      const serialNode = entry._elements[0];
      if (serialNode._tag !== DerType.Integer) continue;
      const serialHex = Bytes.toHex(Bytes.of(serialNode._bytes)).toUpperCase();
      serials.add(serialHex);
    }
    return { serials, issuerDnDerHex, authorityKeyId, tbsDer, signatureValue, nextUpdateMs };
  }
  /** Convenience wrapper that returns only the revoked serial numbers from a CRL. */
  static parseCrlRevokedSerials(crlDer) {
    return DclCertificateService.parseCrl(crlDer).serials;
  }
}
export {
  DclCertificateService
};
//# sourceMappingURL=DclCertificateService.js.map
