{"version":3,"file":"X509Service.mjs","names":[],"sources":["../../../src/modules/x509/X509Service.ts"],"sourcesContent":["import * as x509 from '@peculiar/x509'\nimport { AgentContext } from '../../agent'\nimport { CredoWebCrypto } from '../../crypto/webcrypto'\nimport { injectable } from '../../plugins'\nimport { CertificateSigningRequest } from './CertificateSigningRequest'\nimport { X509Certificate } from './X509Certificate'\nimport { X509Error } from './X509Error'\nimport type {\n  X509CreateCertificateOptions,\n  X509CreateCertificateSigningRequestOptions,\n  X509GetLeafCertificateOptions,\n  X509ParseCertificateOptions,\n  X509ParseCertificateSigningRequestOptions,\n  X509ValidateCertificateChainOptions,\n} from './X509ServiceOptions'\n\n@injectable()\n// biome-ignore lint/complexity/noStaticOnlyClass: no explanation\nexport class X509Service {\n  /**\n   *\n   * Validate a chain of X.509 certificates according to RFC 5280\n   *\n   * This function requires a list of base64 encoded certificates and, optionally, a certificate that should be found in the chain.\n   * If no certificate is provided, it will just assume the leaf certificate\n   *\n   * The leaf certificate should be the 0th index and the root the last\n   *\n   * The Issuer of the certificate is found with the following algorithm:\n   * - Check if there is an AuthorityKeyIdentifierExtension\n   * - Go through all the other certificates and see if the SubjectKeyIdentifier is equal to the AuthorityKeyIdentifier\n   * - If they are equal, the certificate is verified and returned as the issuer\n   *\n   * Additional validation:\n   *   - Make sure at least a single certificate is in the chain\n   *   - Check whether a certificate in the chain matches with a trusted certificate\n   */\n  public static async validateCertificateChain(\n    agentContext: AgentContext,\n    {\n      certificateChain,\n      certificate = certificateChain[0],\n      verificationDate = new Date(),\n      trustedCertificates,\n    }: X509ValidateCertificateChainOptions\n  ) {\n    if (certificateChain.length === 0) throw new X509Error('Certificate chain is empty')\n    const webCrypto = new CredoWebCrypto(agentContext)\n\n    let parsedLeafCertificate: x509.X509Certificate\n    let certificatesToBuildChain: x509.X509Certificate[]\n    try {\n      parsedLeafCertificate = new x509.X509Certificate(certificate)\n      certificatesToBuildChain = [...certificateChain, ...(trustedCertificates ?? [])].map(\n        (c) => new x509.X509Certificate(c)\n      )\n    } catch (error) {\n      throw new X509Error('Error during parsing of x509 certificate', { cause: error })\n    }\n\n    const certificateChainBuilder = new x509.X509ChainBuilder({\n      certificates: certificatesToBuildChain,\n    })\n\n    const chain = await certificateChainBuilder.build(parsedLeafCertificate, webCrypto)\n\n    // The chain is reversed here as the `x5c` header (the expected input),\n    // has the leaf certificate as the first entry, while the `x509` library expects this as the last\n    let parsedChain = chain.map((c) => X509Certificate.fromRawCertificate(new Uint8Array(c.rawData))).reverse()\n\n    // We allow longer parsed chain, in case the root cert was not part of the chain, but in the\n    // list of trusted certificates\n    if (parsedChain.length < certificateChain.length) {\n      throw new X509Error('Could not parse the full chain. Likely due to incorrect ordering')\n    }\n\n    let previousCertificate: X509Certificate | undefined\n\n    if (trustedCertificates) {\n      const parsedTrustedCertificates = trustedCertificates.map((trustedCertificate) =>\n        X509Certificate.fromEncodedCertificate(trustedCertificate)\n      )\n\n      const trustedCertificateIndex = parsedChain.findIndex((cert) =>\n        parsedTrustedCertificates.some((tCert) => cert.equal(tCert))\n      )\n\n      if (trustedCertificateIndex === -1) {\n        throw new X509Error('No trusted certificate was found while validating the X.509 chain')\n      }\n\n      if (trustedCertificateIndex > 0) {\n        // When we trust a certificate other than the first certificate in the provided chain we keep a reference to the\n        // previous certificate as we need the key of this certificate to verify the first certificate in the chain as\n        // it's not self-sigend.\n        previousCertificate = parsedChain[trustedCertificateIndex - 1]\n\n        // Pop everything off before the index of the trusted certificate (those are more root) as it is not relevant for validation\n        parsedChain = parsedChain.slice(trustedCertificateIndex)\n      }\n    }\n\n    // Verify the certificate with the publicKey of the certificate above\n    for (let i = 0; i < parsedChain.length; i++) {\n      const cert = parsedChain[i]\n      const publicJwk = previousCertificate ? previousCertificate.publicJwk : undefined\n\n      // The only scenario where this will trigger is if the trusted certificates and the x509 chain both do not contain the\n      // intermediate/root certificate needed. E.g. for ISO 18013-5 mDL the root cert MUST NOT be in the chain. If the signer\n      // certificate is then trusted, it will fail, as we can't verify the signer certifciate without having access to the signer\n      // key of the root certificate.\n      // See also https://github.com/openid/OpenID4VCI/issues/62\n      //\n      // In this case we could skip the signature verification (not other verifications), as we already trust the signer certificate,\n      // but i think the purpose of ISO 18013-5 mDL is that you trust the root certificate. If we can't verify the whole chain e.g.\n      // when we receive a credential we have the chance it will fail later on.\n      const skipSignatureVerification = i === 0 && trustedCertificates && !publicJwk\n      // NOTE: at some point we might want to change this to throw an error instead of skipping the signature verification of the trusted\n      // but it would basically prevent mDOCs from unknown issuers to be verified in the wallet. Verifiers should only trust the root certificate\n      // anyway.\n      // if (i === 0 && trustedCertificates && cert.issuer !== cert.subject && !publicKey) {\n      //   throw new X509Error(\n      //     'Unable to verify the certificate chain. A non-self-signed certificate is the first certificate in the chain, and no parent certificate was found in the trusted certificates, meaning the first certificate in the chain cannot be verified. Ensure the certificate is added '\n      //   )\n      // }\n\n      await cert.verify(\n        {\n          publicJwk,\n          verificationDate,\n          skipSignatureVerification,\n        },\n        webCrypto\n      )\n      previousCertificate = cert\n    }\n\n    return parsedChain\n  }\n\n  /**\n   *\n   * Parses a base64-encoded X.509 certificate into a {@link X509Certificate}\n   *\n   */\n  public static parseCertificate(\n    _agentContext: AgentContext,\n    { encodedCertificate }: X509ParseCertificateOptions\n  ): X509Certificate {\n    const certificate = X509Certificate.fromEncodedCertificate(encodedCertificate)\n\n    return certificate\n  }\n\n  public static getLeafCertificate(\n    _agentContext: AgentContext,\n    { certificateChain }: X509GetLeafCertificateOptions\n  ): X509Certificate {\n    if (certificateChain.length === 0) throw new X509Error('Certificate chain is empty')\n\n    const certificate = X509Certificate.fromEncodedCertificate(certificateChain[0])\n\n    return certificate\n  }\n\n  public static async createCertificate(agentContext: AgentContext, options: X509CreateCertificateOptions) {\n    const webCrypto = new CredoWebCrypto(agentContext)\n\n    const certificate = await X509Certificate.create(options, webCrypto)\n\n    return certificate\n  }\n\n  public static async createCertificateSigningRequest(\n    agentContext: AgentContext,\n    options: X509CreateCertificateSigningRequestOptions\n  ) {\n    const webCrypto = new CredoWebCrypto(agentContext)\n\n    const csr = await CertificateSigningRequest.create(options, webCrypto)\n\n    return csr\n  }\n\n  public static parseCertificateSigningRequest({\n    encodedCertificateSigningRequest,\n  }: X509ParseCertificateSigningRequestOptions) {\n    const csr = CertificateSigningRequest.fromEncodedCertificateRequest(encodedCertificateSigningRequest)\n\n    return csr\n  }\n}\n"],"mappings":";;;;;;;;;;;;;AAkBO,wBAAM,YAAY;;;;;;;;;;;;;;;;;;;CAmBvB,aAAoB,yBAClB,cACA,EACE,kBACA,cAAc,iBAAiB,IAC/B,mCAAmB,IAAI,MAAM,EAC7B,uBAEF;AACA,MAAI,iBAAiB,WAAW,EAAG,OAAM,IAAI,UAAU,6BAA6B;EACpF,MAAM,YAAY,IAAI,eAAe,aAAa;EAElD,IAAI;EACJ,IAAI;AACJ,MAAI;AACF,2BAAwB,IAAI,KAAK,gBAAgB,YAAY;AAC7D,8BAA2B,CAAC,GAAG,kBAAkB,GAAI,uBAAuB,EAAE,CAAE,CAAC,KAC9E,MAAM,IAAI,KAAK,gBAAgB,EAAE,CACnC;WACM,OAAO;AACd,SAAM,IAAI,UAAU,4CAA4C,EAAE,OAAO,OAAO,CAAC;;EAWnF,IAAI,eAJU,MAJkB,IAAI,KAAK,iBAAiB,EACxD,cAAc,0BACf,CAAC,CAE0C,MAAM,uBAAuB,UAAU,EAI3D,KAAK,MAAM,gBAAgB,mBAAmB,IAAI,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC,SAAS;AAI3G,MAAI,YAAY,SAAS,iBAAiB,OACxC,OAAM,IAAI,UAAU,mEAAmE;EAGzF,IAAI;AAEJ,MAAI,qBAAqB;GACvB,MAAM,4BAA4B,oBAAoB,KAAK,uBACzD,gBAAgB,uBAAuB,mBAAmB,CAC3D;GAED,MAAM,0BAA0B,YAAY,WAAW,SACrD,0BAA0B,MAAM,UAAU,KAAK,MAAM,MAAM,CAAC,CAC7D;AAED,OAAI,4BAA4B,GAC9B,OAAM,IAAI,UAAU,oEAAoE;AAG1F,OAAI,0BAA0B,GAAG;AAI/B,0BAAsB,YAAY,0BAA0B;AAG5D,kBAAc,YAAY,MAAM,wBAAwB;;;AAK5D,OAAK,IAAI,IAAI,GAAG,IAAI,YAAY,QAAQ,KAAK;GAC3C,MAAM,OAAO,YAAY;GACzB,MAAM,YAAY,sBAAsB,oBAAoB,YAAY;GAWxE,MAAM,4BAA4B,MAAM,KAAK,uBAAuB,CAAC;AAUrE,SAAM,KAAK,OACT;IACE;IACA;IACA;IACD,EACD,UACD;AACD,yBAAsB;;AAGxB,SAAO;;;;;;;CAQT,OAAc,iBACZ,eACA,EAAE,sBACe;AAGjB,SAFoB,gBAAgB,uBAAuB,mBAAmB;;CAKhF,OAAc,mBACZ,eACA,EAAE,oBACe;AACjB,MAAI,iBAAiB,WAAW,EAAG,OAAM,IAAI,UAAU,6BAA6B;AAIpF,SAFoB,gBAAgB,uBAAuB,iBAAiB,GAAG;;CAKjF,aAAoB,kBAAkB,cAA4B,SAAuC;EACvG,MAAM,YAAY,IAAI,eAAe,aAAa;AAIlD,SAFoB,MAAM,gBAAgB,OAAO,SAAS,UAAU;;CAKtE,aAAoB,gCAClB,cACA,SACA;EACA,MAAM,YAAY,IAAI,eAAe,aAAa;AAIlD,SAFY,MAAM,0BAA0B,OAAO,SAAS,UAAU;;CAKxE,OAAc,+BAA+B,EAC3C,oCAC4C;AAG5C,SAFY,0BAA0B,8BAA8B,iCAAiC;;;0BA3KxG,YAAY"}