Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(iam): oidc provider retrieves leaf certificate instead of root certificate #22509

Merged
merged 13 commits into from
Nov 4, 2022
8 changes: 0 additions & 8 deletions packages/@aws-cdk/aws-eks/lib/oidc-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,10 @@ export class OpenIdConnectProvider extends iam.OpenIdConnectProvider {
* @param props Initialization properties
*/
public constructor(scope: Construct, id: string, props: OpenIdConnectProviderProps) {
/**
* For some reason EKS isn't validating the root certificate but a intermediate certificate
* which is one level up in the tree. Because of the a constant thumbprint value has to be
* stated with this OpenID Connect provider. The certificate thumbprint is the same for all the regions.
*/
const thumbprints = ['9e99a48a9960b14926bb7f3b02e22da2b0ab7280'];

const clientIds = ['sts.amazonaws.com'];

super(scope, id, {
url: props.url,
thumbprints,
clientIds,
});
}
Expand Down
3 changes: 0 additions & 3 deletions packages/@aws-cdk/aws-eks/test/cluster.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2156,9 +2156,6 @@ describe('cluster', () => {
ClientIDList: [
'sts.amazonaws.com',
],
ThumbprintList: [
'9e99a48a9960b14926bb7f3b02e22da2b0ab7280',
],
Url: {
'Fn::GetAtt': [
'Cluster9EE0221C',
Expand Down
67 changes: 61 additions & 6 deletions packages/@aws-cdk/aws-iam/lib/oidc-provider/external.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* istanbul ignore file */

import { DetailedPeerCertificate } from 'node:tls';
import * as util from 'node:util';
import * as tls from 'tls';
import * as url from 'url';
// eslint-disable-next-line import/no-extraneous-dependencies
Expand All @@ -20,26 +21,80 @@ function defaultLogger(fmt: string, ...args: any[]) {
/**
* Downloads the CA thumbprint from the issuer URL
*/
async function downloadThumbprint(issuerUrl: string) {
external.log(`downloading certificate authority thumbprint for ${issuerUrl}`);
export async function downloadThumbprint(issuerUrl: string) {
external.log(`Downloading certificate authority thumbprint for ${issuerUrl}`);

return new Promise<string>((ok, ko) => {
const purl = url.parse(issuerUrl);
const port = purl.port ? parseInt(purl.port, 10) : 443;

if (!purl.host) {
return ko(new Error(`unable to determine host from issuer url ${issuerUrl}`));
}

const socket = tls.connect(port, purl.host, { rejectUnauthorized: false, servername: purl.host });
socket.once('error', ko);

socket.once('secureConnect', () => {
const cert = socket.getPeerCertificate();
// This set to `true` will return the entire chain of certificates as a nested object
let cert = socket.getPeerCertificate(true);

const unqiueCerts = new Set<DetailedPeerCertificate>();
do {
unqiueCerts.add(cert);
cert = cert.issuerCertificate;
} while ( cert && typeof cert === 'object' && !unqiueCerts.has(cert));
vinayak-kukreja marked this conversation as resolved.
Show resolved Hide resolved

if (unqiueCerts.size == 0) {
return ko(new Error(`No certificates were returned for the mentioned url: ${issuerUrl}`));
}

// The last `cert` obtained must be the root certificate in the certificate chain
const rootCert = [...unqiueCerts].pop()!;
vinayak-kukreja marked this conversation as resolved.
Show resolved Hide resolved

// Add `ca: true` when node merges the feature. Awaiting resolution: https://github.com/nodejs/node/issues/44905
if (!(util.isDeepStrictEqual(rootCert.issuer, rootCert.subject))) {
return ko(new Error(`Subject and Issuer of certificate received are different.
Received: \'Subject\' is ${JSON.stringify(rootCert.subject, null, 4)} and \'Issuer\':${JSON.stringify(rootCert.issuer, null, 4)}`));
}

const validTo = new Date(rootCert.valid_to);
const certificateValidity = getCertificateValidity(validTo);

if (certificateValidity < 0) {
return ko(new Error(`The certificate has already expired on: ${validTo.toUTCString()}`));
}

// Warning user if certificate validity is expiring within 6 months
if (certificateValidity < 180) {
/* eslint-disable-next-line no-console */
console.warn(`The root certificate obtained would expire in ${certificateValidity} days!`);
vinayak-kukreja marked this conversation as resolved.
Show resolved Hide resolved
}

socket.end();
const thumbprint = cert.fingerprint.split(':').join('');
external.log(`certificate authority thumbprint for ${issuerUrl} is ${thumbprint}`);

const thumbprint = rootCert.fingerprint.split(':').join('');
external.log(`Certificate Authority thumbprint for ${issuerUrl} is ${thumbprint}`);

ok(thumbprint);
});
});
}

/**
* To get the validity timeline for the certificate
* @param certDate The valid to date for the certificate
* @returns The number of days the certificate is valid wrt current date
*/
function getCertificateValidity(certDate: Date): Number {
const millisecondsInDay = 24 * 60 * 60 * 1000;
const currentDate = new Date();

const validity = Math.round((certDate.getTime() - currentDate.getTime()) / millisecondsInDay);
TheRealAmazonKendra marked this conversation as resolved.
Show resolved Hide resolved

return validity;
}

// allows unit test to replace with mocks
/* eslint-disable max-len */
export const external = {
Expand Down
172 changes: 172 additions & 0 deletions packages/@aws-cdk/aws-iam/test/oidc-provider/external.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { TLSSocket, DetailedPeerCertificate, Certificate } from 'tls';
import { downloadThumbprint } from '../../lib/oidc-provider/external';

const chainLength = 3;
let certificateCount = 0;
let placeholderCertificate: DetailedPeerCertificate;
let peerCertificate: DetailedPeerCertificate;

describe('downloadThumbprint', () => {

const peerCertificateMock = jest.spyOn(TLSSocket.prototype, 'getPeerCertificate').mockImplementation(()=> {
return peerCertificate;
});

beforeEach(() => {
certificateCount = 0;
peerCertificate = createChainedCertificateObject();

// This is to create a circular reference in the root certificate
getRootCertificateFromChain().issuerCertificate = peerCertificate;

// To have silent test runs for this test
jest.spyOn(console, 'warn').mockImplementation(() => {});
jest.spyOn(console, 'log').mockImplementation(() => {});
});

test('is able to get root certificate from certificate chain', async () => {
// WHEN
await downloadThumbprint('https://example.com');

// THEN
expect(peerCertificateMock).toHaveBeenCalledTimes(2);
});

test('throws when subject and issuer are different of expected root certificate', async () => {
// GIVEN
const subject: Certificate = {
C: 'another-country-code-root',
ST: 'another-street-root',
L: 'another-locality-root',
O: 'another-organization-root',
OU: 'another-organizational-unit-root',
CN: 'another-common-name-root',
};

getRootCertificateFromChain().subject = subject;

// THEN
await expect(() => downloadThumbprint('https://example.com')).rejects.toThrowError(/Subject and Issuer of certificate received are different/);

expect(peerCertificateMock).toHaveBeenCalledTimes(2);
});

test('throws error when certificate receieved is expired', async () => {
// GIVEN
const currentDate = new Date();
const expiredValidityDate = subtractDaysFromDate(currentDate, 5);

getRootCertificateFromChain().valid_to = expiredValidityDate.toUTCString();

// THEN
await expect(() => downloadThumbprint('https://example.com')).rejects.toThrowError(/The certificate has already expired on/);

expect(peerCertificateMock).toHaveBeenCalledTimes(2);
});

afterEach(() => {
peerCertificateMock.mockClear();
});
});

function createChainedCertificateObject(): DetailedPeerCertificate {
return createCertificateObject();
}

function createCertificateObject(): DetailedPeerCertificate {
const currentDate = new Date();

if (certificateCount == chainLength ) {
// Root Certificate with circular reference to first certificate
return {
subject: {
C: 'country-code-root',
ST: 'street-root',
L: 'locality-root',
O: 'organization-root',
OU: 'organizational-unit-root',
CN: 'common-name-root',
},
issuer: {
C: 'country-code-root',
ST: 'street-root',
L: 'locality-root',
O: 'organization-root',
OU: 'organizational-unit-root',
CN: 'common-name-root',
},
subjectaltname: 'subjectal-name-root',
infoAccess: {
key: ['value-root'],
},
modulus: 'modulus-root',
exponent: 'exponent-root',
valid_from: currentDate.toUTCString(),
valid_to: addDaysToDate(currentDate, 200).toUTCString(),
fingerprint: '01:02:59:D9:C3:D2:0D:08:F7:82:4E:44:A4:B4:53:C5:E2:3A:87:00',
fingerprint256: '69:AE:1A:6A:D4:3D:C6:C1:1B:EA:C6:23:DE:BA:2A:14:62:62:93:5C:7A:EA:06:41:9B:0B:BC:87:CE:48:4E:00',
ext_key_usage: ['key-usage-root'],
serialNumber: 'serial-number-root',
raw: Buffer.alloc(10),
issuerCertificate: placeholderCertificate,
};
}

certificateCount++;

const certificate = {
subject: {
C: `subject-country-code-${certificateCount}`,
ST: `subject-street-${certificateCount}`,
L: `subject-locality-${certificateCount}`,
O: `subject-organization-${certificateCount}`,
OU: `subject-organizational-unit-${certificateCount}`,
CN: `subject-common-name-${certificateCount}`,
},
issuer: {
C: `issuer-country-code-${certificateCount}`,
ST: `issuer-street-${certificateCount}`,
L: `issuer-locality-${certificateCount}`,
O: `issuer-organization-${certificateCount}`,
OU: `issuer-organizational-unit-${certificateCount}`,
CN: `issuer-common-name-${certificateCount}`,
},
subjectaltname: `subjectal-name-${certificateCount}`,
infoAccess: {
key: [`value-${certificateCount}`],
},
modulus: `modulus-${certificateCount}`,
exponent: `exponent-${certificateCount}`,
valid_from: currentDate.toUTCString(),
valid_to: addDaysToDate(currentDate, 200).toUTCString(),
fingerprint: `01:02:59:D9:C3:D2:0D:08:F7:82:4E:44:A4:B4:53:C5:E2:3A:87:${certificateCount}D`,
fingerprint256: `69:AE:1A:6A:D4:3D:C6:C1:1B:EA:C6:23:DE:BA:2A:14:62:62:93:5C:7A:EA:06:41:9B:0B:BC:87:CE:48:4E:0${certificateCount}`,
ext_key_usage: [`key-usage-${certificateCount}`],
serialNumber: `serial-number-${certificateCount}`,
raw: Buffer.alloc(10),
issuerCertificate: createCertificateObject(),
};

return certificate;
}

function addDaysToDate(date: Date, numberOfDays: number): Date {
const newDate = new Date();
return new Date(newDate.setDate(date.getDate() + numberOfDays));
}

function subtractDaysFromDate(date: Date, numberOfDays: number): Date {
const newDate = new Date();
return new Date(newDate.setDate(date.getDate() - numberOfDays));
}

function getRootCertificateFromChain(): DetailedPeerCertificate {
let rootCert: DetailedPeerCertificate = peerCertificate;
let certificateNumber = 0;

while (chainLength > certificateNumber++) {
rootCert = rootCert.issuerCertificate;
}

return rootCert;
}