Skip to content

Commit ab4810a

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

15 files changed

+548
-91
lines changed

package-lock.json

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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: 124 additions & 7 deletions
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': () => {},
@@ -1964,8 +1969,6 @@ describe('Activation State Machine', () => {
19641969
'DELAYED_TRANSITION',
19651970
'SAVE_DEVICE_TO_SECRET_PROVIDER',
19661971
'SAVE_DEVICE_TO_MPS',
1967-
'COMMIT_CHANGES_FOR_TLS',
1968-
'WAIT_AFTER_TLS_COMMIT',
19691972
'UNCONFIGURATION',
19701973
'NETWORK_CONFIGURATION',
19711974
'FEATURES_CONFIGURATION',
@@ -1975,7 +1978,7 @@ describe('Activation State Machine', () => {
19751978
ccmActivationService.subscribe((state) => {
19761979
const expectedState: any = flowStates[currentStateIndex++]
19771980
expect(state.matches(expectedState)).toBe(true)
1978-
if (state.matches('DELAYED_TRANSITION') || state.matches('WAIT_AFTER_TLS_COMMIT')) {
1981+
if (state.matches('DELAYED_TRANSITION')) {
19791982
jest.advanceTimersByTime(60000)
19801983
} else if (state.matches('PROVISIONED') && currentStateIndex === flowStates.length) {
19811984
done()
@@ -2019,8 +2022,6 @@ describe('Activation State Machine', () => {
20192022
'SET_MEBX_PASSWORD',
20202023
'SAVE_DEVICE_TO_SECRET_PROVIDER',
20212024
'SAVE_DEVICE_TO_MPS',
2022-
'COMMIT_CHANGES_FOR_TLS',
2023-
'WAIT_AFTER_TLS_COMMIT',
20242025
'UNCONFIGURATION',
20252026
'NETWORK_CONFIGURATION',
20262027
'FEATURES_CONFIGURATION',
@@ -2030,7 +2031,7 @@ describe('Activation State Machine', () => {
20302031
acmActivationService.subscribe((state) => {
20312032
const expectedState: any = flowStates[currentStateIndex++]
20322033
expect(state.matches(expectedState)).toBe(true)
2033-
if (state.matches('DELAYED_TRANSITION') || state.matches('WAIT_AFTER_TLS_COMMIT')) {
2034+
if (state.matches('DELAYED_TRANSITION')) {
20342035
jest.advanceTimersByTime(60000)
20352036
} else if (state.matches('PROVISIONED') && currentStateIndex === flowStates.length) {
20362037
done()
@@ -2097,4 +2098,120 @@ describe('Activation State Machine', () => {
20972098
expect(devices[clientId].tlsTunnelManager).toBeUndefined()
20982099
})
20992100
})
2101+
2102+
describe('TLS Tunnel Activation', () => {
2103+
describe('saveTlsTunnelCerts', () => {
2104+
it('should save certs to vault for ACM', async () => {
2105+
const clientObj = devices[clientId]
2106+
clientObj.uuid = 'test-uuid'
2107+
clientObj.amtPassword = 'testAMTpw'
2108+
clientObj.mebxPassword = 'testMEBXpw'
2109+
clientObj.action = ClientAction.ADMINCTLMODE
2110+
clientObj.tls = { rootCertPEM: 'rootCert', issuedCertPEM: 'issuedCert' } as any
2111+
2112+
const insertSpy = spyOn(activation.configurator.secretsManager, 'writeSecretWithObject').mockImplementation(
2113+
async () => true
2114+
)
2115+
const result = await activation.saveTlsTunnelCerts({ input: context } as any)
2116+
expect(result).toBe(true)
2117+
expect(insertSpy).toHaveBeenCalledWith('devices/test-uuid', {
2118+
AMT_PASSWORD: 'testAMTpw',
2119+
MEBX_PASSWORD: 'testMEBXpw',
2120+
TLS_ROOT_CERTIFICATE: 'rootCert',
2121+
TLS_ISSUED_CERTIFICATE: 'issuedCert'
2122+
})
2123+
})
2124+
2125+
it('should save certs to vault for CCM (mebxPassword null)', async () => {
2126+
const clientObj = devices[clientId]
2127+
clientObj.uuid = 'test-uuid'
2128+
clientObj.amtPassword = 'testAMTpw'
2129+
clientObj.mebxPassword = null
2130+
clientObj.action = ClientAction.CLIENTCTLMODE as any
2131+
clientObj.tls = { rootCertPEM: 'rootCert', issuedCertPEM: 'issuedCert' } as any
2132+
2133+
const insertSpy = spyOn(activation.configurator.secretsManager, 'writeSecretWithObject').mockImplementation(
2134+
async () => true
2135+
)
2136+
const result = await activation.saveTlsTunnelCerts({ input: context } as any)
2137+
expect(result).toBe(true)
2138+
expect(insertSpy).toHaveBeenCalledWith('devices/test-uuid', {
2139+
AMT_PASSWORD: 'testAMTpw',
2140+
MEBX_PASSWORD: null,
2141+
TLS_ROOT_CERTIFICATE: 'rootCert',
2142+
TLS_ISSUED_CERTIFICATE: 'issuedCert'
2143+
})
2144+
})
2145+
2146+
it('should throw when amtPassword is null', async () => {
2147+
const clientObj = devices[clientId]
2148+
clientObj.uuid = 'test-uuid'
2149+
clientObj.amtPassword = null
2150+
clientObj.mebxPassword = 'testMEBXpw'
2151+
clientObj.action = ClientAction.ADMINCTLMODE
2152+
2153+
await expect(activation.saveTlsTunnelCerts({ input: context } as any)).rejects.toThrow(
2154+
'Missing prerequisites for saving TLS tunnel certs'
2155+
)
2156+
})
2157+
2158+
it('should throw when mebxPassword is null for ACM', async () => {
2159+
const clientObj = devices[clientId]
2160+
clientObj.uuid = 'test-uuid'
2161+
clientObj.amtPassword = 'testAMTpw'
2162+
clientObj.mebxPassword = null
2163+
clientObj.action = ClientAction.ADMINCTLMODE
2164+
2165+
await expect(activation.saveTlsTunnelCerts({ input: context } as any)).rejects.toThrow(
2166+
'Missing prerequisites for saving TLS tunnel certs'
2167+
)
2168+
})
2169+
})
2170+
2171+
describe('signalPortSwitch', () => {
2172+
it('should send port_switch message and resolve on ACK', async () => {
2173+
const clientObj = devices[clientId]
2174+
clientObj.uuid = 'test-uuid'
2175+
clientObj.tls = { rootCertPEM: 'rootCert' } as any
2176+
responseMessageSpy.mockRestore()
2177+
sendSpy.mockRestore()
2178+
sendSpy = spyOn(devices[clientId].ClientSocket, 'send').mockReturnValue()
2179+
2180+
const promise = activation.signalPortSwitch({ input: context } as any)
2181+
2182+
// Simulate PORT_SWITCH_ACK by resolving the pending promise
2183+
await new Promise((r) => setTimeout(r, 10))
2184+
clientObj.resolve('port_switch_ack')
2185+
2186+
const result = await promise
2187+
expect(result).toBe(true)
2188+
expect(sendSpy).toHaveBeenCalled()
2189+
})
2190+
2191+
it('should reject on ACK timeout', async () => {
2192+
jest.useFakeTimers()
2193+
const clientObj = devices[clientId]
2194+
clientObj.uuid = 'test-uuid'
2195+
clientObj.tls = { rootCertPEM: 'rootCert' } as any
2196+
Environment.Config.delay_tls_timer = 1
2197+
2198+
const promise = activation.signalPortSwitch({ input: context } as any)
2199+
jest.advanceTimersByTime(61 * 1000 + 1)
2200+
2201+
await expect(promise).rejects.toThrow('PORT_SWITCH_ACK timeout')
2202+
jest.useRealTimers()
2203+
})
2204+
})
2205+
2206+
describe('initializeTlsTunnelConnection', () => {
2207+
it('should set tlsEnforced on the client object', () => {
2208+
// initializeTlsTunnelConnection creates a TLSTunnelManager and sets tlsEnforced = true on success.
2209+
// We verify that the state machine correctly routes failures via onError by checking the state definition.
2210+
const states = activation.machine.config.states as any
2211+
expect(states.INIT_TLS_TUNNEL_CONNECTION).toBeDefined()
2212+
expect(states.INIT_TLS_TUNNEL_CONNECTION.invoke.onError.target).toBe('FAILED')
2213+
expect(states.INIT_TLS_TUNNEL_CONNECTION.invoke.src).toBe('initializeTlsTunnelConnection')
2214+
})
2215+
})
2216+
})
21002217
})

0 commit comments

Comments
 (0)