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
transportstopped 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: 0arrives as the reason with nothing beside it.This keys off the
no_responseflag the API sets on that one case, not ondatabeing empty. A reply too short to parse, an unsupported SPDMVersion and a parse error all leavedataempty 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
transportis 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
transportdown earlier in this run, or None.
utils.py
SPDM conformance test case utilities.
- LARGE_RESPONSE = 15
The response is bigger than the
DataTransferSizethe 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
MaxSPDMmsgSizethe requester advertised in GET_CAPABILITIES, so it cannot be received at all.
- SPDM_ERROR = 127
RequestResponseCodeof 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.VersionNumberEntrysatisfies 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.contextto 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
datawith 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 raisesAttributeErroronNoneand 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 passfail_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
DataTransferSizethe 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
MaxSPDMmsgSizecannot be received at all, and ERROR(ResponseTooLarge) is the conformant answer. There is then noresponse_nameto 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_nameto 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)somain(dut)inif __name__ == '__main__'keeps working without actxparameter.