Skip to content
Open
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
8 changes: 4 additions & 4 deletions .mpsrc
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"common_name": "localhost",
"common_name": "10.72.4.39",
"port": 4433,
"country": "US",
"company": "NoCorp",
"listen_any": true,
"tls_offload": false,
"web_port": 3000,
"generate_certificates": true,
"web_admin_user": "",
"web_admin_password": "",
"web_admin_user": "standalone",
"web_admin_password": "G@ppm0ym",
"web_auth_enabled": true,
"vault_address": "http://localhost:8200",
"vault_token": "myroot",
Expand All @@ -18,7 +18,7 @@
"cert_format": "file",
"data_path": "../private/data.json",
"cert_path": "../private",
"jwt_secret": "",
"jwt_secret": "myjwtsecret",
"jwt_issuer": "9EmRJTbIiIb4bIeSsmgcWIjrR6HyETqc",
"jwt_expiration": "1440",
"cors_origin": "*",
Expand Down
17 changes: 15 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ services:
ports:
- 3000:3000
depends_on:
- 'vault'
- 'db'
db:
condition: service_healthy
vault:
condition: service_healthy
build:
context: .
dockerfile: ./Dockerfile
Expand All @@ -35,6 +37,11 @@ services:
POSTGRES_PASSWORD: admin123
volumes:
- ./data:/docker-entrypoint-initdb.d
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgresadmin -d mpsdb"]
interval: 5s
timeout: 5s
retries: 10
vault:
image: hashicorp/vault:1.21
networks:
Expand All @@ -44,8 +51,14 @@ services:
environment:
VAULT_DEV_ROOT_TOKEN_ID: myroot
VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200
SKIP_SETCAP: 'true'
cap_add:
- IPC_LOCK
healthcheck:
test: ["CMD-SHELL", "VAULT_ADDR=http://localhost:8200 vault status"]
interval: 5s
timeout: 5s
retries: 10
consul:
restart: always
image: hashicorp/consul
Expand Down
108 changes: 104 additions & 4 deletions src/amt/DeviceAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,15 +190,115 @@ export class DeviceAction {
return getResponse.Envelope
}

async getPowerCapabilities(): Promise<Common.Models.Envelope<AMT.Models.BootCapabilities>> {
logger.silly(`getPowerCapabilities ${messages.REQUEST}`)
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.info(JSON.stringify(result))
logger.silly(`getPowerCapabilities ${messages.COMPLETE}`)
logger.silly(`getBootCapabilities ${messages.COMPLETE}`)
return result.Envelope
}

// Backward-compatible alias. Prefer getBootCapabilities for new code.
async getPowerCapabilities(): Promise<Common.Models.Envelope<AMT.Models.BootCapabilities>> {
return await this.getBootCapabilities()
}

async setRPE(isEnabled: boolean): Promise<void> {
logger.silly(`setRPE ${messages.REQUEST}`)
const bootOptions = await this.getBootOptions()
const current = bootOptions.AMT_BootSettingData
;(current as any).RPE = isEnabled
await this.setBootConfiguration(current)
logger.silly(`setRPE ${messages.COMPLETE}`)
}

