SPDM

transport_health.py

Stop running a suite over an SPDM transport that has stopped answering.

SE-2915 / SBSUPPORT-10184. A drive whose PCIe DOE mailbox wedges mid-suite – seen when the third GET_CERTIFICATE portion of case 6.13 went unanswered – answers nothing over that transport afterwards, and does not recover across the setup and teardown between scripts. The 51 DOE cases that followed each spent about ten seconds proving that again, so one fault was reported as 52 separate failures over 9.8 minutes.

The PCIe capability register cannot see it: setpci ECAP_DOE+2.w reads the capability back as present throughout, because the mailbox is up and silent rather than gone. Only an SPDM exchange tells the two apart, so the first script to find the transport silent records that here, and the ones behind it re-check with a single GET_VERSION instead of running a whole case.

The record is per target and per transport. SMBUS and VDM keep running: in the run this came from they passed the same cases the DOE transport could not.

record_transport_down(transport: str, detail: str) → None

Record that transport stopped answering during the running script.

response_missing(result) → bool

True if the responder sent nothing back at all.

An SPDM ERROR is an answer – the responder is there and talking – and comes back parsed, with data. A silent transport looks different: DOE error, mailbox busy / spdm_rsp_len: 0 arrives as the reason with nothing beside it.

This keys off the no_response flag the API sets on that one case, not on data being empty. A reply too short to parse, an unsupported SPDMVersion and a parse error all leave data empty too, and reading those as a dead transport turns every later case on that transport from Failed into Skipped – hiding the responder’s real defect behind a wedge that is not there.

skip_if_transport_down(dut, transport: str) → None

Skip this case if transport is still silent from an earlier failure.

Re-checking rather than trusting the record is what keeps a stale one from doing harm – a later run against a drive that has since been power cycled, or the same drive once the mailbox clears, answers and carries on.

transport_answers(dut, transport: str) → bool

True if the responder answers a GET_VERSION over transport.

GET_VERSION needs no negotiated state, so it is both the cheapest exchange available and the one every wedged case died on anyway: a silent DOE mailbox returns it empty in about a second, against the ten a case costs. The transport is put back afterwards because the validator-driven scripts reach here without having set one.

transport_down(transport: str)

What took transport down earlier in this run, or None.

utils.py

SPDM conformance test case utilities.

LARGE_RESPONSE = 15

The response is bigger than the DataTransferSize the requester advertised, so it has to be collected with CHUNK_GET. Carries a Handle.

NO_PENDING_REQUESTS = frozenset({69})

Defined response when the responder has no encapsulated request outstanding.

OUT_OF_FLOW_ERRORS = frozenset({1, 4, 5, 11})

ErrorCodes a conformant responder may return when a request that belongs to a secure session, or to a transfer already in progress, is issued on its own. The conformance suite has no way to establish a secure session, so these are a consequence of how the case is driven, not a defect in the responder.

RESPONSE_TOO_LARGE = 13

The response is bigger than the MaxSPDMmsgSize the requester advertised in GET_CAPABILITIES, so it cannot be received at all.

SPDM_ERROR = 127

RequestResponseCode of an SPDM ERROR response.

class TestSetup(steps=None)

Composable SPDM TestSetup step pipeline.

get_capabilities(*, required_capabilities: dict[str, int] | None = None, **kwargs: Any) → TestSetup

Run GET_CAPABILITIES. Skip test if provided capabilities are not met.

get_version(required_spdm_version: int | None = None) → TestSetup

Run GET_VERSION and select a negotiated version.

The test is skipped when no entry in VERSION.VersionNumberEntry satisfies the requirement.

negotiate_algorithms(**kwargs) → TestSetup

Run NEGOTIATE_ALGORITHMS using the supplied kwargs.

run(dut: sanblaze.script.dut.DUT) → dict[str, Any]

Execute SPDM pipeline. Returns the accumulated context.

step(action: Callable[[sanblaze.script.dut.DUT], None]) → TestSetup

Add step to SPDM pipeline.

Steps may write into self.context to expose values to the main function (e.g. the result of GET_CAPABILITIES).

UNSUPPORTED_REQUEST = frozenset({7})

Some responders reject a session-only request with UnsupportedRequest rather than SessionRequired. Accepted where the request is issued out of session.

base_hash(BaseHashSel: int, data: bytes) → bytes | None

Hash data with the negotiated BaseHashSel algorithm.

Returns None when no bit is set or this host’s hashlib cannot compute the algorithm (SM3 depends on the OpenSSL build).

certificate_slot_digests(DIGESTS: dict[str, Any], hash_size: int, multi_key_conn_rsp: bool) → dict[int, bytes]

Map each SlotID in a DIGESTS response to its digest.

Digest[] holds one digest per bit set in Param2, lowest SlotID first. With MULTI_KEY_CONN_RSP a slot whose CertificateInfo.CertModel is 0 holds no certificate chain and is left out, which is what the DMTF validator’s spdm_test_filter_valid_slot_mask() does. Read from the raw message because the DIGESTS template only decodes the multi-key fields on request.

common_req_alg_struct(spdm_version: int, *, include_pqc: bool = False, **kwargs) → list

Generate ReqAlgStruct with all requester supported algorithms set, based on the SPDM version

