Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/api-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ jobs:
egress-policy: audit

- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- run: docker build -f Dockerfile -t mps:${GITHUB_SHA} .
- run: docker build -f Dockerfile -t mps:${GITHUB_SHA} -t mps .
- run: docker compose up -d
- run: sleep 30
- run: timeout 120 bash -c 'until curl -sf http://localhost:3000/api/v1/health; do echo "waiting for MPS..."; sleep 5; done'
- run: docker run --network=host -v /home/runner/work/mps/mps/src/test/collections/:/collections -v /home/runner/work/mps/mps/src/test/results/:/results postman/newman run /collections/MPS.postman_collection.json -e /collections/MPS.postman_environment.json --insecure --reporters cli,json,junit --reporter-json-export /results/mps_api_results.json --reporter-junit-export /results/mps_api_results_junit.xml
- run: docker run --network=host -v /home/runner/work/mps/mps/src/test/collections/:/collections -v /home/runner/work/mps/mps/src/test/results/:/results postman/newman run /collections/mps_security_api_test_postman_collection.json -e /collections/MPS.postman_environment.json -d /collections/data/mps_security_api_test_data.json --insecure --reporters cli,json,junit --reporter-json-export /results/mps_api_security_results.json --reporter-junit-export /results/mps_api_security_results_junit.xml
- name: Dump docker logs on
Expand Down
100 changes: 100 additions & 0 deletions src/amt/DeviceAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,106 @@ export class DeviceAction {
return result.Envelope
}

async getBootCapabilities(): Promise<Common.Models.Envelope<AMT.Models.BootCapabilities>> {
logger.silly(`getBootCapabilities ${messages.REQUEST}`)
const xmlRequestBody = this.amt.BootCapabilities.Get()
const result = await this.ciraHandler.Get<AMT.Models.BootCapabilities>(this.ciraSocket, xmlRequestBody)
logger.silly(`getBootCapabilities ${messages.COMPLETE}`)
return result.Envelope
}

async setRPEEnabled(enabled: boolean): Promise<void> {
logger.silly(`setRPEEnabled ${messages.REQUEST}`)
const bootOptions = await this.getBootOptions()
const current = bootOptions.AMT_BootSettingData
current.PlatformErase = enabled
await this.setBootConfiguration(current)
logger.silly(`setRPEEnabled ${messages.COMPLETE}`)
}

async sendRemoteErase(eraseMask: number): Promise<void> {
logger.silly(`sendRemoteErase ${messages.REQUEST}`)

// CSME sentinel bit: 0x10000 maps to ConfigurationDataReset, not a hardware erase target
const CSME_BIT = 0x10000
const csmeRequested = (eraseMask & CSME_BIT) !== 0
const hwMask = eraseMask & ~CSME_BIT // strip the CSME bit for hardware TLV

// Step 1: GET current boot settings and verify RPEEnabled
const bootOptions = await this.getBootOptions()
const current = bootOptions.AMT_BootSettingData
if (!current.RPEEnabled) {
throw new Error('RPE is not enabled on this device')
}

// Step 1a: Clear boot source override (CSME path only)
if (csmeRequested) {
await this.changeBootOrder()
}

// Step 1b: Switch firmware to RPE mode BEFORE the PUT
// Required when boot service is in OCR mode (32769); must precede PUT
const xmlRpeMode = this.cim.BootService.RequestStateChange(32770)
const rscResult = await this.ciraHandler.Send(this.ciraSocket, xmlRpeMode)
if (rscResult?.Envelope?.Body?.RequestStateChange_OUTPUT?.ReturnValue !== 0) {
logger.error(`sendRemoteErase RequestStateChange(32770) failed: ${JSON.stringify(rscResult?.Envelope?.Body)}`)
}

// Step 2: Build minimal PUT body — only writable fields, no read-only fields.
// Read-only fields (BIOSLastStatus, BootguardStatus, RPEEnabled, SecureBootControlEnabled,
// UEFIHTTPSBootEnabled, UEFILocalPBABootEnabled, WinREBootEnabled, OptionsCleared)
// cause InvalidRepresentation if included. Use 'Uefi' (not 'UEFI') to match AMT XML element names.
const putBody: any = {
ElementName: current.ElementName,
InstanceID: current.InstanceID,
OwningEntity: current.OwningEntity,
BIOSPause: current.BIOSPause,
BIOSSetup: current.BIOSSetup,
BootMediaIndex: current.BootMediaIndex,
ConfigurationDataReset: csmeRequested,
EnforceSecureBoot: current.EnforceSecureBoot,
FirmwareVerbosity: current.FirmwareVerbosity,
ForcedProgressEvents: current.ForcedProgressEvents,
IDERBootDevice: current.IDERBootDevice,
LockKeyboard: current.LockKeyboard,
LockPowerButton: current.LockPowerButton,
LockResetButton: current.LockResetButton,
LockSleepButton: current.LockSleepButton,
PlatformErase: hwMask !== 0,
RSEPassword: current.RSEPassword,
ReflashBIOS: current.ReflashBIOS,
SecureErase: current.SecureErase,
UseIDER: current.UseIDER,
UseSOL: current.UseSOL,
UseSafeMode: current.UseSafeMode,
UserPasswordBypass: current.UserPasswordBypass,
}

if (hwMask !== 0) {
const buf = Buffer.alloc(12)
buf.writeUInt16LE(0x8086, 0) // Intel vendor prefix
buf.writeUInt16LE(1, 2) // ParameterTypeID = 1
buf.writeUInt32LE(4, 4) // value length = 4 bytes
buf.writeUInt32LE(hwMask, 8) // device bitmask
putBody.UefiBootParametersArray = buf.toString('base64')
putBody.UefiBootNumberOfParams = 1
}

const xmlPut = this.amt.BootSettingData.Put(putBody as AMT.Models.BootSettingData)
const putResult = await this.ciraHandler.Send(this.ciraSocket, xmlPut)
if (putResult?.Envelope?.Body?.Fault) {
throw new Error(`BootSettingData PUT failed: ${JSON.stringify(putResult.Envelope.Body.Fault)}`)
}

// Step 4: Activate boot configuration
await this.forceBootMode(1)

// Step 5: Power Cycle Off Hard — S5→S0 required; warm reset keeps ME power rails active
await this.sendPowerAction(5)

logger.silly(`sendRemoteErase ${messages.COMPLETE}`)
}

