grpc-node/packages/grpc-js-xds/test/test-xds-credentials.ts

506 lines
22 KiB
TypeScript

/*
* Copyright 2024 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import * as assert from 'assert';
import { createBackends } from './backend';
import { FakeEdsCluster, FakeRouteGroup, FakeServerRoute } from './framework';
import { ControlPlaneServer } from './xds-server';
import { XdsTestClient } from './client';
import { XdsChannelCredentials, XdsServerCredentials } from '../src';
import { credentials, ServerCredentials, experimental, status } from '@grpc/grpc-js';
import { readFileSync } from 'fs';
import * as path from 'path';
import { Listener } from '../src/generated/envoy/config/listener/v3/Listener';
import { DownstreamTlsContext } from '../src/generated/envoy/extensions/transport_sockets/tls/v3/DownstreamTlsContext';
import { AnyExtension } from '@grpc/proto-loader';
import { DOWNSTREAM_TLS_CONTEXT_TYPE_URL } from '../src/resources';
import { UpstreamTlsContext } from '../src/generated/envoy/extensions/transport_sockets/tls/v3/UpstreamTlsContext';
import { StringMatcher } from '../src/generated/envoy/type/matcher/v3/StringMatcher';
import FileWatcherCertificateProvider = experimental.FileWatcherCertificateProvider;
import createCertificateProviderChannelCredentials = experimental.createCertificateProviderChannelCredentials;
const caPath = path.join(__dirname, 'fixtures', 'ca.pem');
const ca = readFileSync(path.join(__dirname, 'fixtures', 'ca.pem'));
const key = readFileSync(path.join(__dirname, 'fixtures', 'server1.key'));
const cert = readFileSync(path.join(__dirname, 'fixtures', 'server1.pem'));
describe('Server xDS Credentials', () => {
let xdsServer: ControlPlaneServer;
let client: XdsTestClient;
beforeEach(done => {
xdsServer = new ControlPlaneServer();
xdsServer.startServer(error => {
done(error);
});
});
afterEach(() => {
client?.close();
xdsServer?.shutdownServer();
});
it('Should use fallback credentials when certificate providers are not configured', async () => {
const [backend] = await createBackends(1, true, new XdsServerCredentials(ServerCredentials.createInsecure()));
const serverRoute = new FakeServerRoute(backend.getPort(), 'serverRoute');
xdsServer.setRdsResource(serverRoute.getRouteConfiguration());
xdsServer.setLdsResource(serverRoute.getListener());
xdsServer.addResponseListener((typeUrl, responseState) => {
if (responseState.state === 'NACKED') {
client?.stopCalls();
assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`);
}
});
const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [backend], locality:{region: 'region1'}}]);
const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]);
await routeGroup.startAllBackends(xdsServer);
xdsServer.setEdsResource(cluster.getEndpointConfig());
xdsServer.setCdsResource(cluster.getClusterConfig());
xdsServer.setRdsResource(routeGroup.getRouteConfiguration());
xdsServer.setLdsResource(routeGroup.getListener());
client = XdsTestClient.createFromServer('listener1', xdsServer, credentials.createInsecure());
const error = await client.sendOneCallAsync();
assert.strictEqual(error, null);
});
it('Should use the identity certificate when configured', async () => {
const [backend] = await createBackends(1, true, new XdsServerCredentials(ServerCredentials.createInsecure()));
const downstreamTlsContext: DownstreamTlsContext & AnyExtension = {
'@type': DOWNSTREAM_TLS_CONTEXT_TYPE_URL,
common_tls_context: {
tls_certificate_provider_instance: {
instance_name: 'test_certificates'
},
validation_context: {}
},
ocsp_staple_policy: 'LENIENT_STAPLING'
}
const baseServerListener: Listener = {
default_filter_chain: {
filter_chain_match: {
source_type: 'SAME_IP_OR_LOOPBACK'
},
transport_socket: {
name: 'envoy.transport_sockets.tls',
typed_config: downstreamTlsContext
}
}
}
const serverRoute = new FakeServerRoute(backend.getPort(), 'serverRoute', baseServerListener);
xdsServer.setRdsResource(serverRoute.getRouteConfiguration());
xdsServer.setLdsResource(serverRoute.getListener());
xdsServer.addResponseListener((typeUrl, responseState) => {
if (responseState.state === 'NACKED') {
client?.stopCalls();
assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`);
}
});
const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [backend], locality:{region: 'region1'}}]);
const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]);
await routeGroup.startAllBackends(xdsServer);
xdsServer.setEdsResource(cluster.getEndpointConfig());
xdsServer.setCdsResource(cluster.getClusterConfig());
xdsServer.setRdsResource(routeGroup.getRouteConfiguration());
xdsServer.setLdsResource(routeGroup.getListener());
client = XdsTestClient.createFromServer('listener1', xdsServer, credentials.createSsl(ca), {
'grpc.ssl_target_name_override': 'foo.test.google.fr',
'grpc.default_authority': 'foo.test.google.fr',
});
const error = await client.sendOneCallAsync();
assert.strictEqual(error, null);
});
it('Should use identity and CA certificates when configured', async () => {
const [backend] = await createBackends(1, true, new XdsServerCredentials(ServerCredentials.createInsecure()));
const downstreamTlsContext: DownstreamTlsContext & AnyExtension = {
'@type': DOWNSTREAM_TLS_CONTEXT_TYPE_URL,
common_tls_context: {
tls_certificate_provider_instance: {
instance_name: 'test_certificates'
},
validation_context: {
ca_certificate_provider_instance: {
instance_name: 'test_certificates'
}
}
},
ocsp_staple_policy: 'LENIENT_STAPLING',
require_client_certificate: {
value: true
}
}
const baseServerListener: Listener = {
default_filter_chain: {
filter_chain_match: {
source_type: 'SAME_IP_OR_LOOPBACK'
},
transport_socket: {
name: 'envoy.transport_sockets.tls',
typed_config: downstreamTlsContext
}
}
}
const serverRoute = new FakeServerRoute(backend.getPort(), 'serverRoute', baseServerListener);
xdsServer.setRdsResource(serverRoute.getRouteConfiguration());
xdsServer.setLdsResource(serverRoute.getListener());
xdsServer.addResponseListener((typeUrl, responseState) => {
if (responseState.state === 'NACKED') {
client?.stopCalls();
assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`);
}
});
const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [backend], locality:{region: 'region1'}}]);
const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]);
await routeGroup.startAllBackends(xdsServer);
xdsServer.setEdsResource(cluster.getEndpointConfig());
xdsServer.setCdsResource(cluster.getClusterConfig());
xdsServer.setRdsResource(routeGroup.getRouteConfiguration());
xdsServer.setLdsResource(routeGroup.getListener());
client = XdsTestClient.createFromServer('listener1', xdsServer, credentials.createSsl(ca, key, cert), {
'grpc.ssl_target_name_override': 'foo.test.google.fr',
'grpc.default_authority': 'foo.test.google.fr',
});
const error = await client.sendOneCallAsync();
assert.strictEqual(error, null);
});
});
describe('Client xDS credentials', () => {
let xdsServer: ControlPlaneServer;
let client: XdsTestClient;
beforeEach(done => {
xdsServer = new ControlPlaneServer();
xdsServer.startServer(error => {
done(error);
});
});
afterEach(() => {
client?.close();
xdsServer?.shutdownServer();
});
it('Should use fallback credentials when certificate providers are not configured', async () => {
const [backend] = await createBackends(1, true, ServerCredentials.createInsecure());
const serverRoute = new FakeServerRoute(backend.getPort(), 'serverRoute');
xdsServer.setRdsResource(serverRoute.getRouteConfiguration());
xdsServer.setLdsResource(serverRoute.getListener());
xdsServer.addResponseListener((typeUrl, responseState) => {
if (responseState.state === 'NACKED') {
client?.stopCalls();
assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`);
}
});
const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [backend], locality:{region: 'region1'}}]);
const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]);
await routeGroup.startAllBackends(xdsServer);
xdsServer.setEdsResource(cluster.getEndpointConfig());
xdsServer.setCdsResource(cluster.getClusterConfig());
xdsServer.setRdsResource(routeGroup.getRouteConfiguration());
xdsServer.setLdsResource(routeGroup.getListener());
client = XdsTestClient.createFromServer('listener1', xdsServer, new XdsChannelCredentials(credentials.createInsecure()));
const error = await client.sendOneCallAsync();
assert.strictEqual(error, null);
});
it('Should use CA certificates when configured', async () => {
const [backend] = await createBackends(1, true, ServerCredentials.createSsl(null, [{private_key: key, cert_chain: cert}]));
const serverRoute = new FakeServerRoute(backend.getPort(), 'serverRoute');
xdsServer.setRdsResource(serverRoute.getRouteConfiguration());
xdsServer.setLdsResource(serverRoute.getListener());
xdsServer.addResponseListener((typeUrl, responseState) => {
if (responseState.state === 'NACKED') {
client?.stopCalls();
assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`);
}
});
const upstreamTlsContext: UpstreamTlsContext = {
common_tls_context: {
validation_context: {
ca_certificate_provider_instance: {
instance_name: 'test_certificates'
}
}
}
};
const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [backend], locality:{region: 'region1'}}], undefined, upstreamTlsContext);
const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]);
await routeGroup.startAllBackends(xdsServer);
xdsServer.setEdsResource(cluster.getEndpointConfig());
xdsServer.setCdsResource(cluster.getClusterConfig());
xdsServer.setRdsResource(routeGroup.getRouteConfiguration());
xdsServer.setLdsResource(routeGroup.getListener());
client = XdsTestClient.createFromServer('listener1', xdsServer, new XdsChannelCredentials(credentials.createInsecure()));
const error = await client.sendOneCallAsync();
assert.strictEqual(error, null);
});
it('Should use identity and CA certificates when configured', async () => {
const [backend] = await createBackends(1, true, ServerCredentials.createSsl(ca, [{private_key: key, cert_chain: cert}], true));
const serverRoute = new FakeServerRoute(backend.getPort(), 'serverRoute');
xdsServer.setRdsResource(serverRoute.getRouteConfiguration());
xdsServer.setLdsResource(serverRoute.getListener());
xdsServer.addResponseListener((typeUrl, responseState) => {
if (responseState.state === 'NACKED') {
client?.stopCalls();
assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`);
}
});
const upstreamTlsContext: UpstreamTlsContext = {
common_tls_context: {
tls_certificate_provider_instance: {
instance_name: 'test_certificates'
},
validation_context: {
ca_certificate_provider_instance: {
instance_name: 'test_certificates'
}
}
}
};
const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [backend], locality:{region: 'region1'}}], undefined, upstreamTlsContext);
const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]);
await routeGroup.startAllBackends(xdsServer);
xdsServer.setEdsResource(cluster.getEndpointConfig());
xdsServer.setCdsResource(cluster.getClusterConfig());
xdsServer.setRdsResource(routeGroup.getRouteConfiguration());
xdsServer.setLdsResource(routeGroup.getListener());
client = XdsTestClient.createFromServer('listener1', xdsServer, new XdsChannelCredentials(credentials.createInsecure()));
const error = await client.sendOneCallAsync();
assert.strictEqual(error, null);
});
describe('Subject Alternative Name matching', () => {
interface SanTestCase {
name: string;
matchers: StringMatcher[];
expectedSuccess: boolean;
}
const testCases: SanTestCase[] = [
{
name: 'empty match',
matchers: [],
expectedSuccess: true
},
{
name: 'exact DNS match',
matchers: [{
exact: 'waterzooi.test.google.be',
ignore_case: false
}],
expectedSuccess: true
},
{
name: 'wildcard DNS match',
matchers: [{
exact: 'foo.test.google.fr',
ignore_case: false
}],
expectedSuccess: true
},
{
name: 'exact IP match',
matchers: [{
exact: '192.168.1.3',
ignore_case: false
}],
expectedSuccess: true
},
{
name: 'suffix match',
matchers: [{
suffix: 'test.google.fr',
ignore_case: false
}],
expectedSuccess: true
},
{
name: 'unmatched matcher',
matchers: [{
exact: 'incorret',
ignore_case: false
}],
expectedSuccess: false
},
];
for (const {name, matchers, expectedSuccess} of testCases) {
it(name, async () => {
const [backend] = await createBackends(1, true, ServerCredentials.createSsl(null, [{private_key: key, cert_chain: cert}]));
const serverRoute = new FakeServerRoute(backend.getPort(), 'serverRoute');
xdsServer.setRdsResource(serverRoute.getRouteConfiguration());
xdsServer.setLdsResource(serverRoute.getListener());
xdsServer.addResponseListener((typeUrl, responseState) => {
if (responseState.state === 'NACKED') {
client?.stopCalls();
assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`);
}
});
const upstreamTlsContext: UpstreamTlsContext = {
common_tls_context: {
validation_context: {
ca_certificate_provider_instance: {
instance_name: 'test_certificates'
},
match_subject_alt_names: matchers
}
}
};
const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [backend], locality:{region: 'region1'}}], undefined, upstreamTlsContext);
const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]);
await routeGroup.startAllBackends(xdsServer);
xdsServer.setEdsResource(cluster.getEndpointConfig());
xdsServer.setCdsResource(cluster.getClusterConfig());
xdsServer.setRdsResource(routeGroup.getRouteConfiguration());
xdsServer.setLdsResource(routeGroup.getListener());
client = XdsTestClient.createFromServer('listener1', xdsServer, new XdsChannelCredentials(credentials.createInsecure()));
const error = await client.sendOneCallAsync();
if (expectedSuccess) {
assert.strictEqual(error, null);
} else {
assert.ok(error);
}
});
}
});
describe('Client and server xDS credentials', () => {
let xdsServer: ControlPlaneServer;
let client: XdsTestClient;
beforeEach(done => {
xdsServer = new ControlPlaneServer();
xdsServer.startServer(error => {
done(error);
});
});
afterEach(() => {
client?.close();
xdsServer?.shutdownServer();
});
it('Should use identity and CA certificates when configured', async () => {
const [backend] = await createBackends(1, true, new XdsServerCredentials(ServerCredentials.createInsecure()));
const downstreamTlsContext: DownstreamTlsContext & AnyExtension = {
'@type': DOWNSTREAM_TLS_CONTEXT_TYPE_URL,
common_tls_context: {
tls_certificate_provider_instance: {
instance_name: 'test_certificates'
},
validation_context: {
ca_certificate_provider_instance: {
instance_name: 'test_certificates'
}
}
},
ocsp_staple_policy: 'LENIENT_STAPLING',
require_client_certificate: {
value: true
}
}
const baseServerListener: Listener = {
default_filter_chain: {
filter_chain_match: {
source_type: 'SAME_IP_OR_LOOPBACK'
},
transport_socket: {
name: 'envoy.transport_sockets.tls',
typed_config: downstreamTlsContext
}
}
}
const serverRoute = new FakeServerRoute(backend.getPort(), 'serverRoute', baseServerListener);
xdsServer.setRdsResource(serverRoute.getRouteConfiguration());
xdsServer.setLdsResource(serverRoute.getListener());
xdsServer.addResponseListener((typeUrl, responseState) => {
if (responseState.state === 'NACKED') {
client?.stopCalls();
assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`);
}
});
const upstreamTlsContext: UpstreamTlsContext = {
common_tls_context: {
tls_certificate_provider_instance: {
instance_name: 'test_certificates'
},
validation_context: {
ca_certificate_provider_instance: {
instance_name: 'test_certificates'
}
}
}
};
const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [backend], locality:{region: 'region1'}}], undefined, upstreamTlsContext);
const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]);
await routeGroup.startAllBackends(xdsServer);
xdsServer.setEdsResource(cluster.getEndpointConfig());
xdsServer.setCdsResource(cluster.getClusterConfig());
xdsServer.setRdsResource(routeGroup.getRouteConfiguration());
xdsServer.setLdsResource(routeGroup.getListener());
client = XdsTestClient.createFromServer('listener1', xdsServer, new XdsChannelCredentials(credentials.createInsecure()));
const error = await client.sendOneCallAsync();
assert.strictEqual(error, null);
});
it('Should fail when the server expects a certificate and does not get one', async () => {
const [backend] = await createBackends(1, true, new XdsServerCredentials(ServerCredentials.createInsecure()));
const downstreamTlsContext: DownstreamTlsContext & AnyExtension = {
'@type': DOWNSTREAM_TLS_CONTEXT_TYPE_URL,
common_tls_context: {
tls_certificate_provider_instance: {
instance_name: 'test_certificates'
},
validation_context: {
ca_certificate_provider_instance: {
instance_name: 'test_certificates'
}
}
},
ocsp_staple_policy: 'LENIENT_STAPLING',
require_client_certificate: {
value: true
}
}
const baseServerListener: Listener = {
default_filter_chain: {
filter_chain_match: {
source_type: 'SAME_IP_OR_LOOPBACK'
},
transport_socket: {
name: 'envoy.transport_sockets.tls',
typed_config: downstreamTlsContext
}
}
}
const serverRoute = new FakeServerRoute(backend.getPort(), 'serverRoute', baseServerListener);
xdsServer.setRdsResource(serverRoute.getRouteConfiguration());
xdsServer.setLdsResource(serverRoute.getListener());
xdsServer.addResponseListener((typeUrl, responseState) => {
if (responseState.state === 'NACKED') {
client?.stopCalls();
assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`);
}
});
const upstreamTlsContext: UpstreamTlsContext = {
common_tls_context: {
validation_context: {
ca_certificate_provider_instance: {
instance_name: 'test_certificates'
}
}
}
};
const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [backend], locality:{region: 'region1'}}], undefined, upstreamTlsContext);
const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]);
await routeGroup.startAllBackends(xdsServer);
xdsServer.setEdsResource(cluster.getEndpointConfig());
xdsServer.setCdsResource(cluster.getClusterConfig());
xdsServer.setRdsResource(routeGroup.getRouteConfiguration());
xdsServer.setLdsResource(routeGroup.getListener());
client = XdsTestClient.createFromServer('listener1', xdsServer, new XdsChannelCredentials(credentials.createInsecure()));
const error = await client.sendOneCallAsync();
assert(error);
assert.strictEqual(error.code, status.UNAVAILABLE);
});
});
});