channel-credentials.ts 12 KB

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