From 667f1370819d5bb128459fdf7b2c232d01b0ef6f Mon Sep 17 00:00:00 2001
From: Yun Lai <ylai@squareup.com>
Date: Wed, 6 Sep 2023 22:02:34 +1000
Subject: [PATCH] feat: add options to host rules to enable mTLS calls to host
 (#24155)

Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com>
Co-authored-by: Rhys Arkins <rhys@arkins.net>
---
 docs/usage/configuration-options.md | 18 ++++++++
 lib/config/options/index.ts         | 30 +++++++++++++
 lib/types/host-rules.ts             |  3 ++
 lib/util/http/host-rules.spec.ts    | 65 +++++++++++++++++++++++++++++
 lib/util/http/host-rules.ts         | 23 ++++++++++
 5 files changed, 139 insertions(+)

diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md
index b0fbe64344..695b889a16 100644
--- a/docs/usage/configuration-options.md
+++ b/docs/usage/configuration-options.md
@@ -1556,6 +1556,24 @@ To adjust it down to 10s for all queries, do this:
 }
 ```
 
+### httpsCertificateAuthority
+
+By default, Renovate uses the curated list of well-known [CA](https://en.wikipedia.org/wiki/Certificate_authority)s by Mozilla.
+You may use another Certificate Authority instead, by setting it in the `httpsCertificateAuthority` config option.
+
+### httpsPrivateKey
+
+Specifies the private key in [PEM format](https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail) for mTLS authentication.
+
+<!-- prettier-ignore -->
+!!! warning
+    Do _not_ put your private key into this field, to avoid losing confidentiality completely.
+    You must use [secrets](https://docs.renovatebot.com/self-hosted-configuration/#secrets) to pass it down securely instead.
+
+### httpsCertificate
+
+Specifies the [Certificate chains](https://en.wikipedia.org/wiki/X.509#Certificate_chains_and_cross-certification) in [PEM format](https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail) for mTLS authentication.
+
 ## ignoreDeprecated
 
 By default, Renovate won't update a dependency version to a deprecated release unless the current version was _itself_ deprecated.
diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts
index d7ae7c55ef..a3cdb0c41f 100644
--- a/lib/config/options/index.ts
+++ b/lib/config/options/index.ts
@@ -2348,6 +2348,36 @@ const options: RenovateOptions[] = [
     cli: false,
     env: false,
   },
+  {
+    name: 'httpsCertificateAuthority',
+    description: 'The overriding trusted CA certificate.',
+    type: 'string',
+    stage: 'repository',
+    parent: 'hostRules',
+    default: null,
+    cli: false,
+    env: false,
+  },
+  {
+    name: 'httpsPrivateKey',
+    description: 'The private key in PEM format.',
+    type: 'string',
+    stage: 'repository',
+    parent: 'hostRules',
+    default: null,
+    cli: false,
+    env: false,
+  },
+  {
+    name: 'httpsCertificate',
+    description: 'The certificate chains in PEM format.',
+    type: 'string',
+    stage: 'repository',
+    parent: 'hostRules',
+    default: null,
+    cli: false,
+    env: false,
+  },
   {
     name: 'cacheHardTtlMinutes',
     description:
diff --git a/lib/types/host-rules.ts b/lib/types/host-rules.ts
index ac02a813c3..058ed21e1e 100644
--- a/lib/types/host-rules.ts
+++ b/lib/types/host-rules.ts
@@ -15,6 +15,9 @@ export interface HostRuleSearchResult {
   dnsCache?: boolean;
   keepalive?: boolean;
   artifactAuth?: string[] | null;
+  httpsCertificateAuthority?: string;
+  httpsPrivateKey?: string;
+  httpsCertificate?: string;
 }
 
 export interface HostRule extends HostRuleSearchResult {
diff --git a/lib/util/http/host-rules.spec.ts b/lib/util/http/host-rules.spec.ts
index aeff4e65d1..6ad1f655b5 100644
--- a/lib/util/http/host-rules.spec.ts
+++ b/lib/util/http/host-rules.spec.ts
@@ -148,6 +148,71 @@ describe('util/http/host-rules', () => {
     `);
   });
 
+  it('certificateAuthority', () => {
+    hostRules.add({
+      hostType: 'maven',
+      matchHost: 'https://custom.datasource.ca',
+      httpsCertificateAuthority: 'ca-cert',
+    });
+
+    expect(
+      applyHostRules('https://custom.datasource.ca/data/path', {
+        ...options,
+        hostType: 'maven',
+      })
+    ).toMatchInlineSnapshot(`
+      {
+        "hostType": "maven",
+        "https": {
+          "certificateAuthority": "ca-cert",
+        },
+      }
+    `);
+  });
+
+  it('privateKey', () => {
+    hostRules.add({
+      hostType: 'maven',
+      matchHost: 'https://custom.datasource.key',
+      httpsPrivateKey: 'key',
+    });
+    expect(
+      applyHostRules('https://custom.datasource.key/data/path', {
+        ...options,
+        hostType: 'maven',
+      })
+    ).toMatchInlineSnapshot(`
+      {
+        "hostType": "maven",
+        "https": {
+          "key": "key",
+        },
+      }
+    `);
+  });
+
+  it('certificate', () => {
+    hostRules.add({
+      hostType: 'maven',
+      matchHost: 'https://custom.datasource.cert',
+      httpsCertificate: 'cert',
+    });
+
+    expect(
+      applyHostRules('https://custom.datasource.cert/data/path', {
+        ...options,
+        hostType: 'maven',
+      })
+    ).toMatchInlineSnapshot(`
+      {
+        "hostType": "maven",
+        "https": {
+          "certificate": "cert",
+        },
+      }
+    `);
+  });
+
   it('no fallback to github', () => {
     hostRules.add({
       hostType: 'github-tags',
diff --git a/lib/util/http/host-rules.ts b/lib/util/http/host-rules.ts
index bf88e5e888..577afb5976 100644
--- a/lib/util/http/host-rules.ts
+++ b/lib/util/http/host-rules.ts
@@ -30,6 +30,7 @@ export type HostRulesGotOptions = Pick<
   | 'lookup'
   | 'agent'
   | 'http2'
+  | 'https'
 >;
 
 export function findMatchingRules<GotOptions extends HostRulesGotOptions>(
@@ -162,6 +163,28 @@ export function applyHostRules<GotOptions extends HostRulesGotOptions>(
   if (!hasProxy() && foundRules.enableHttp2 === true) {
     options.http2 = true;
   }
+
+  if (is.nonEmptyString(foundRules.httpsCertificateAuthority)) {
+    options.https = {
+      ...(options.https ?? {}),
+      certificateAuthority: foundRules.httpsCertificateAuthority,
+    };
+  }
+
+  if (is.nonEmptyString(foundRules.httpsPrivateKey)) {
+    options.https = {
+      ...(options.https ?? {}),
+      key: foundRules.httpsPrivateKey,
+    };
+  }
+
+  if (is.nonEmptyString(foundRules.httpsCertificate)) {
+    options.https = {
+      ...(options.https ?? {}),
+      certificate: foundRules.httpsCertificate,
+    };
+  }
+
   return options;
 }
 
-- 
GitLab