common_spdm_test(setup: ~utils.TestSetup = <utils.TestSetup object>, teardown: ~collections.abc.Callable[[sanblaze.script.dut.DUT], None] | None = None)

Run SPDM TestSetup and TestTeardown

doe_capability_version(dut) → int

Return the device’s PCIe DOE capability version, or skip if it has none.

The DOE transport needs the PCIe Data Object Exchange extended capability. Without it the doe binary exits 9 with “failed to find PCIe ‘Data Object Exchange’ capability”, which is the device not being a candidate for the case rather than a test failure. SBSUPPORT-10273.

get_capabilities_common_params() → dict

Default GET_CAPABILITIES requester flags from SPDM-Responder-Validator.

See spdm_test_case_algorithms_setup_version_capabilities() in spdm_responder_test_3_algorithms.c

is_large_response(result: dict[str, Any]) → bool

True when the responder answered ERROR(LargeResponse).

multi_key_conn_rsp(MULTI_KEY_CAP: int, ResponderMultiKeyConn: int) → bool | None

Table 38 — MULTI_KEY_CONN_RSP value calculation

negotiate_algorithms_common_params(spdm_version: int, *, include_pqc: bool | None = None) → dict

Generate common NEGOTIATE_ALGORITHIMS parameters based on SPDM version

Based on: SPDM-Responder-Validator/library/spdm_responder_conformance_test_lib/spdm_responder_test_3_algorithms.c

NEGOTIATE_ALGORITHMS {SPDMVersion=NegotiatedVersion, …}

Code=E3h(NEGOTIATE_ALGORITHMS) NumAlgStruct=4 (NumAlgStruct=6 for SPDM 1.4+ with PQC) Length=48 (Length=56 for SPDM 1.4+ with PQC) MeasureSpec=1h OtherParamsSupport=2h BaseAsymAlgo=FFFh BaseHashAlgo=7Fh ExtAsymCount=0 ExtHashCount=0 ReqAlgStruct=02207F00,03200F00,0420FF0F,05200100 (+0620FF7F00,07200700 for SPDM 1.4+ with PQC)

require_response(result: dict[str, Any], request_name: str, response_name: str = 'response') → Any

Return the parsed response, failing the case when there is none.

A responder that answers nothing at all leaves result['data'] unset, so every assertion that reads the message raises AttributeError on None and the case ends in Warning with a traceback instead of a verdict. Fail it with the transport’s reason instead.

Use this where the case does not already stop on a failed check_result – typically the ERROR-expecting cases, which pass fail_type='notify' because the request under test is meant to fail.

Parameters:
  • result (dict) – The dictionary returned by the SPDM API call.

  • request_name (str) – Request that was sent, e.g. 'GET_CSR'. Used in the log message.

  • response_name (str) – What the case expected back, e.g. 'ERROR(VersionMismatch)'.

Return type:

The parsed response message.

resolve_large_response(dut: sanblaze.script.dut.DUT, result: dict[str, Any], request_name: str, response_name: str, structure: str) → dict[str, Any]

Return the response, collecting it with CHUNK_GET when it is too large.

A response bigger than the DataTransferSize the requester advertised is not returned directly. The responder answers ERROR(LargeResponse) with a Handle, and the response has to be fetched with CHUNK_GET; this helper does that and hands back the reassembled message so the case can validate it as if it had arrived in one piece.

A response bigger than the advertised MaxSPDMmsgSize cannot be received at all, and ERROR(ResponseTooLarge) is the conformant answer. There is then no response_name to validate, so the case is Skipped rather than Failed. Certificate chains signed with a post-quantum algorithm are the case that reaches this in practice.

Anything else is returned untouched, for the caller to check as usual.

Parameters:
  • result (dict) – The dictionary returned by the SPDM API call.

  • request_name (str) – Request that was sent, e.g. 'SLOT_MANAGEMENT'. For log messages.

  • response_name (str) – Response the case validates, e.g. 'SLOT_MANAGEMENT_RESP'.

  • structure (str) – Name of the response template the reassembled bytes are parsed with.

skip_on_expected_error(result: dict[str, Any], request_name: str, response_name: str, expected_errors, note: str | None = None) → None

Skip the case when the responder answers a request with a legal ERROR.

Several SPDM messages are only meaningful inside a secure session or a transfer that is already under way. The conformance suite issues them on their own, so a conformant responder is entitled to answer ERROR instead of the response under test. There is then no response_name to validate and the case is Skipped; any other failure is still a failure.

Parameters:
  • result (dict) – The dictionary returned by the SPDM API call.

  • request_name (str) – Request that was sent, e.g. 'CHUNK_GET'. Used in log messages.

  • response_name (str) – Response the case validates, e.g. 'CHUNK_RESPONSE'.

  • expected_errors (set of int) – ErrorCodes accepted for this case, e.g. OUT_OF_FLOW_ERRORS.

  • note (str) – Optional extra sentence logged with the skip message.

spdm_test_ctx(dut: sanblaze.script.dut.DUT) → dict[str, Any]

Return TestSetup context from @common_spdm_test(...).

Scripts can use this with def main(dut) so main(dut) in if __name__ == '__main__' keeps working without a ctx parameter.