async requestUserConsentCode(): Promise<Common.Models.Envelope<IPS.Models.StartOptIn_OUTPUT>> {
logger.silly(`requestUserConsentCode ${messages.REQUEST}`)
const xmlRequestBody = this.ips.OptInService.StartOptIn()
Expand Down
34 changes: 34 additions & 0 deletions src/amt/deviceAction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,40 @@ describe('Device Action Tests', () => {
expect(result).toEqual(bootCapabilities.Envelope)
})
})
describe('boot capabilities and RPE', () => {
it('should get boot capabilities', async () => {
getSpy.mockResolvedValueOnce(bootCapabilities)
const result = await device.getBootCapabilities()
expect(result).toEqual(bootCapabilities.Envelope)
})
it('should set RPE enabled', async () => {
getSpy.mockResolvedValueOnce({ Envelope: { Body: { AMT_BootSettingData: { ElementName: 'test', PlatformErase: false } } } })
sendSpy.mockResolvedValueOnce({ Envelope: { Body: {} } })
await device.setRPEEnabled(true)
expect(getSpy).toHaveBeenCalled()
expect(sendSpy).toHaveBeenCalled()
})
it('should send remote erase with non-zero mask', async () => {
getSpy.mockResolvedValueOnce({ Envelope: { Body: { AMT_BootSettingData: { ElementName: 'test', PlatformErase: false, RPEEnabled: true } } } })
getSpy.mockResolvedValueOnce({ Envelope: { Body: { RequestPowerStateChange_OUTPUT: { ReturnValue: 0 } } } })
sendSpy.mockResolvedValueOnce({ Envelope: { Body: {} } }) // RequestStateChange(32770)
sendSpy.mockResolvedValueOnce({ Envelope: { Body: {} } }) // Put(putBody)
sendSpy.mockResolvedValueOnce({ Envelope: { Body: {} } }) // forceBootMode(1)
await device.sendRemoteErase(3)
expect(getSpy).toHaveBeenCalled()
expect(sendSpy).toHaveBeenCalled()
})
it('should send remote erase with zero mask sets PlatformErase to false', async () => {
getSpy.mockResolvedValueOnce({ Envelope: { Body: { AMT_BootSettingData: { ElementName: 'test', PlatformErase: true, RPEEnabled: true } } } })
getSpy.mockResolvedValueOnce({ Envelope: { Body: { RequestPowerStateChange_OUTPUT: { ReturnValue: 0 } } } })
sendSpy.mockResolvedValueOnce({ Envelope: { Body: {} } }) // RequestStateChange(32770)
sendSpy.mockResolvedValueOnce({ Envelope: { Body: {} } }) // Put(putBody)
sendSpy.mockResolvedValueOnce({ Envelope: { Body: {} } }) // forceBootMode(1)
await device.sendRemoteErase(0)
expect(getSpy).toHaveBeenCalled()
expect(sendSpy).toHaveBeenCalled()
})
})
describe('alarm occurrences', () => {
it('should return null when enumerate call to getAlarmClockOccurrences fails', async () => {
enumerateSpy.mockResolvedValueOnce(null)
Expand Down
3 changes: 2 additions & 1 deletion src/routes/amt/amtFeatureValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ export const amtFeaturesValidator = (): any => [
check('enableSOL').isBoolean().toBoolean(),
check('enableIDER').isBoolean().toBoolean(),
check('enableKVM').isBoolean().toBoolean(),
check('ocr').optional().isBoolean().toBoolean()
check('ocr').optional().isBoolean().toBoolean(),
check('platformEraseEnabled').optional().isBoolean().toBoolean()
]
14 changes: 10 additions & 4 deletions src/routes/amt/getAMTFeatures.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ describe('get amt features', () => {
ForcedProgressEvents: true,
IDER: true,
InstanceID: 'Intel(r) AMT:BootCapabilities 0',
SOL: true
SOL: true,
PlatformErase: 3
}
}
},
Expand All @@ -156,7 +157,8 @@ describe('get amt features', () => {
IDERBootDevice: 0,
InstanceID: 'Intel(r) AMT:BootSettingData 0',
UseIDER: false,
UseSOL: false
UseSOL: false,
PlatformErase: true
}
}
})
Expand All @@ -175,7 +177,9 @@ describe('get amt features', () => {
httpsBootSupported: true,
winREBootSupported: true,
localPBABootSupported: false,
remoteErase: false
rpeEnabled: true,
rpeSupported: true,
rpeCaps: 3
})
expect(mqttSpy).toHaveBeenCalledTimes(2)
})
Expand Down Expand Up @@ -271,7 +275,9 @@ describe('get amt features', () => {
httpsBootSupported: false,
winREBootSupported: false,
localPBABootSupported: false,
remoteErase: false
rpeEnabled: false,
rpeSupported: false,
rpeCaps: 0
})
})
})
10 changes: 8 additions & 2 deletions src/routes/amt/getAMTFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ export async function getAMTFeatures(req: Request, res: Response): Promise<void>
const userConsent = Object.keys(UserConsentOptions).find((key) => UserConsentOptions[key] === value)
const ocrProcessResult = processOCRData(OCRData)

