Skip to content

Commit e4bcbb0

Browse files
committed
feat: adds support for E2E TLS connection between AMT and RPS#
1 parent 1335ad4 commit e4bcbb0

File tree

13 files changed

+523
-31
lines changed

13 files changed

+523
-31
lines changed

src/DataProcessor.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ export class DataProcessor {
7979
await this.handleConnectionReset(clientMsg, clientId)
8080
break
8181
}
82+
case ClientMethods.PORT_SWITCH_ACK: {
83+
await this.handlePortSwitchAck(clientMsg, clientId)
84+
break
85+
}
8286
default: {
8387
const uuid = clientMsg.payload.uuid ? clientMsg.payload.uuid : devices[clientId].ClientData.payload.uuid
8488
throw new RPSError(`Device ${uuid} Not a supported method received from AMT device`)
@@ -253,6 +257,15 @@ export class DataProcessor {
253257
}
254258
}
255259

260+
async handlePortSwitchAck(clientMsg: ClientMsg, clientId: string): Promise<void> {
261+
const clientObj = devices[clientId]
262+
this.logger.info(`PORT_SWITCH_ACK received from rpc-go for device ${clientObj?.uuid}`)
263+
264+
if (clientObj?.pendingPromise != null && clientObj.resolve != null) {
265+
clientObj.resolve('port_switch_ack')
266+
}
267+
}
268+
256269
async handleConnectionReset(clientMsg: ClientMsg, clientId: string): Promise<void> {
257270
const clientObj = devices[clientId]
258271
this.logger.warn(`CONNECTION RESET from rpc-go`)

src/Validator.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ export class Validator implements IValidator {
8080
this.logger.info(`Device ${payload.uuid} has TLS enforced - enabling TLS tunnel mode`)
8181
}
8282
}
83+
// Extract TLS tunnel activation flag from payload
84+
if (msg.payload.tlsTunnel === true) {
85+
clientObj.tlsTunnelActivation = true
86+
this.logger.info(`Device ${payload.uuid} requested TLS tunnel activation`)
87+
}
8388
// Check for client requested action and profile activation
8489
const profile: AMTConfiguration | null = await this.configurator.profileManager.getAmtProfile(
8590
payload.profile,

src/interfaces/ISecretManagerService.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ export interface DeviceCredentials {
99
AMT_PASSWORD: string | null
1010
MPS_PASSWORD?: string // only required for CIRA
1111
MEBX_PASSWORD?: string | null
12+
TLS_ROOT_CERTIFICATE?: string
13+
TLS_ISSUED_CERTIFICATE?: string
1214
version?: string
1315
}
1416

src/models/RCS.Config.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export interface ClientObject {
126126
resolve: (value: unknown) => void
127127
reject: (value: unknown) => void
128128
tlsEnforced?: boolean
129+
tlsTunnelActivation?: boolean
129130
tlsTunnelManager?: TLSTunnelManager
130131
tlsTunnelNeedsReset?: boolean
131132
tlsTunnelSessionId?: string // Current TLS session ID for filtering stale data
@@ -200,6 +201,9 @@ export interface TLSConfigFlow {
200201
commitLocalTLS?: boolean
201202
getTimeSynch?: boolean
202203
setTimeSynch?: boolean
204+
rootCertPEM?: string
205+
rootCertKey?: any
206+
issuedCertPEM?: string
203207
}
204208

205209
export interface mpsServer {
@@ -240,6 +244,7 @@ export interface Payload {
240244
client: string
241245
profile?: any
242246
tlsEnforced?: boolean
247+
tlsTunnel?: boolean
243248
}
244249

245250
export interface ConnectionObject {
@@ -271,7 +276,9 @@ export enum ClientMethods {
271276
HEARTBEAT = 'heartbeat_response',
272277
MAINTENANCE = 'maintenance',
273278
TLS_DATA = 'tls_data',
274-
CONNECTION_RESET = 'connection_reset'
279+
CONNECTION_RESET = 'connection_reset',
280+
PORT_SWITCH = 'port_switch',
281+
PORT_SWITCH_ACK = 'port_switch_ack'
275282
}
276283

277284
export interface apiResponse {

src/stateMachines/activation.test.ts

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ describe('Activation State Machine', () => {
133133
canActivate: true,
134134
shbcCCMComplete: false,
135135
shbcACMComplete: false,
136+
tlsTunnelCCMComplete: false,
136137
message: '',
137138
clientId,
138139
xmlMessage: '',
@@ -259,7 +260,11 @@ describe('Activation State Machine', () => {
259260
tls: fromPromise(async ({ input }) => await Promise.resolve({ clientId })),
260261
cira: fromPromise(async ({ input }) => await Promise.resolve({ clientId })),
261262
setMEBxPassword: fromPromise(async ({ input }) => await Promise.resolve({ clientId })),
262-
initializeTLSTunnel: fromPromise(async () => await Promise.resolve(true))
263+
initializeTLSTunnel: fromPromise(async () => await Promise.resolve(true)),
264+
saveTlsTunnelCerts: fromPromise(async () => await Promise.resolve(true)),
265+
signalPortSwitch: fromPromise(async () => await Promise.resolve(true)),
266+
initializeTlsTunnelConnection: fromPromise(async () => await Promise.resolve(true)),
267+
tlsTunnelProvisioning: fromPromise(async () => await Promise.resolve({ clientId }))
263268
},
264269
actions: {
265270
'Read General Settings': () => {},
@@ -2097,4 +2102,120 @@ describe('Activation State Machine', () => {
20972102
expect(devices[clientId].tlsTunnelManager).toBeUndefined()
20982103
})
20992104
})
2105+
2106+
describe('TLS Tunnel Activation', () => {
2107+
describe('saveTlsTunnelCerts', () => {
2108+
it('should save certs to vault for ACM', async () => {
2109+
const clientObj = devices[clientId]
2110+
clientObj.uuid = 'test-uuid'
2111+
clientObj.amtPassword = 'testAMTpw'
2112+
clientObj.mebxPassword = 'testMEBXpw'
2113+
clientObj.action = ClientAction.ADMINCTLMODE
2114+
clientObj.tls = { rootCertPEM: 'rootCert', issuedCertPEM: 'issuedCert' } as any
2115+
2116+
const insertSpy = spyOn(activation.configurator.secretsManager, 'writeSecretWithObject').mockImplementation(
2117+
async () => true
2118+
)
2119+
const result = await activation.saveTlsTunnelCerts({ input: context } as any)
2120+
expect(result).toBe(true)
2121+
expect(insertSpy).toHaveBeenCalledWith('devices/test-uuid', {
2122+
AMT_PASSWORD: 'testAMTpw',
2123+
MEBX_PASSWORD: 'testMEBXpw',
2124+
TLS_ROOT_CERTIFICATE: 'rootCert',
2125+
TLS_ISSUED_CERTIFICATE: 'issuedCert'
2126+
})
2127+
})
2128+
2129+
it('should save certs to vault for CCM (mebxPassword null)', async () => {
2130+
const clientObj = devices[clientId]
2131+
clientObj.uuid = 'test-uuid'
2132+
clientObj.amtPassword = 'testAMTpw'
2133+
clientObj.mebxPassword = null
2134+
clientObj.action = ClientAction.CLIENTCTLMODE as any
2135+
clientObj.tls = { rootCertPEM: 'rootCert', issuedCertPEM: 'issuedCert' } as any
2136+
2137+
const insertSpy = spyOn(activation.configurator.secretsManager, 'writeSecretWithObject').mockImplementation(
2138+
async () => true
2139+
)
2140+
const result = await activation.saveTlsTunnelCerts({ input: context } as any)
2141+
expect(result).toBe(true)
2142+
expect(insertSpy).toHaveBeenCalledWith('devices/test-uuid', {
2143+
AMT_PASSWORD: 'testAMTpw',
2144+
MEBX_PASSWORD: null,
2145+
TLS_ROOT_CERTIFICATE: 'rootCert',
2146+
TLS_ISSUED_CERTIFICATE: 'issuedCert'
2147+
})
2148+
})
2149+
2150+
it('should throw when amtPassword is null', async () => {
2151+
const clientObj = devices[clientId]
2152+
clientObj.uuid = 'test-uuid'
2153+
clientObj.amtPassword = null
2154+
clientObj.mebxPassword = 'testMEBXpw'
2155+
clientObj.action = ClientAction.ADMINCTLMODE
2156+
2157+
await expect(activation.saveTlsTunnelCerts({ input: context } as any)).rejects.toThrow(
2158+
'Missing prerequisites for saving TLS tunnel certs'
2159+
)
2160+
})
2161+
2162+
it('should throw when mebxPassword is null for ACM', async () => {
2163+
const clientObj = devices[clientId]
2164+
clientObj.uuid = 'test-uuid'
2165+
clientObj.amtPassword = 'testAMTpw'
2166+
clientObj.mebxPassword = null
2167+
clientObj.action = ClientAction.ADMINCTLMODE
2168+
2169+
await expect(activation.saveTlsTunnelCerts({ input: context } as any)).rejects.toThrow(
2170+
'Missing prerequisites for saving TLS tunnel certs'
2171+
)
2172+
})
2173+
})
2174+
2175+
describe('signalPortSwitch', () => {
2176+
it('should send port_switch message and resolve on ACK', async () => {
2177+
const clientObj = devices[clientId]
2178+
clientObj.uuid = 'test-uuid'
2179+
clientObj.tls = { rootCertPEM: 'rootCert' } as any
2180+
responseMessageSpy.mockRestore()
2181+
sendSpy.mockRestore()
2182+
sendSpy = spyOn(devices[clientId].ClientSocket, 'send').mockReturnValue()
2183+
2184+
const promise = activation.signalPortSwitch({ input: context } as any)
2185+
2186+
// Simulate PORT_SWITCH_ACK by resolving the pending promise
2187+
await new Promise((r) => setTimeout(r, 10))
2188+
clientObj.resolve('port_switch_ack')
2189+
2190+
const result = await promise
2191+
expect(result).toBe(true)
2192+
expect(sendSpy).toHaveBeenCalled()
2193+
})
2194+
2195+
it('should reject on ACK timeout', async () => {
2196+
jest.useFakeTimers()
2197+
const clientObj = devices[clientId]
2198+
clientObj.uuid = 'test-uuid'
2199+
clientObj.tls = { rootCertPEM: 'rootCert' } as any
2200+
Environment.Config.delay_tls_timer = 1
2201+
2202+
const promise = activation.signalPortSwitch({ input: context } as any)
2203+
jest.advanceTimersByTime(61 * 1000 + 1)
2204+
2205+
await expect(promise).rejects.toThrow('PORT_SWITCH_ACK timeout')
2206+
jest.useRealTimers()
2207+
})
2208+
})
2209+
2210+
describe('initializeTlsTunnelConnection', () => {
2211+
it('should set tlsEnforced on the client object', () => {
2212+
// initializeTlsTunnelConnection creates a TLSTunnelManager and sets tlsEnforced = true on success.
2213+
// We verify that the state machine correctly routes failures via onError by checking the state definition.
2214+
const states = activation.machine.config.states as any
2215+
expect(states.INIT_TLS_TUNNEL_CONNECTION).toBeDefined()
2216+
expect(states.INIT_TLS_TUNNEL_CONNECTION.invoke.onError.target).toBe('FAILED')
2217+
expect(states.INIT_TLS_TUNNEL_CONNECTION.invoke.src).toBe('initializeTlsTunnelConnection')
2218+
})
2219+
})
2220+
})
21002221
})

0 commit comments

Comments
 (0)