From ee29fdcaf10b7cdf6619cc8dd05b44be11529f5b Mon Sep 17 00:00:00 2001 From: Rhys Arkins Date: Thu, 16 Sep 2021 12:11:13 +0200 Subject: [PATCH] feat(config): scoped secrets using pgp/gpg (#11673) --- docs/usage/configuration-options.md | 5 +- .../usage/getting-started/private-packages.md | 2 - docs/usage/self-hosted-configuration.md | 81 ++++++++- lib/config/__fixtures__/private-pgp.pem | 106 +++++++++++ lib/config/decrypt.spec.ts | 98 +++++++++-- lib/config/decrypt.ts | 118 +++++++++++-- lib/config/options/index.ts | 7 + lib/config/types.ts | 1 + lib/workers/global/config/parse/index.spec.ts | 5 + lib/workers/global/config/parse/index.ts | 5 + lib/workers/repository/init/merge.ts | 10 +- package.json | 1 + yarn.lock | 166 ++++++++++-------- 13 files changed, 491 insertions(+), 114 deletions(-) create mode 100644 lib/config/__fixtures__/private-pgp.pem diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index 8cfcb0a98af3dc..0c626b5262f7c3 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -603,7 +603,10 @@ For the full list of available managers, see the [Supported Managers](https://do ## encrypted -See [Private npm module support](https://docs.renovatebot.com/getting-started/private-packages) for details on how this is used to encrypt npm tokens. +See [Private module support](https://docs.renovatebot.com/getting-started/private-packages) for details on how this is used to encrypt npm tokens. + +Note: encrypted secrets must have at least an org/group scope, and optionally a repository scope. +This means that Renovate will check if a secret's scope matches the current repository before applying it, and warn/discard if there is a mismatch. ## excludeCommitPaths diff --git a/docs/usage/getting-started/private-packages.md b/docs/usage/getting-started/private-packages.md index f5e70f05a989aa..02fa5d52259b2d 100644 --- a/docs/usage/getting-started/private-packages.md +++ b/docs/usage/getting-started/private-packages.md @@ -240,8 +240,6 @@ The end-result looks like this: } ``` -However be aware that if your `.npmrc` is too big to encrypt then the above command will fail. - #### Automatically authenticate for npm package stored in private GitHub npm repository ```json diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md index 0928d3c06cd45b..339805bbc3693f 100644 --- a/docs/usage/self-hosted-configuration.md +++ b/docs/usage/self-hosted-configuration.md @@ -324,31 +324,94 @@ This private key is used to decrypt config files. The corresponding public key can be used to create encrypted values for config files. If you want a simple UI to encrypt values you can put the public key in a HTML page similar to . -To create the key pair with OpenSSL use the following commands: +To create the key pair with GPG use the following commands: -- `openssl genrsa -out rsa_priv.pem 4096` for generating the private key -- `openssl rsa -pubout -in rsa_priv.pem -out rsa_pub.pem` for extracting the public key +- `gpg --full-generate-key` and follow the prompts to generate a key. Name and email are not important to Renovate, and do not configure a passphrase. Use a 4096bit key. -To encrypt a secret with OpenSSL use the following command: +
key generation log -```bash -echo 'actual-secret' | openssl rsautl -encrypt -pubin -inkey rsa_pub.pem | base64 ``` +❯ gpg --full-generate-key +gpg (GnuPG) 2.2.24; Copyright (C) 2020 Free Software Foundation, Inc. +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law. + +Please select what kind of key you want: + (1) RSA and RSA (default) + (2) DSA and Elgamal + (3) DSA (sign only) + (4) RSA (sign only) + (14) Existing key from card +Your selection? 1 +RSA keys may be between 1024 and 4096 bits long. +What keysize do you want? (3072) 4096 +Requested keysize is 4096 bits +Please specify how long the key should be valid. + 0 = key does not expire + = key expires in n days + w = key expires in n weeks + m = key expires in n months + y = key expires in n years +Key is valid for? (0) +Key does not expire at all +Is this correct? (y/N) y + +GnuPG needs to construct a user ID to identify your key. + +Real name: Renovate Bot +Email address: renovate@whitesourcesoftware.com +Comment: +You selected this USER-ID: + "Renovate Bot " + +Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O + +gpg: key 0649CC3899F22A66 marked as ultimately trusted +gpg: revocation certificate stored as '/Users/rhys/.gnupg/openpgp-revocs.d/794B820F34B34A8DF32AADB20649CC3899F22A66.rev' +public and secret key created and signed. + +pub rsa4096 2021-09-10 [SC] + 794B820F34B34A8DF32AADB20649CEXAMPLEONLY +uid Renovate Bot +sub rsa4096 2021-09-10 [E] +``` + +
+ +- Copy the key ID from the output (`794B820F34B34A8DF32AADB20649CEXAMPLEONLY` in the above example) or run `gpg --list-secret-keys` if you forgot to take a copy +- Run `gpg --armor --export-secret-keys YOUR_NEW_KEY_ID > renovate-private-key.asc` to generate an armored (text-based) private key file +- Run `gpg --armor --export YOUR_NEW_KEY_ID > renovate-public-key.asc` to generate an armored (text-based) public key file + +The private key should then be added to your Renovate Bot global config (either using `privateKeyPath` or exporting it to the `RENOVATE_PRIVATE_KEY` environment variable). +The public key can be used to replace the existing key in for your own use. -Replace `actual-secret` with the secret to encrypt. +Any encrypted secrets using GPG must have a mandatory organization/group scope, and optionally can be scoped for a single repository only. +The reason for this is to avoid "replay" attacks where someone could learn your encrypted secret and then reuse it in their own Renovate repositories. +Instead, with scoped secrets it means that Renovate ensures that the organization and optionally repository values encrypted with the secret match against the running repository. + +Note: simple public key encryption was previously used to encrypt secrets, but this approach has now been deprecated and no longer documented. ## privateKeyOld Use this field if you need to perform a "key rotation" and support more than one keypair at a time. Decryption with this key will be attempted after `privateKey`. +If you are migrating from the legacy public key encryption approach to use GPG, then move your legacy private key from `privateKey` to `privateKeyOld` and then put your new GPG private key in `privateKey`. +Doing so will mean that Renovate will first attempt to decrypt using the GPG key but fall back to the legacy key and try that next. + +You can remove the `privateKeyOld` config option once all the old encrypted values have been migrated, or if you no longer want to support the old key and let the processing of repositories fail. + ## privateKeyPath -Used as an alternative to `privateKey`, if you wish for the key to be read from disk instead. +Used as an alternative to `privateKey`, if you want the key to be read from disk instead. + +## privateKeyPathOld + +Used as an alternative to `privateKeyOld`, if you want the key to be read from disk instead. ## productLinks -Override this object if you wish to change the URLs that Renovate links to, e.g. if you have an internal forum for asking for help. +Override this object if you want to change the URLs that Renovate links to, e.g. if you have an internal forum for asking for help. ## redisUrl diff --git a/lib/config/__fixtures__/private-pgp.pem b/lib/config/__fixtures__/private-pgp.pem new file mode 100644 index 00000000000000..2c3a411cdd9f96 --- /dev/null +++ b/lib/config/__fixtures__/private-pgp.pem @@ -0,0 +1,106 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +mJ4R+EnCuazu +=UnDn +-----END PGP PRIVATE KEY BLOCK----- diff --git a/lib/config/decrypt.spec.ts b/lib/config/decrypt.spec.ts index 970c7814571ef3..ba2ad76e2d1ff6 100644 --- a/lib/config/decrypt.spec.ts +++ b/lib/config/decrypt.spec.ts @@ -4,6 +4,8 @@ import { setGlobalConfig } from './global'; import type { RenovateConfig } from './types'; const privateKey = loadFixture('private.pem', '.'); +const privateKeyPgp = loadFixture('private-pgp.pem', '.'); +const repository = 'abc/def'; describe('config/decrypt', () => { describe('decryptConfig()', () => { @@ -12,29 +14,31 @@ describe('config/decrypt', () => { config = {}; setGlobalConfig(); }); - it('returns empty with no privateKey', () => { + it('returns empty with no privateKey', async () => { delete config.encrypted; - const res = decryptConfig(config); + const res = await decryptConfig(config, repository); expect(res).toMatchObject(config); }); - it('warns if no privateKey found', () => { + it('warns if no privateKey found', async () => { config.encrypted = { a: '1' }; - const res = decryptConfig(config); + const res = await decryptConfig(config, repository); expect(res.encrypted).not.toBeDefined(); expect(res.a).not.toBeDefined(); }); - it('handles invalid encrypted type', () => { + it('handles invalid encrypted type', async () => { config.encrypted = 1; setGlobalConfig({ privateKey }); - const res = decryptConfig(config); + const res = await decryptConfig(config, repository); expect(res.encrypted).not.toBeDefined(); }); - it('handles invalid encrypted value', () => { + it('handles invalid encrypted value', async () => { config.encrypted = { a: 1 }; setGlobalConfig({ privateKey, privateKeyOld: 'invalid-key' }); - expect(() => decryptConfig(config)).toThrow(Error('config-validation')); + await expect(decryptConfig(config, repository)).rejects.toThrow( + 'config-validation' + ); }); - it('replaces npm token placeholder in npmrc', () => { + it('replaces npm token placeholder in npmrc', async () => { setGlobalConfig({ privateKey: 'invalid-key', privateKeyOld: privateKey }); // test old key failover config.npmrc = '//registry.npmjs.org/:_authToken=${NPM_TOKEN}\n//registry.npmjs.org/:_authToken=${NPM_TOKEN}\n'; // eslint-disable-line no-template-curly-in-string @@ -42,26 +46,26 @@ describe('config/decrypt', () => { npmToken: 'FLA9YHIzpE7YetAg/P0X46npGRCMqn7hgyzwX5ZQ9wYgu9BRRbTiBVsUIFTyM5BuP1Q22slT2GkWvFvum7GU236Y6QiT7Nr8SLvtsJn2XUuq8H7REFKzdy3+wqyyWbCErYTFyY1dcPM7Ht+CaGDWdd8u/FsoX7AdMRs/X1jNUo6iSmlUiyGlYDKF+QMnCJom1VPVgZXWsGKdjI2MLny991QMaiv0VajmFIh4ENv4CtXOl/1twvIl/6XTXAaqpJJKDTPZEuydi+PHDZmal2RAOfrkH4m0UURa7SlfpUlIg+EaqbNGp85hCYXLwRcEET1OnYr3rH1oYkcYJ40any1tvQ==', }; - const res = decryptConfig(config); + const res = await decryptConfig(config, repository); expect(res.encrypted).not.toBeDefined(); expect(res.npmToken).not.toBeDefined(); expect(res.npmrc).toEqual( '//registry.npmjs.org/:_authToken=abcdef-ghijklm-nopqf-stuvwxyz\n//registry.npmjs.org/:_authToken=abcdef-ghijklm-nopqf-stuvwxyz\n' ); }); - it('appends npm token in npmrc', () => { + it('appends npm token in npmrc', async () => { setGlobalConfig({ privateKey }); config.npmrc = 'foo=bar\n'; // eslint-disable-line no-template-curly-in-string config.encrypted = { npmToken: 'FLA9YHIzpE7YetAg/P0X46npGRCMqn7hgyzwX5ZQ9wYgu9BRRbTiBVsUIFTyM5BuP1Q22slT2GkWvFvum7GU236Y6QiT7Nr8SLvtsJn2XUuq8H7REFKzdy3+wqyyWbCErYTFyY1dcPM7Ht+CaGDWdd8u/FsoX7AdMRs/X1jNUo6iSmlUiyGlYDKF+QMnCJom1VPVgZXWsGKdjI2MLny991QMaiv0VajmFIh4ENv4CtXOl/1twvIl/6XTXAaqpJJKDTPZEuydi+PHDZmal2RAOfrkH4m0UURa7SlfpUlIg+EaqbNGp85hCYXLwRcEET1OnYr3rH1oYkcYJ40any1tvQ==', }; - const res = decryptConfig(config); + const res = await decryptConfig(config, repository); expect(res.encrypted).not.toBeDefined(); expect(res.npmToken).not.toBeDefined(); expect(res.npmrc).toMatchSnapshot(); }); - it('decrypts nested', () => { + it('decrypts nested', async () => { setGlobalConfig({ privateKey }); config.packageFiles = [ { @@ -77,7 +81,7 @@ describe('config/decrypt', () => { }, 'backend/package.json', ]; - const res = decryptConfig(config); + const res = await decryptConfig(config, repository); expect(res.encrypted).not.toBeDefined(); expect(res.packageFiles[0].devDependencies.encrypted).not.toBeDefined(); expect(res.packageFiles[0].devDependencies.branchPrefix).toEqual( @@ -88,5 +92,71 @@ describe('config/decrypt', () => { '//registry.npmjs.org/:_authToken=abcdef-ghijklm-nopqf-stuvwxyz\n' ); }); + it('rejects invalid PGP message', async () => { + setGlobalConfig({ privateKey: privateKeyPgp }); + config.encrypted = { + token: + 'long-but-wrong-wcFMAw+4H7SgaqGOAQ//ZNPgHJ4RQBdfFoDX8Ywe9UxqMlc8k6VasCszQ2JULh/BpEdKdgRUGNaKaeZ+oBKYDBmDwAD5V5FEMlsg+KO2gykp/p2BAwvKGtYK0MtxLh4h9yJbN7TrVnGO3/cC+Inp8exQt0gD6f1Qo/9yQ9NE4/BIbaSs2b2DgeIK7Ed8N675AuSo73UOa6o7t+9pKeAAK5TQwgSvolihbUs8zjnScrLZD+nhvL3y5gpAqK9y//a+bTu6xPA1jdLjsswoCUq/lfVeVsB2GWV2h6eex/0fRKgN7xxNgdMn0a7msrvumhTawP8mPisPY2AAsHRIgQ9vdU5HbOPdGoIwI9n9rMdIRn9Dy7/gcX9Ic+RP2WwS/KnPHLu/CveY4W5bYqYoikWtJs9HsBCyWFiHIRrJF+FnXwtKdoptRfxTfJIkBoLrV6fDIyKo79iL+xxzgrzWs77KEJUJfexZBEGBCnrV2o7mo3SU197S0qx7HNvqrmeCj8CLxq8opXC71TNa+XE6BQUVyhMFxtW9LNxZUHRiNzrTSikArT4hzjyr3f9cb0kZVcs6XJQsm1EskU3WXo7ETD7nsukS9GfbwMn7tfYidB/yHSHl09ih871BcgByDmEKKdmamcNilW2bmTAqB5JmtaYT5/H8jRQWo/VGrEqlmiA4KmwSv7SZPlDnaDFrmzmMZZDSRgHe5KWl283XLmSeE8J0NPqwFH3PeOv4fIbOjJrnbnFBwSAsgsMe2K4OyFDh2COfrho7s8EP1Kl5lBkYJ+VRreGRerdSu24', + }; + await expect(decryptConfig(config, repository)).rejects.toThrow( + 'config-validation' + ); + config.encrypted = { + // Missing value + token: + 'wcFMAw+4H7SgaqGOAQ//ZNPgHJ4RQBdfFoDX8Ywe9UxqMlc8k6VasCszQ2JULh/BpEdKdgRUGNaKaeZ+oBKYDBmDwAD5V5FEMlsg+KO2gykp/p2BAwvKGtYK0MtxLh4h9yJbN7TrVnGO3/cC+Inp8exQt0gD6f1Qo/9yQ9NE4/BIbaSs2b2DgeIK7Ed8N675AuSo73UOa6o7t+9pKeAAK5TQwgSvolihbUs8zjnScrLZD+nhvL3y5gpAqK9y//a+bTu6xPA1jdLjsswoCUq/lfVeVsB2GWV2h6eex/0fRKgN7xxNgdMn0a7msrvumhTawP8mPisPY2AAsHRIgQ9vdU5HbOPdGoIwI9n9rMdIRn9Dy7/gcX9Ic+RP2WwS/KnPHLu/CveY4W5bYqYoikWtJs9HsBCyWFiHIRrJF+FnXwtKdoptRfxTfJIkBoLrV6fDIyKo79iL+xxzgrzWs77KEJUJfexZBEGBCnrV2o7mo3SU197S0qx7HNvqrmeCj8CLxq8opXC71TNa+XE6BQUVyhMFxtW9LNxZUHRiNzrTSikArT4hzjyr3f9cb0kZVcs6XJQsm1EskU3WXo7ETD7nsukS9GfbwMn7tfYidB/yHSHl09ih871BcgByDmEKKdmamcNilW2bmTAqB5JmtaYT5/H8jRQWo/VGrEqlmiA4KmwSv7SZPlDnaDFrmzmMZZDSRgHe5KWl283XLmSeE8J0NPqwFH3PeOv4fIbOjJrnbnFBwSAsgsMe2K4OyFDh2COfrho7s8EP1Kl5lBkYJ+VRreGRerdSu24', + }; + await expect(decryptConfig(config, repository)).rejects.toThrow( + 'config-validation' + ); + config.encrypted = { + // Missing org scope + token: + 'wcFMAw+4H7SgaqGOAQ//W38A3PmaZnE9XTCHGDQFD52Kz78UYnaiYeAT13cEqYWTwEvQ57B7D7I6i4jCLe7KwkUCS90kyoqd7twD75W/sO70MyIveKnMlqqnpkagQkFgmzMaXXNHaJXEkjzsflTELZu6UsUs/kZYmab7r14YLl9HbH/pqN9exil/9s3ym9URCPOyw/l04KWntdMAy0D+c5M4mE+obv6fz6nDb8tkdeT5Rt2uU+qw3gH1OsB2yu+zTWpI/xTGwDt5nB5txnNTsVrQ/ZK85MSktacGVcYuU9hsEDmSrShmtqlg6Myq+Hjb7cYAp2g4n13C/I3gGGaczl0PZaHD7ALMjI7p6O1q+Ix7vMxipiKMVjS3omJoqBCz3FKc6DVhyX4tfhxgLxFo0DpixNwGbBRbMBO8qZfUk7bicAl/oCRc2Ijmay5DDYuvtkw3G3Ou+sZTe6DNpWUFy6VA4ai7hhcLvcAuiYmLdwPISRR/X4ePa8ZrmSVPyVOvbmmwLhcDYSDlC9Mw4++7ELomlve5kvjVSHvPv9BPVb5sJF7gX4vOT4FrcKalQRPmhNCZrE8tY2lvlrXwV2EEhya8EYv4QTd3JUYEYW5FXiJrORK5KDTnISw+U02nFZjFlnoz9+R6h+aIT1crS3/+YjCHE/EIKvSftOnieYb02Gk7M9nqU19EYL9ApYw4+IjSRgFM3DShIrvuDwDkAwUfaq8mKtr9Vjg/r+yox//GKS3u3r4I3+dfCljA3OwskTPfbSD+huBk4mylIvaL5v8Fngxo979wiLw', + }; + await expect(decryptConfig(config, repository)).rejects.toThrow( + 'config-validation' + ); + config.encrypted = { + // Impossible to parse + token: + 'wcFMAw+4H7SgaqGOAQ//Wa/gHgQdH7tj3LQdW6rWKjzmkYVKZW9EbexJExu4WLaMgEKodlRMilcqCKfQZpjzoiC31J8Ly/x6Soury+lQnLVbtIQ4KWa/uCIz4lXCpPpGNgN2jPfOmdwWBMOcXIT+BgAMxRu3rAmvTtunrkACJ3J92eYNwJhTzp2Azn9LpT7kHnZ64z2SPhbdUgMMhCBwBG5BPArPzF5fdaqa8uUSbKhY0GMiqPXq6Zeq+EBNoPc/RJp2urpYTknO+nRb39avKjihd9MCZ/1d3QYymbRj7SZC3LJhenVF0hil3Uk8TBASnGQiDmBcIXQFhJ0cxavXqKjx+AEALq+kTdwGu5vuE2+2B820/o3lAXR9OnJHr8GodJ2ZBpzOaPrQe5zvxL0gLEeUUPatSOwuLhdo/6+bRCl2wNz23jIjDEFFTmsLqfEHcdVYVTH2QqvLjnUYcCRRuM32vS4rCMOEe0l6p0CV2rk22UZDIPcxqXjKucxse2Sow8ATWiPoIw7zWj7XBLqUKHFnMpPV2dCIKFKBsOKYgLjF4BvKzZJyhmVEPgMcKQLYqeT/2uWDR77NSWH0Cyiwk9M3KbOIMmV3pWh9PiXk6CvumECELbJHYH0Mc+P//BnbDq2Ie9dHdmKhFgRyHU7gWvkPhic9BX36xyldPcnhTgr1XWRoVe0ETGLDPCcqrQ/SUQGrLiujSOgxGu2K/6LDJhi4IKz1/nf7FUSj5eTIDqQiSPP5pXDjlH7oYxXXrHI/aYOCZ5sBx7mOzlEcENIrYblCHO/CYMTWdCJ4Wrftqk7K/A=', + }; + await expect(decryptConfig(config, repository)).rejects.toThrow( + 'config-validation' + ); + config.encrypted = { + token: 'too-short', + }; + await expect(decryptConfig(config, repository)).rejects.toThrow( + 'config-validation' + ); + }); + it('handles PGP org constraint', async () => { + setGlobalConfig({ privateKey: privateKeyPgp }); + config.encrypted = { + token: + 'wcFMAw+4H7SgaqGOAQ/+Lz6RlbEymbnmMhrktuaGiDPWRNPEQFuMRwwYM6/B/r0JMZa9tskAA5RpyYKxGmJJeuRtlA8GkTw02GoZomlJf/KXJZ95FwSbkXMSRJRD8LJ2402Hw2TaOTaSvfamESnm8zhNo8cok627nkKQkyrpk64heVlU5LIbO2+UgYgbiSQjuXZiW+QuJ1hVRjx011FQgEYc59+22yuKYqd8rrni7TrVqhGRlHCAqvNAGjBI4H7uTFh0sP4auunT/JjxTeTkJoNu8KgS/LdrvISpO67TkQziZo9XD5FOzSN7N3e4f8vO4N4fpjgkIDH/9wyEYe0zYz34xMAFlnhZzqrHycRqzBJuMxGqlFQcKWp9IisLMoVJhLrnvbDLuwwcjeqYkhvODjSs7UDKwTE4X4WmvZr0x4kOclOeAAz/pM6oNVnjgWJd9SnYtoa67bZVkne0k6mYjVhosie8v8icijmJ4OyLZUGWnjZCRd/TPkzQUw+B0yvsop9FYGidhCI+4MVx6W5w7SRtCctxVfCjLpmU4kWaBUUJ5YIQ5xm55yxEYuAsQkxOAYDCMFlV8ntWStYwIG1FsBgJX6VPevXuPPMjWiPNedIpJwBH2PLB4blxMfzDYuCeaIqU4daDaEWxxpuFTTK9fLdJKuipwFG6rwE3OuijeSN+2SLszi834DXtUjQdikHSTQG392+oTmZCFPeffLk/OiV2VpdXF3gGL7sr5M9hOWIZ783q0vW1l6nAElZ7UA//kW+L6QRxbnBVTJK5eCmMY6RJmL76zjqC1jQ0FC10', + }; + const res = await decryptConfig(config, repository); + expect(res.encrypted).not.toBeDefined(); + expect(res.token).toEqual('123'); + await expect(decryptConfig(config, 'wrong/org')).rejects.toThrow( + 'config-validation' + ); + }); + it('handles PGP org/repo constraint', async () => { + setGlobalConfig({ privateKey: privateKeyPgp }); + config.encrypted = { + token: + 'wcFMAw+4H7SgaqGOAQ//Wp7N0PaDZp0uOdwsc1CuqAq0UPcq+IQdHyKpJs3tHiCecXBHogy4P+rY9nGaUrVneCr4HexuKGuyJf1yl0ZqFffAUac5PjF8eDvjukQGOUq4aBlOogJCEefnuuVxVJx+NRR5iF1P6v57bmI1c+zoqZI/EQB30KU6O1BsdGPLUA/+R3dwCZd5Mbd36s34eYBasqcY9/QbqFcpElXMEPMse3kMCsVXPbZ+UMjtPJiBPUmtJq+ifnu1LzDrfshusSQMwgd/QNk7nEsijiYKllkWhHTP6g7zigvJ46x0h6AYS108YiuK3B9XUhXN9m05Ac6KTEEUdRI3E/dK2dQuRkLjXC8wceQm4A19Gm0uHoMIJYOCbiVoBCH6ayvKbZWZV5lZ4D1JbDNGmKeIj6OX9XWEMKiwTx0Xe89V7BdJzwIGrL0TCLtXuYWZ/R2k+UuBqtgzr44BsBqMpKUA0pcGBoqsEou1M05Ae9fJMF6ADezF5UQZPxT1hrMldiTp3p9iHGfWN2tKHeoW/8CqlIqg9JEkTc+Pl/L9E6ndy5Zjf097PvcmSGhxUQBE7XlrZoIlGhiEU/1HPMen0UUIs0LUu1ywpjCex2yTWnU2YmEwy0MQI1sekSr96QFxDDz9JcynYOYbqR/X9pdxEWyzQ+NJ3n6K97nE1Dj9Sgwu7mFGiUdNkf/SUAF0eZi/eXg71qumpMGBd4eWPtgkeMPLHjvMSYw9vBUfcoKFz6RJ4woG0dw5HOFkPnIjXKWllnl/o01EoBp/o8uswsIS9Nb8i+bp27U6tAHE', + }; + const res = await decryptConfig(config, repository); + expect(res.encrypted).not.toBeDefined(); + expect(res.token).toEqual('123'); + await expect(decryptConfig(config, 'abc/defg')).rejects.toThrow( + 'config-validation' + ); + }); }); }); diff --git a/lib/config/decrypt.ts b/lib/config/decrypt.ts index afabca0c6824a8..d5e8f5e7093d67 100644 --- a/lib/config/decrypt.ts +++ b/lib/config/decrypt.ts @@ -1,11 +1,49 @@ import crypto from 'crypto'; import is from '@sindresorhus/is'; +import * as openpgp from 'openpgp'; import { logger } from '../logger'; import { maskToken } from '../util/mask'; import { add } from '../util/sanitize'; import { getGlobalConfig } from './global'; import type { RenovateConfig } from './types'; +export async function tryDecryptPgp( + privateKey: string, + encryptedStr: string +): Promise { + if (encryptedStr.length < 500) { + // optimization during transition of public key -> pgp + return null; + } + try { + const pk = await openpgp.readPrivateKey({ + // prettier-ignore + armoredKey: privateKey.replace(/\n[ \t]+/g, '\n'), // little massage to help a common problem + }); + const startBlock = '-----BEGIN PGP MESSAGE-----\n\n'; + const endBlock = '\n-----END PGP MESSAGE-----'; + let armoredMessage = encryptedStr.trim(); + if (!armoredMessage.startsWith(startBlock)) { + armoredMessage = `${startBlock}${armoredMessage}`; + } + if (!armoredMessage.endsWith(endBlock)) { + armoredMessage = `${armoredMessage}${endBlock}`; + } + const message = await openpgp.readMessage({ + armoredMessage, + }); + const { data } = await openpgp.decrypt({ + message, + decryptionKeys: pk, + }); + logger.debug('Decrypted config using openpgp'); + return data; + } catch (err) { + logger.debug({ err }, 'Could not decrypt using openpgp'); + return null; + } +} + export function tryDecryptPublicKeyDefault( privateKey: string, encryptedStr: string @@ -17,7 +55,7 @@ export function tryDecryptPublicKeyDefault( .toString(); logger.debug('Decrypted config using default padding'); } catch (err) { - logger.debug('Failed to decrypt using default padding'); + logger.debug('Could not decrypt using default padding'); } return decryptedStr; } @@ -38,23 +76,70 @@ export function tryDecryptPublicKeyPKCS1( ) .toString(); } catch (err) { - logger.debug('Failed to decrypt using PKCS1 padding'); + logger.debug('Could not decrypt using PKCS1 padding'); } return decryptedStr; } -export function tryDecrypt( +export async function tryDecrypt( privateKey: string, - encryptedStr: string -): string | null { - let decryptedStr = tryDecryptPublicKeyDefault(privateKey, encryptedStr); - if (!is.string(decryptedStr)) { - decryptedStr = tryDecryptPublicKeyPKCS1(privateKey, encryptedStr); + encryptedStr: string, + repository: string +): Promise { + let decryptedStr: string = null; + if (privateKey?.startsWith('-----BEGIN PGP PRIVATE KEY BLOCK-----')) { + const decryptedObjStr = await tryDecryptPgp(privateKey, encryptedStr); + if (decryptedObjStr) { + try { + const decryptedObj = JSON.parse(decryptedObjStr); + const { o: org, r: repo, v: value } = decryptedObj; + if (is.nonEmptyString(value)) { + if (is.nonEmptyString(org)) { + const orgName = org.replace(/\/$/, ''); // Strip trailing slash + if (is.nonEmptyString(repo)) { + const scopedRepository = `${orgName}/${repo}`; + if (scopedRepository === repository) { + decryptedStr = value; + } else { + logger.warn( + { scopedRepository }, + 'Secret is scoped to a different repository' + ); + } + } else { + const scopedOrg = `${orgName}/`; + if (repository.startsWith(scopedOrg)) { + decryptedStr = value; + } else { + logger.warn( + { scopedOrg }, + 'Secret is scoped to a different org' + ); + } + } + } else { + logger.warn('Missing scope from decrypted object'); + } + } else { + logger.warn('Decrypted object is missing a value'); + } + } catch (err) { + logger.warn({ err }, 'Could not parse decrypted string'); + } + } + } else { + decryptedStr = tryDecryptPublicKeyDefault(privateKey, encryptedStr); + if (!is.string(decryptedStr)) { + decryptedStr = tryDecryptPublicKeyPKCS1(privateKey, encryptedStr); + } } return decryptedStr; } -export function decryptConfig(config: RenovateConfig): RenovateConfig { +export async function decryptConfig( + config: RenovateConfig, + repository: string +): Promise { logger.trace({ config }, 'decryptConfig()'); const decryptedConfig = { ...config }; const { privateKey, privateKeyOld } = getGlobalConfig(); @@ -64,10 +149,10 @@ export function decryptConfig(config: RenovateConfig): RenovateConfig { if (privateKey) { for (const [eKey, eVal] of Object.entries(val)) { logger.debug('Trying to decrypt ' + eKey); - let decryptedStr = tryDecrypt(privateKey, eVal); + let decryptedStr = await tryDecrypt(privateKey, eVal, repository); if (privateKeyOld && !is.nonEmptyString(decryptedStr)) { logger.debug(`Trying to decrypt with old private key`); - decryptedStr = tryDecrypt(privateKeyOld, eVal); + decryptedStr = await tryDecrypt(privateKeyOld, eVal, repository); } if (!is.nonEmptyString(decryptedStr)) { const error = new Error('config-validation'); @@ -113,17 +198,20 @@ export function decryptConfig(config: RenovateConfig): RenovateConfig { delete decryptedConfig.encrypted; } else if (is.array(val)) { decryptedConfig[key] = []; - val.forEach((item) => { + for (const item of val) { if (is.object(item) && !is.array(item)) { (decryptedConfig[key] as RenovateConfig[]).push( - decryptConfig(item as RenovateConfig) + await decryptConfig(item as RenovateConfig, repository) ); } else { (decryptedConfig[key] as unknown[]).push(item); } - }); + } } else if (is.object(val) && key !== 'content') { - decryptedConfig[key] = decryptConfig(val as RenovateConfig); + decryptedConfig[key] = await decryptConfig( + val as RenovateConfig, + repository + ); } } delete decryptedConfig.encrypted; diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index 6cc5c664440d05..622c0439077de4 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -467,6 +467,13 @@ const options: RenovateOptions[] = [ type: 'string', globalOnly: true, }, + { + name: 'privateKeyPathOld', + description: 'Path to the Server-side old private key.', + stage: 'repository', + type: 'string', + globalOnly: true, + }, { name: 'encrypted', description: diff --git a/lib/config/types.ts b/lib/config/types.ts index 322eea60ad1207..6094917fa250c2 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -80,6 +80,7 @@ export interface GlobalOnlyConfig { logFileLevel?: LogLevel; prCommitsPerRunLimit?: number; privateKeyPath?: string; + privateKeyPathOld?: string; redisUrl?: string; repositories?: RenovateRepository[]; } diff --git a/lib/workers/global/config/parse/index.spec.ts b/lib/workers/global/config/parse/index.spec.ts index 066713775ba05c..5a75c6724e70e3 100644 --- a/lib/workers/global/config/parse/index.spec.ts +++ b/lib/workers/global/config/parse/index.spec.ts @@ -81,9 +81,14 @@ describe('workers/global/config/parse/index', () => { }); it('reads private key from file', async () => { const privateKeyPath = upath.join(__dirname, '__fixtures__/private.pem'); + const privateKeyPathOld = upath.join( + __dirname, + '__fixtures__/private.pem' + ); const env: NodeJS.ProcessEnv = { ...defaultEnv, RENOVATE_PRIVATE_KEY_PATH: privateKeyPath, + RENOVATE_PRIVATE_KEY_PATH_OLD: privateKeyPathOld, }; const expected = await readFile(privateKeyPath, 'utf8'); const parsedConfig = await configParser.parseConfigs(env, defaultArgv); diff --git a/lib/workers/global/config/parse/index.ts b/lib/workers/global/config/parse/index.ts index 7fef39bc09cfe8..7f9bcc757b99eb 100644 --- a/lib/workers/global/config/parse/index.ts +++ b/lib/workers/global/config/parse/index.ts @@ -43,6 +43,11 @@ export async function parseConfigs( delete config.privateKeyPath; } + if (!config.privateKeyOld && config.privateKeyPathOld) { + config.privateKey = await readFile(config.privateKeyPathOld, 'utf8'); + delete config.privateKeyPathOld; + } + if (config.logContext) { // This only has an effect if logContext was defined via file or CLI, otherwise it would already have been detected in env setContext(config.logContext); diff --git a/lib/workers/repository/init/merge.ts b/lib/workers/repository/init/merge.ts index 31d2bbffb05453..9d8d0d24300687 100644 --- a/lib/workers/repository/init/merge.ts +++ b/lib/workers/repository/init/merge.ts @@ -195,15 +195,19 @@ export async function mergeRenovateConfig( delete migratedConfig.warnings; logger.debug({ config: migratedConfig }, 'migrated config'); // Decrypt before resolving in case we need npm authentication for any presets - const decryptedConfig = decryptConfig(migratedConfig); + const decryptedConfig = await decryptConfig( + migratedConfig, + config.repository + ); // istanbul ignore if if (is.string(decryptedConfig.npmrc)) { logger.debug('Found npmrc in decrypted config - setting'); npmApi.setNpmrc(decryptedConfig.npmrc); } // Decrypt after resolving in case the preset contains npm authentication instead - let resolvedConfig = decryptConfig( - await presets.resolveConfigPresets(decryptedConfig, config) + let resolvedConfig = await decryptConfig( + await presets.resolveConfigPresets(decryptedConfig, config), + config.repository ); logger.trace({ config: resolvedConfig }, 'resolved config'); const migrationResult = migrateConfig(resolvedConfig); diff --git a/package.json b/package.json index 0be8040acc3b8c..08b48446d35d07 100644 --- a/package.json +++ b/package.json @@ -171,6 +171,7 @@ "minimatch": "3.0.4", "moo": "0.5.1", "node-html-parser": "3.3.6", + "openpgp": "5.0.0", "p-all": "3.0.0", "p-map": "4.0.0", "p-queue": "6.6.2", diff --git a/yarn.lock b/yarn.lock index d6bfd194adc379..055450bd45f370 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1543,7 +1543,6 @@ "@renovate/eslint-plugin@https://github.com/renovatebot/eslint-plugin#v0.0.3": version "0.0.1" - uid c88253170ce9e9248bc0653197ed2ff1ecf41ac1 resolved "https://github.com/renovatebot/eslint-plugin#c88253170ce9e9248bc0653197ed2ff1ecf41ac1" "@renovate/pep440@1.0.0": @@ -2629,6 +2628,16 @@ asap@^2.0.0, asap@~2.0.3: resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= +asn1.js@^5.0.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" + integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + safer-buffer "^2.1.0" + asn1@~0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" @@ -2816,6 +2825,11 @@ bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" +bn.js@^4.0.0: + version "4.12.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== + boolbase@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -6858,6 +6872,11 @@ min-indent@^1.0.0: resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== +minimalistic-assert@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + "minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -7320,75 +7339,75 @@ npm@^7.0.0: resolved "https://registry.yarnpkg.com/npm/-/npm-7.23.0.tgz#aeafaafe847fdd7c496d8e4d4bcbb5201aa1930c" integrity sha512-m7WFTwGfiBX+jL4ObX7rIDkug/hG/Jn8vZUjKw4WS8CqMjVydHiWTARLDIll7LtHu5i7ZHBnqXZbL2S73U5p6A== dependencies: - "@npmcli/arborist" "*" - "@npmcli/ci-detect" "*" - "@npmcli/config" "*" - "@npmcli/map-workspaces" "*" - "@npmcli/package-json" "*" - "@npmcli/run-script" "*" - abbrev "*" - ansicolors "*" - ansistyles "*" - archy "*" - cacache "*" - chalk "*" - chownr "*" - cli-columns "*" - cli-table3 "*" - columnify "*" - fastest-levenshtein "*" - glob "*" - graceful-fs "*" - hosted-git-info "*" - ini "*" - init-package-json "*" - is-cidr "*" - json-parse-even-better-errors "*" - libnpmaccess "*" - libnpmdiff "*" - libnpmexec "*" - libnpmfund "*" - libnpmhook "*" - libnpmorg "*" - libnpmpack "*" - libnpmpublish "*" - libnpmsearch "*" - libnpmteam "*" - libnpmversion "*" - make-fetch-happen "*" - minipass "*" - minipass-pipeline "*" - mkdirp "*" - mkdirp-infer-owner "*" - ms "*" - node-gyp "*" - nopt "*" - npm-audit-report "*" - npm-install-checks "*" - npm-package-arg "*" - npm-pick-manifest "*" - npm-profile "*" - npm-registry-fetch "*" - npm-user-validate "*" - npmlog "*" - opener "*" - pacote "*" - parse-conflict-json "*" - qrcode-terminal "*" - read "*" - read-package-json "*" - read-package-json-fast "*" - readdir-scoped-modules "*" - rimraf "*" - semver "*" - ssri "*" - tar "*" - text-table "*" - tiny-relative-date "*" - treeverse "*" - validate-npm-package-name "*" - which "*" - write-file-atomic "*" + "@npmcli/arborist" "^2.8.3" + "@npmcli/ci-detect" "^1.2.0" + "@npmcli/config" "^2.3.0" + "@npmcli/map-workspaces" "^1.0.4" + "@npmcli/package-json" "^1.0.1" + "@npmcli/run-script" "^1.8.6" + abbrev "~1.1.1" + ansicolors "~0.3.2" + ansistyles "~0.1.3" + archy "~1.0.0" + cacache "^15.3.0" + chalk "^4.1.2" + chownr "^2.0.0" + cli-columns "^3.1.2" + cli-table3 "^0.6.0" + columnify "~1.5.4" + fastest-levenshtein "^1.0.12" + glob "^7.1.7" + graceful-fs "^4.2.8" + hosted-git-info "^4.0.2" + ini "^2.0.0" + init-package-json "^2.0.4" + is-cidr "^4.0.2" + json-parse-even-better-errors "^2.3.1" + libnpmaccess "^4.0.2" + libnpmdiff "^2.0.4" + libnpmexec "^2.0.1" + libnpmfund "^1.1.0" + libnpmhook "^6.0.2" + libnpmorg "^2.0.2" + libnpmpack "^2.0.1" + libnpmpublish "^4.0.1" + libnpmsearch "^3.1.1" + libnpmteam "^2.0.3" + libnpmversion "^1.2.1" + make-fetch-happen "^9.1.0" + minipass "^3.1.3" + minipass-pipeline "^1.2.4" + mkdirp "^1.0.4" + mkdirp-infer-owner "^2.0.0" + ms "^2.1.2" + node-gyp "^7.1.2" + nopt "^5.0.0" + npm-audit-report "^2.1.5" + npm-install-checks "^4.0.0" + npm-package-arg "^8.1.5" + npm-pick-manifest "^6.1.1" + npm-profile "^5.0.3" + npm-registry-fetch "^11.0.0" + npm-user-validate "^1.0.1" + npmlog "^5.0.1" + opener "^1.5.2" + pacote "^11.3.5" + parse-conflict-json "^1.1.1" + qrcode-terminal "^0.12.0" + read "~1.0.7" + read-package-json "^4.1.1" + read-package-json-fast "^2.0.3" + readdir-scoped-modules "^1.1.0" + rimraf "^3.0.2" + semver "^7.3.5" + ssri "^8.0.1" + tar "^6.1.11" + text-table "~0.2.0" + tiny-relative-date "^1.3.0" + treeverse "^1.0.4" + validate-npm-package-name "~3.0.0" + which "^2.0.2" + write-file-atomic "^3.0.3" npmlog@*: version "5.0.1" @@ -7524,6 +7543,13 @@ opener@*: resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== +openpgp@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/openpgp/-/openpgp-5.0.0.tgz#2da0ee406c8834223ae928a9a214f4811f83f923" + integrity sha512-H4Jsj9Bp1KFQ/w520M1d2x45iz9V39Lf+IwIXmUaBmJAMagAt0zanqmWeFzIMJUYmrHTcm6fO/rpc6aftFUHbA== + dependencies: + asn1.js "^5.0.0" + optionator@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"