const rpeCaps = OCRData.capabilities?.Body?.AMT_BootCapabilities?.PlatformErase ?? 0
const rpeEnabled = !!(OCRData.bootData?.AMT_BootSettingData?.PlatformErase)
const rpeSupported = rpeCaps !== 0

MqttProvider.publishEvent('success', ['AMT_GetFeatures'], messages.AMT_FEATURES_GET_SUCCESS, guid)
res
res
.status(200)
.json({
userConsent,
Expand All @@ -43,7 +47,9 @@ export async function getAMTFeatures(req: Request, res: Response): Promise<void>
httpsBootSupported: ocrProcessResult.HTTPSBootSupported,
winREBootSupported: ocrProcessResult.WinREBootSupported,
localPBABootSupported: ocrProcessResult.LocalPBABootSupported,
remoteErase: false
rpeEnabled,
rpeSupported,
rpeCaps
})
.end()
} catch (error) {
Expand Down
65 changes: 65 additions & 0 deletions src/routes/amt/getBootCapabilities.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*********************************************************************
* Copyright (c) Intel Corporation 2022
* SPDX-License-Identifier: Apache-2.0
**********************************************************************/

import { ErrorResponse } from '../../utils/amtHelper.js'
import { MqttProvider } from '../../utils/MqttProvider.js'
import { getBootCapabilities } from './getBootCapabilities.js'
import { createSpyObj } from '../../test/helper/jest.js'
import { DeviceAction } from '../../amt/DeviceAction.js'
import { CIRAHandler } from '../../amt/CIRAHandler.js'
import { HttpHandler } from '../../amt/HttpHandler.js'
import { messages } from '../../logging/index.js'
import { type Spied, spyOn } from 'jest-mock'

describe('Get Boot Capabilities', () => {
let req: any
let resSpy: any
let mqttSpy: Spied<any>
let bootCapsSpy: Spied<any>
let device: DeviceAction

beforeEach(() => {
const handler = new CIRAHandler(new HttpHandler(), 'admin', 'P@ssw0rd')
device = new DeviceAction(handler, null)
req = {
params: { guid: '4c4c4544-004b-4210-8033-b6c04f504633' },
deviceAction: device
}
resSpy = createSpyObj('Response', ['status', 'json', 'end', 'send'])
resSpy.status.mockReturnThis()
resSpy.json.mockReturnThis()
resSpy.send.mockReturnThis()

mqttSpy = spyOn(MqttProvider, 'publishEvent')
bootCapsSpy = spyOn(device, 'getBootCapabilities')
})

it('should return boot capabilities', async () => {
const bootCaps = {
IDER: true,
SOL: true,
BIOSSetup: true,
PlatformErase: 3
}
bootCapsSpy.mockResolvedValue({
Body: { AMT_BootCapabilities: bootCaps }
})

await getBootCapabilities(req, resSpy)
expect(resSpy.status).toHaveBeenCalledWith(200)
expect(resSpy.json).toHaveBeenCalledWith(bootCaps)
expect(resSpy.end).toHaveBeenCalled()
expect(mqttSpy).toHaveBeenCalledTimes(2)
})

it('should return 500 on error', async () => {
bootCapsSpy.mockRejectedValue(new Error('AMT error'))

await getBootCapabilities(req, resSpy)
expect(resSpy.status).toHaveBeenCalledWith(500)
expect(resSpy.json).toHaveBeenCalledWith(ErrorResponse(500, messages.POWER_CAPABILITIES_EXCEPTION))
expect(resSpy.end).toHaveBeenCalled()
})
})
27 changes: 27 additions & 0 deletions src/routes/amt/getBootCapabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*********************************************************************
* Copyright (c) Intel Corporation 2022
* SPDX-License-Identifier: Apache-2.0
**********************************************************************/

import { type Response, type Request } from 'express'
import { logger, messages } from '../../logging/index.js'
import { ErrorResponse } from '../../utils/amtHelper.js'
import { MqttProvider } from '../../utils/MqttProvider.js'

export async function getBootCapabilities(req: Request, res: Response): Promise<void> {
try {
const guid: string = req.params.guid

MqttProvider.publishEvent('request', ['AMT_BootCapabilities'], messages.POWER_CAPABILITIES_REQUESTED, guid)

const result = await req.deviceAction.getBootCapabilities()
const capabilities = result.Body?.AMT_BootCapabilities

MqttProvider.publishEvent('success', ['AMT_BootCapabilities'], messages.POWER_CAPABILITIES_SUCCESS, guid)
res.status(200).json(capabilities).end()
} catch (error) {
logger.error(`${messages.POWER_CAPABILITIES_EXCEPTION} : ${error}`)
MqttProvider.publishEvent('fail', ['AMT_BootCapabilities'], messages.INTERNAL_SERVICE_ERROR)
res.status(500).json(ErrorResponse(500, messages.POWER_CAPABILITIES_EXCEPTION)).end()
}
}
6 changes: 6 additions & 0 deletions src/routes/amt/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ import { getScreenSettingData } from './kvm/get.js'
import { setKVMRedirectionSettingData } from './kvm/set.js'
import { setLinkPreference } from './setLinkPreference.js'
import { linkPreferenceValidator } from './linkPreferenceValidator.js'
import { getBootCapabilities } from './getBootCapabilities.js'
import { setRPEEnabled } from './setRPEEnabled.js'
import { sendRemoteErase } from './sendRemoteErase.js'

const amtRouter: Router = Router()

Expand All @@ -53,6 +56,9 @@ amtRouter.get('/power/capabilities/:guid', ciraMiddleware, powerCapabilities)
amtRouter.get('/power/state/:guid', ciraMiddleware, powerState)
amtRouter.get('/features/:guid', ciraMiddleware, getAMTFeatures)
amtRouter.post('/features/:guid', amtFeaturesValidator(), validateMiddleware, ciraMiddleware, setAMTFeatures)
amtRouter.get('/boot/capabilities/:guid', ciraMiddleware, getBootCapabilities)
amtRouter.post('/boot/rpe/:guid', ciraMiddleware, setRPEEnabled)
amtRouter.post('/remoteErase/:guid', ciraMiddleware, sendRemoteErase)
amtRouter.get('/version/:guid', ciraMiddleware, version)
amtRouter.delete('/deactivate/:guid', ciraMiddleware, deactivate)
amtRouter.get('/power/bootSources/:guid', ciraMiddleware, bootSources)
Expand Down
Loading
Loading