Skip to content

Commit a62d693

Browse files
authored
Fixes incomplete client cert chain when using PKI authentication with the login selector (#88229)
1 parent 8678954 commit a62d693

8 files changed

Lines changed: 414 additions & 128 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
2+
3+
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [IKibanaSocket](./kibana-plugin-core-server.ikibanasocket.md) &gt; [getProtocol](./kibana-plugin-core-server.ikibanasocket.getprotocol.md)
4+
5+
## IKibanaSocket.getProtocol() method
6+
7+
Returns a string containing the negotiated SSL/TLS protocol version of the current connection. The value 'unknown' will be returned for connected sockets that have not completed the handshaking process. The value null will be returned for server sockets or disconnected client sockets. See https://www.openssl.org/docs/man1.0.2/ssl/SSL\_get\_version.html for more information.
8+
9+
<b>Signature:</b>
10+
11+
```typescript
12+
getProtocol(): string | null;
13+
```
14+
<b>Returns:</b>
15+
16+
`string | null`
17+

docs/development/core/server/kibana-plugin-core-server.ikibanasocket.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,6 @@ export interface IKibanaSocket
2626
| [getPeerCertificate(detailed)](./kibana-plugin-core-server.ikibanasocket.getpeercertificate.md) | |
2727
| [getPeerCertificate(detailed)](./kibana-plugin-core-server.ikibanasocket.getpeercertificate_1.md) | |
2828
| [getPeerCertificate(detailed)](./kibana-plugin-core-server.ikibanasocket.getpeercertificate_2.md) | Returns an object representing the peer's certificate. The returned object has some properties corresponding to the field of the certificate. If detailed argument is true the full chain with issuer property will be returned, if false only the top certificate without issuer property. If the peer does not provide a certificate, it returns null. |
29+
| [getProtocol()](./kibana-plugin-core-server.ikibanasocket.getprotocol.md) | Returns a string containing the negotiated SSL/TLS protocol version of the current connection. The value 'unknown' will be returned for connected sockets that have not completed the handshaking process. The value null will be returned for server sockets or disconnected client sockets. See https://www.openssl.org/docs/man1.0.2/ssl/SSL\_get\_version.html for more information. |
30+
| [renegotiate(options)](./kibana-plugin-core-server.ikibanasocket.renegotiate.md) | Renegotiates a connection to obtain the peer's certificate. This cannot be used when the protocol version is TLSv1.3. |
2931

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
2+
3+
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [IKibanaSocket](./kibana-plugin-core-server.ikibanasocket.md) &gt; [renegotiate](./kibana-plugin-core-server.ikibanasocket.renegotiate.md)
4+
5+
## IKibanaSocket.renegotiate() method
6+
7+
Renegotiates a connection to obtain the peer's certificate. This cannot be used when the protocol version is TLSv1.3.
8+
9+
<b>Signature:</b>
10+
11+
```typescript
12+
renegotiate(options: {
13+
rejectUnauthorized?: boolean;
14+
requestCert?: boolean;
15+
}): Promise<void>;
16+
```
17+
18+
## Parameters
19+
20+
| Parameter | Type | Description |
21+
| --- | --- | --- |
22+
| options | <code>{</code><br/><code> rejectUnauthorized?: boolean;</code><br/><code> requestCert?: boolean;</code><br/><code> }</code> | The options may contain the following fields: rejectUnauthorized, requestCert (See tls.createServer() for details). |
23+
24+
<b>Returns:</b>
25+
26+
`Promise<void>`
27+
28+
A Promise that will be resolved if renegotiation succeeded, or will be rejected if renegotiation failed.
29+

src/core/server/http/router/socket.test.ts

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ import { KibanaSocket } from './socket';
2222

2323
describe('KibanaSocket', () => {
2424
describe('getPeerCertificate', () => {
25-
it('returns null for net.Socket instance', () => {
25+
it('returns `null` for net.Socket instance', () => {
2626
const socket = new KibanaSocket(new Socket());
2727

28-
expect(socket.getPeerCertificate()).toBe(null);
28+
expect(socket.getPeerCertificate()).toBeNull();
2929
});
3030

3131
it('delegates a call to tls.Socket instance', () => {
@@ -40,20 +40,82 @@ describe('KibanaSocket', () => {
4040
expect(result).toBe(cert);
4141
});
4242

43-
it('returns null if tls.Socket getPeerCertificate returns null', () => {
43+
it('returns `null` if tls.Socket getPeerCertificate returns null', () => {
4444
const tlsSocket = new TLSSocket(new Socket());
4545
jest.spyOn(tlsSocket, 'getPeerCertificate').mockImplementation(() => null as any);
4646
const socket = new KibanaSocket(tlsSocket);
4747

48-
expect(socket.getPeerCertificate()).toBe(null);
48+
expect(socket.getPeerCertificate()).toBeNull();
4949
});
5050

51-
it('returns null if tls.Socket getPeerCertificate returns empty object', () => {
51+
it('returns `null` if tls.Socket getPeerCertificate returns empty object', () => {
5252
const tlsSocket = new TLSSocket(new Socket());
5353
jest.spyOn(tlsSocket, 'getPeerCertificate').mockImplementation(() => ({} as any));
5454
const socket = new KibanaSocket(tlsSocket);
5555

56-
expect(socket.getPeerCertificate()).toBe(null);
56+
expect(socket.getPeerCertificate()).toBeNull();
57+
});
58+
});
59+
60+
describe('getProtocol', () => {
61+
it('returns `null` for net.Socket instance', () => {
62+
const socket = new KibanaSocket(new Socket());
63+
64+
expect(socket.getProtocol()).toBeNull();
65+
});
66+
67+
it('delegates a call to tls.Socket instance', () => {
68+
const tlsSocket = new TLSSocket(new Socket());
69+
const protocol = 'TLSv1.2';
70+
const spy = jest.spyOn(tlsSocket, 'getProtocol').mockImplementation(() => protocol);
71+
const socket = new KibanaSocket(tlsSocket);
72+
const result = socket.getProtocol();
73+
74+
expect(spy).toBeCalledTimes(1);
75+
expect(result).toBe(protocol);
76+
});
77+
78+
it('returns `null` if tls.Socket getProtocol returns null', () => {
79+
const tlsSocket = new TLSSocket(new Socket());
80+
jest.spyOn(tlsSocket, 'getProtocol').mockImplementation(() => null as any);
81+
const socket = new KibanaSocket(tlsSocket);
82+
83+
expect(socket.getProtocol()).toBeNull();
84+
});
85+
});
86+
87+
describe('renegotiate', () => {
88+
it('throws error for net.Socket instance', async () => {
89+
const socket = new KibanaSocket(new Socket());
90+
91+
expect(() => socket.renegotiate({})).rejects.toThrowErrorMatchingInlineSnapshot(
92+
`"Cannot renegotiate a connection when TLS is not enabled."`
93+
);
94+
});
95+
96+
it('delegates a call to tls.Socket instance', async () => {
97+
const tlsSocket = new TLSSocket(new Socket());
98+
const result = Symbol();
99+
const spy = jest.spyOn(tlsSocket, 'renegotiate').mockImplementation((_, callback) => {
100+
callback(result as any);
101+
return undefined;
102+
});
103+
const socket = new KibanaSocket(tlsSocket);
104+
105+
expect(socket.renegotiate({})).resolves.toBe(result);
106+
expect(spy).toBeCalledTimes(1);
107+
});
108+
109+
it('throws error if tls.Socket renegotiate returns error', async () => {
110+
const tlsSocket = new TLSSocket(new Socket());
111+
const error = new Error('Oh no!');
112+
jest.spyOn(tlsSocket, 'renegotiate').mockImplementation((_, callback) => {
113+
callback(error);
114+
return undefined;
115+
});
116+
const socket = new KibanaSocket(tlsSocket);
117+
118+
expect(() => socket.renegotiate({})).rejects.toThrow(error);
57119
});
58120
});
59121

@@ -68,12 +130,11 @@ describe('KibanaSocket', () => {
68130
const tlsSocket = new TLSSocket(new Socket());
69131

70132
tlsSocket.authorized = true;
71-
let socket = new KibanaSocket(tlsSocket);
133+
const socket = new KibanaSocket(tlsSocket);
72134
expect(tlsSocket.authorized).toBe(true);
73135
expect(socket.authorized).toBe(true);
74136

75137
tlsSocket.authorized = false;
76-
socket = new KibanaSocket(tlsSocket);
77138
expect(tlsSocket.authorized).toBe(false);
78139
expect(socket.authorized).toBe(false);
79140
});
@@ -90,13 +151,12 @@ describe('KibanaSocket', () => {
90151
const tlsSocket = new TLSSocket(new Socket());
91152
tlsSocket.authorizationError = undefined as any;
92153

93-
let socket = new KibanaSocket(tlsSocket);
154+
const socket = new KibanaSocket(tlsSocket);
94155
expect(tlsSocket.authorizationError).toBeUndefined();
95156
expect(socket.authorizationError).toBeUndefined();
96157

97158
const authorizationError = new Error('some error');
98159
tlsSocket.authorizationError = authorizationError;
99-
socket = new KibanaSocket(tlsSocket);
100160

101161
expect(tlsSocket.authorizationError).toBe(authorizationError);
102162
expect(socket.authorizationError).toBe(authorizationError);

src/core/server/http/router/socket.ts

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import { Socket } from 'net';
2121
import { DetailedPeerCertificate, PeerCertificate, TLSSocket } from 'tls';
22+
import { promisify } from 'util';
2223

2324
/**
2425
* A tiny abstraction for TCP socket.
@@ -38,6 +39,20 @@ export interface IKibanaSocket {
3839
*/
3940
getPeerCertificate(detailed?: boolean): PeerCertificate | DetailedPeerCertificate | null;
4041

42+
/**
43+
* Returns a string containing the negotiated SSL/TLS protocol version of the current connection. The value 'unknown' will be returned for
44+
* connected sockets that have not completed the handshaking process. The value null will be returned for server sockets or disconnected
45+
* client sockets. See https://www.openssl.org/docs/man1.0.2/ssl/SSL_get_version.html for more information.
46+
*/
47+
getProtocol(): string | null;
48+
49+
/**
50+
* Renegotiates a connection to obtain the peer's certificate. This cannot be used when the protocol version is TLSv1.3.
51+
* @param options - The options may contain the following fields: rejectUnauthorized, requestCert (See tls.createServer() for details).
52+
* @returns A Promise that will be resolved if renegotiation succeeded, or will be rejected if renegotiation failed.
53+
*/
54+
renegotiate(options: { rejectUnauthorized?: boolean; requestCert?: boolean }): Promise<void>;
55+
4156
/**
4257
* Indicates whether or not the peer certificate was signed by one of the specified CAs. When TLS
4358
* isn't used the value is `undefined`.
@@ -52,15 +67,14 @@ export interface IKibanaSocket {
5267
}
5368

5469
export class KibanaSocket implements IKibanaSocket {
55-
readonly authorized?: boolean;
56-
readonly authorizationError?: Error;
57-
58-
constructor(private readonly socket: Socket) {
59-
if (this.socket instanceof TLSSocket) {
60-
this.authorized = this.socket.authorized;
61-
this.authorizationError = this.socket.authorizationError;
62-
}
70+
public get authorized() {
71+
return this.socket instanceof TLSSocket ? this.socket.authorized : undefined;
6372
}
73+
public get authorizationError() {
74+
return this.socket instanceof TLSSocket ? this.socket.authorizationError : undefined;
75+
}
76+
77+
constructor(private readonly socket: Socket) {}
6478

6579
getPeerCertificate(detailed: true): DetailedPeerCertificate | null;
6680
getPeerCertificate(detailed: false): PeerCertificate | null;
@@ -76,4 +90,18 @@ export class KibanaSocket implements IKibanaSocket {
7690
}
7791
return null;
7892
}
93+
94+
public getProtocol() {
95+
if (this.socket instanceof TLSSocket) {
96+
return this.socket.getProtocol();
97+
}
98+
return null;
99+
}
100+
101+
public async renegotiate(options: { rejectUnauthorized?: boolean; requestCert?: boolean }) {
102+
if (this.socket instanceof TLSSocket) {
103+
return promisify(this.socket.renegotiate.bind(this.socket))(options);
104+
}
105+
return Promise.reject(new Error('Cannot renegotiate a connection when TLS is not enabled.'));
106+
}
79107
}

src/core/server/server.api.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,6 +1101,11 @@ export interface IKibanaSocket {
11011101
// (undocumented)
11021102
getPeerCertificate(detailed: false): PeerCertificate | null;
11031103
getPeerCertificate(detailed?: boolean): PeerCertificate | DetailedPeerCertificate | null;
1104+
getProtocol(): string | null;
1105+
renegotiate(options: {
1106+
rejectUnauthorized?: boolean;
1107+
requestCert?: boolean;
1108+
}): Promise<void>;
11041109
}
11051110

11061111
// @public @deprecated

0 commit comments

Comments
 (0)