UNPKG

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