UNPKG

8.53 kBPlain TextView Raw
1/*
2 * Copyright 2019 gRPC authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 *
16 */
17
18import {
19 ConnectionOptions,
20 createSecureContext,
21 PeerCertificate,
22 SecureContext,
23} from 'tls';
24
25import { CallCredentials } from './call-credentials';
26import { CIPHER_SUITES, getDefaultRootsData } from './tls-helpers';
27
28// eslint-disable-next-line @typescript-eslint/no-explicit-any
29function verifyIsBufferOrNull(obj: any, friendlyName: string): void {
30 if (obj && !(obj instanceof Buffer)) {
31 throw new TypeError(`${friendlyName}, if provided, must be a Buffer.`);
32 }
33}
34
35/**
36 * A callback that will receive the expected hostname and presented peer
37 * certificate as parameters. The callback should return an error to
38 * indicate that the presented certificate is considered invalid and
39 * otherwise returned undefined.
40 */
41export type CheckServerIdentityCallback = (
42 hostname: string,
43 cert: PeerCertificate
44) => Error | undefined;
45
46/**
47 * Additional peer verification options that can be set when creating
48 * SSL credentials.
49 */
50export interface VerifyOptions {
51 /**
52 * If set, this callback will be invoked after the usual hostname verification
53 * has been performed on the peer certificate.
54 */
55 checkServerIdentity?: CheckServerIdentityCallback;
56}
57
58/**
59 * A class that contains credentials for communicating over a channel, as well
60 * as a set of per-call credentials, which are applied to every method call made
61 * over a channel initialized with an instance of this class.
62 */
63export abstract class ChannelCredentials {
64 protected callCredentials: CallCredentials;
65
66 protected constructor(callCredentials?: CallCredentials) {
67 this.callCredentials = callCredentials || CallCredentials.createEmpty();
68 }
69 /**
70 * Returns a copy of this object with the included set of per-call credentials
71 * expanded to include callCredentials.
72 * @param callCredentials A CallCredentials object to associate with this
73 * instance.
74 */
75 abstract compose(callCredentials: CallCredentials): ChannelCredentials;
76
77 /**
78 * Gets the set of per-call credentials associated with this instance.
79 */
80 _getCallCredentials(): CallCredentials {
81 return this.callCredentials;
82 }
83
84 /**
85 * Gets a SecureContext object generated from input parameters if this
86 * instance was created with createSsl, or null if this instance was created
87 * with createInsecure.
88 */
89 abstract _getConnectionOptions(): ConnectionOptions | null;
90
91 /**
92 * Indicates whether this credentials object creates a secure channel.
93 */
94 abstract _isSecure(): boolean;
95
96 /**
97 * Check whether two channel credentials objects are equal. Two secure
98 * credentials are equal if they were constructed with the same parameters.
99 * @param other The other ChannelCredentials Object
100 */
101 abstract _equals(other: ChannelCredentials): boolean;
102
103 /**
104 * Return a new ChannelCredentials instance with a given set of credentials.
105 * The resulting instance can be used to construct a Channel that communicates
106 * over TLS.
107 * @param rootCerts The root certificate data.
108 * @param privateKey The client certificate private key, if available.
109 * @param certChain The client certificate key chain, if available.
110 * @param verifyOptions Additional options to modify certificate verification
111 */
112 static createSsl(
113 rootCerts?: Buffer | null,
114 privateKey?: Buffer | null,
115 certChain?: Buffer | null,
116 verifyOptions?: VerifyOptions
117 ): ChannelCredentials {
118 verifyIsBufferOrNull(rootCerts, 'Root certificate');
119 verifyIsBufferOrNull(privateKey, 'Private key');
120 verifyIsBufferOrNull(certChain, 'Certificate chain');
121 if (privateKey && !certChain) {
122 throw new Error(
123 'Private key must be given with accompanying certificate chain'
124 );
125 }
126 if (!privateKey && certChain) {
127 throw new Error(
128 'Certificate chain must be given with accompanying private key'
129 );
130 }
131 const secureContext = createSecureContext({
132 ca: rootCerts ?? getDefaultRootsData() ?? undefined,
133 key: privateKey ?? undefined,
134 cert: certChain ?? undefined,
135 ciphers: CIPHER_SUITES,
136 });
137 return new SecureChannelCredentialsImpl(secureContext, verifyOptions ?? {});
138 }
139
140 /**
141 * Return a new ChannelCredentials instance with credentials created using
142 * the provided secureContext. The resulting instances can be used to
143 * construct a Channel that communicates over TLS. gRPC will not override
144 * anything in the provided secureContext, so the environment variables
145 * GRPC_SSL_CIPHER_SUITES and GRPC_DEFAULT_SSL_ROOTS_FILE_PATH will
146 * not be applied.
147 * @param secureContext The return value of tls.createSecureContext()
148 * @param verifyOptions Additional options to modify certificate verification
149 */
150 static createFromSecureContext(
151 secureContext: SecureContext,
152 verifyOptions?: VerifyOptions
153 ): ChannelCredentials {
154 return new SecureChannelCredentialsImpl(secureContext, verifyOptions ?? {});
155 }
156
157 /**
158 * Return a new ChannelCredentials instance with no credentials.
159 */
160 static createInsecure(): ChannelCredentials {
161 return new InsecureChannelCredentialsImpl();
162 }
163}
164
165class InsecureChannelCredentialsImpl extends ChannelCredentials {
166 constructor(callCredentials?: CallCredentials) {
167 super(callCredentials);
168 }
169
170 compose(callCredentials: CallCredentials): never {
171 throw new Error('Cannot compose insecure credentials');
172 }
173
174 _getConnectionOptions(): ConnectionOptions | null {
175 return null;
176 }
177 _isSecure(): boolean {
178 return false;
179 }
180 _equals(other: ChannelCredentials): boolean {
181 return other instanceof InsecureChannelCredentialsImpl;
182 }
183}
184
185class SecureChannelCredentialsImpl extends ChannelCredentials {
186 connectionOptions: ConnectionOptions;
187
188 constructor(
189 private secureContext: SecureContext,
190 private verifyOptions: VerifyOptions
191 ) {
192 super();
193 this.connectionOptions = {
194 secureContext,
195 };
196 // Node asserts that this option is a function, so we cannot pass undefined
197 if (verifyOptions?.checkServerIdentity) {
198 this.connectionOptions.checkServerIdentity =
199 verifyOptions.checkServerIdentity;
200 }
201 }
202
203 compose(callCredentials: CallCredentials): ChannelCredentials {
204 const combinedCallCredentials =
205 this.callCredentials.compose(callCredentials);
206 return new ComposedChannelCredentialsImpl(this, combinedCallCredentials);
207 }
208
209 _getConnectionOptions(): ConnectionOptions | null {
210 // Copy to prevent callers from mutating this.connectionOptions
211 return { ...this.connectionOptions };
212 }
213 _isSecure(): boolean {
214 return true;
215 }
216 _equals(other: ChannelCredentials): boolean {
217 if (this === other) {
218 return true;
219 }
220 if (other instanceof SecureChannelCredentialsImpl) {
221 return (
222 this.secureContext === other.secureContext &&
223 this.verifyOptions.checkServerIdentity ===
224 other.verifyOptions.checkServerIdentity
225 );
226 } else {
227 return false;
228 }
229 }
230}
231
232class ComposedChannelCredentialsImpl extends ChannelCredentials {
233 constructor(
234 private channelCredentials: SecureChannelCredentialsImpl,
235 callCreds: CallCredentials
236 ) {
237 super(callCreds);
238 }
239 compose(callCredentials: CallCredentials) {
240 const combinedCallCredentials =
241 this.callCredentials.compose(callCredentials);
242 return new ComposedChannelCredentialsImpl(
243 this.channelCredentials,
244 combinedCallCredentials
245 );
246 }
247
248 _getConnectionOptions(): ConnectionOptions | null {
249 return this.channelCredentials._getConnectionOptions();
250 }
251 _isSecure(): boolean {
252 return true;
253 }
254 _equals(other: ChannelCredentials): boolean {
255 if (this === other) {
256 return true;
257 }
258 if (other instanceof ComposedChannelCredentialsImpl) {
259 return (
260 this.channelCredentials._equals(other.channelCredentials) &&
261 this.callCredentials._equals(other.callCredentials)
262 );
263 } else {
264 return false;
265 }
266 }
267}