async sendRPE(eraseMask: number, powerType: number): Promise<void> {
logger.silly(`sendRPE ${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 RPE
const bootOptions = await this.getBootOptions()
const current = bootOptions.AMT_BootSettingData
const rpeEnabled = (current as any).RPE ?? current.RPEEnabled ?? current.PlatformErase
if (!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(`sendRPE 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
const powerStateResult = await this.getPowerState()
const currentState = powerStateResult?.PullResponse?.Items?.CIM_AssociatedPowerManagementService?.PowerState
const action = currentState === '8' ? 2 : 5

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like MPS only treats power state 8 as off:

currentState === '8' ? 2 : 5

However, CIM defines 6, 8, 12, and 13 as valid off states. Console’s rpe.go already uses isOffPowerState to cover all four, while MPS checks only 8.

Could we align MPS with Console and check all four off states?

await this.sendPowerAction(action as CIM.Types.PowerManagementService.PowerState)

logger.silly(`sendRPE ${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
44 changes: 41 additions & 3 deletions src/amt/deviceAction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,12 +382,50 @@ describe('Device Action Tests', () => {
expect(result).toEqual(chip.Envelope)
})
})
describe('power capabilities', () => {
it('should get power capabilities', async () => {

describe('boot capabilities and RPE', () => {
it('should get boot capabilities', async () => {
getSpy.mockResolvedValueOnce(bootCapabilities)
const result = await device.getPowerCapabilities()
const result = await device.getBootCapabilities()
expect(result).toEqual(bootCapabilities.Envelope)
})
it('should set RPE enabled', async () => {
getSpy.mockResolvedValueOnce({
Envelope: { Body: { AMT_BootSettingData: { ElementName: 'test', RPESupported: true, RPE: false } } }
})
sendSpy.mockResolvedValueOnce({ Envelope: { Body: {} } })
await device.setRPE(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', RPESupported: true, RPE: true } } }
})
enumerateSpy.mockResolvedValueOnce(enumerateResponse)
pullSpy.mockResolvedValueOnce(serviceAvailableToElement)
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.sendRPE(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, RPE: true } } }
})
enumerateSpy.mockResolvedValueOnce(enumerateResponse)
pullSpy.mockResolvedValueOnce(serviceAvailableToElement)
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.sendRPE(0)
expect(getSpy).toHaveBeenCalled()
expect(sendSpy).toHaveBeenCalled()
})
})
describe('alarm occurrences', () => {
it('should return null when enumerate call to getAlarmClockOccurrences fails', async () => {
Expand Down
8 changes: 8 additions & 0 deletions src/models/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,3 +227,11 @@ export interface OCRProcessResult {
HTTPSBootSupported: boolean
OCR: boolean
}

// RPE = Remote Platform Erase
export interface RPECapabilities {
secureEraseAllSSDs: boolean
tpmClear: boolean
restoreBIOSToEOM: boolean
unconfigureCSME: boolean
}
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()
]
12 changes: 8 additions & 4 deletions src/routes/amt/getAMTFeatures.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ describe('get amt features', () => {
ForcedProgressEvents: true,
IDER: true,
InstanceID: 'Intel(r) AMT:BootCapabilities 0',
SOL: true
SOL: true,
PlatformErase: 3
}
}
},
Expand All @@ -155,7 +156,8 @@ describe('get amt features', () => {
IDERBootDevice: 0,
InstanceID: 'Intel(r) AMT:BootSettingData 0',
UseIDER: false,
UseSOL: false
UseSOL: false,
RPE: true
}
}
})
Expand All @@ -174,7 +176,8 @@ describe('get amt features', () => {
httpsBootSupported: true,
winREBootSupported: true,
localPBABootSupported: false,
remoteErase: false
rpe: true,
rpeSupported: true
})
expect(mqttSpy).toHaveBeenCalledTimes(2)
})
Expand Down Expand Up @@ -270,7 +273,8 @@ describe('get amt features', () => {
httpsBootSupported: false,
winREBootSupported: false,
localPBABootSupported: false,
remoteErase: false
rpe: false,
rpeSupported: false
})
})
})
8 changes: 7 additions & 1 deletion src/routes/amt/getAMTFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ 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 bootData = OCRData.bootData?.AMT_BootSettingData
const rpe = !!(bootData?.RPE ?? bootData?.RPEEnabled ?? bootData?.PlatformErase)
const rpeSupported = rpeCaps !== 0

MqttProvider.publishEvent('success', ['AMT_GetFeatures'], messages.AMT_FEATURES_GET_SUCCESS, guid)
res
.status(200)
Expand All @@ -43,7 +48,8 @@ export async function getAMTFeatures(req: Request, res: Response): Promise<void>
httpsBootSupported: ocrProcessResult.HTTPSBootSupported,
winREBootSupported: ocrProcessResult.WinREBootSupported,
localPBABootSupported: ocrProcessResult.LocalPBABootSupported,
remoteErase: false
rpe,
rpeSupported
})
.end()
} catch (error) {
Expand Down
75 changes: 75 additions & 0 deletions src/routes/amt/getBootCapabilities.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*********************************************************************
* 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/vitest.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 { vi, type MockInstance } from 'vitest'

describe('Get Boot Capabilities', () => {
let req: any
let resSpy: any
let mqttSpy: MockInstance
let bootCapsSpy: MockInstance
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 = vi.spyOn(MqttProvider, 'publishEvent')
bootCapsSpy = vi.spyOn(device, 'getBootCapabilities')
})

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

await getBootCapabilities(req, resSpy)
expect(resSpy.status).toHaveBeenCalledWith(200)
expect(resSpy.json).toHaveBeenCalledWith({
secureEraseAllSSDs: true,
tpmClear: true,
restoreBIOSToEOM: false,
unconfigureCSME: true
})
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()
})
})
Loading
Loading