Intro Link to heading
IDE is part of pcie starting PCIe and CXL. The transport channel for SPDM(the protocol used for IDE) is Data Object Exchange (DOE). Although DOE is supported already as an optional mailbox communication protocol, IDE/SPDM is not supported in mainline kernel yet.
IDE is based on SPDM (CMA defines how SPDM applies to PCIE) and IDE defines the vendor specific messages after SPDM standard authentication protocol.
I found this work-in-progress IDE implementation on github. Although it is still in the early stages, but it was interesting to read.
PCIE probe and CMA Link to heading
In probe.c
, pci_init_capabilities
calls pci_cma_init
to initialize the CMA/IDE.
static void pci_init_capabilities(struct pci_dev *dev)
{
...
pci_cma_init(dev); /* Component Measurement & Auth */
In pci_cma_init
, The DOE mailbox initialization is done with pci_find_doe_mailbox
. Then spdm_authenticate
is called to started the first stages of SPDM protocol.
void pci_cma_init(struct pci_dev *pdev)
{
...
...
doe = pci_find_doe_mailbox(pdev, PCI_VENDOR_ID_PCI_SIG,
PCI_DOE_PROTOCOL_CMA);
if (!doe)
return;
pdev->spdm_state = spdm_create(&pdev->dev, spdm_doe_transport, doe,
PCI_DOE_MAX_PAYLOAD, cma_keyring);
rc = spdm_authenticate(pdev->spdm_state);
}
Initialization Link to heading
In this section, I am going through the initialization calls in pci_cma_ini
.
DOE init Link to heading
pci_find_doe_mailbo
is is part of DOE kernel. So, It’s not new.
struct pci_doe_mb *pci_find_doe_mailbox(struct pci_dev *pdev, u16 vendor,
u8 type)
{
struct pci_doe_mb *doe_mb;
unsigned long index;
xa_for_each(&pdev->doe_mbs, index, doe_mb)
if (pci_doe_supports_prot(doe_mb, vendor, type))
return doe_mb;
return NULL;
}
SPDM init Link to heading
spdm_create
mallocs the state structure and initialize the fields passed from CMA init. Nothing major here.
struct spdm_state *spdm_create(struct device *dev, spdm_transport *transport,
void *transport_priv, u32 transport_sz,
struct key *keyring)
{
struct spdm_state *spdm_state = kzalloc(sizeof(*spdm_state), GFP_KERNEL);
if (!spdm_state)
return NULL;
spdm_state->dev = dev;
spdm_state->transport = transport;
spdm_state->transport_priv = transport_priv;
spdm_state->transport_sz = transport_sz;
spdm_state->root_keyring = keyring;
return spdm_state;
}
SPDM Authentication Link to heading
This is the big bulk of authentication logic for SPDM protocol, In spdm_authenticate
, The basic steps authentication
- Version : Start of protocol. The responder send back the version.
- capabilities
- negotiate_algorithms : The requester and responder agrees on the hash and sign algorithm.
Then it goes the basic steps for IDE session establishment.
- digest: Also return number of slots on the device (0-7)
- certificate: For each slot, The certificate is fetched from the slot
- challenge: In this step, After the whole digest is calculated, the signature is verified.
int spdm_authenticate(struct spdm_state *spdm_state)
{
u8 slot;
int rc;
rc = spdm_get_version(spdm_state);
rc = spdm_get_capabilities(spdm_state);
rc = spdm_negotiate_algs(spdm_state);
rc = spdm_get_digests(spdm_state);
for_each_set_bit(slot, &spdm_state->slot_mask, SPDM_SLOTS) {
rc = spdm_get_certificate(spdm_state, slot);
if (rc == 0)
break; /* success */
if ((rc != -ENOKEY && rc != -EKEYREJECTED) ||
slot == SPDM_SLOTS - 1)
return rc; /* try next slot only on signature error */
}
rc = spdm_challenge(spdm_state);
}
Get version Link to heading
Here is the start of protocol, where requester and responder sends version between each other.
static const struct spdm_get_version_req spdm_get_version_req = {
.version = 0x10,
.code = SPDM_GET_VERSION,
};
static int spdm_get_version(struct spdm_state *spdm_state)
{
struct spdm_get_version_rsp *rsp;
size_t *rsp_sz = &spdm_state->get_version_rsp_sz;
u8 version = SPDM_MIN_VER;
bool foundver = false;
int numversions = 2;
int rc, length, i;
*rsp_sz = struct_size(rsp, version_number_entries, numversions);
rsp = spdm_state->get_version_rsp = kzalloc(*rsp_sz, GFP_KERNEL);
/* Bypass spdm_exchange() to be able to set version = 0x10 */
rc = __spdm_exchange(spdm_state, &spdm_get_version_req,
sizeof(spdm_get_version_req), rsp, *rsp_sz);
}
This is the first time to see __spdm_exchange
, So, I will quickly go through it. but the just is that it’s just call the transport channel to send the message (in this case is DOE). There is also spdm_exchange
which is wrapper around __spdm_exchange
.
static int __spdm_exchange(struct spdm_state *spdm_state,
const void *req, size_t req_sz,
void *rsp, size_t rsp_sz)
{
const struct spdm_header *request = req;
struct spdm_header *response = rsp;
int length;
int rc;
rc = spdm_state->transport(spdm_state->transport_priv, spdm_state->dev,
req, req_sz, rsp, rsp_sz);
return length;
}
static int spdm_exchange(struct spdm_state *spdm_state,
void *req, size_t req_sz, void *rsp, size_t rsp_sz)
{
struct spdm_header *req_header = req;
req_header->version = spdm_state->version;
return __spdm_exchange(spdm_state, req, req_sz, rsp, rsp_sz);
}
In spdm_doe_transport
, DOE is called with the messages and return rc
.
static int spdm_doe_transport(void *priv, struct device *dev,
const void *request, size_t request_sz,
void *response, size_t response_sz)
{
struct pci_doe_mb *doe = priv;
int rc;
rc = pci_doe(doe, PCI_VENDOR_ID_PCI_SIG, PCI_DOE_PROTOCOL_CMA,
request, request_sz, response, response_sz);
return rc;
}
spdm_get_capabilities Link to heading
After the version, GET_CAPABILITIES
is sent to responder and responder send back what it supports.
static int spdm_get_capabilities(struct spdm_state *spdm_state)
{
struct spdm_get_capabilities_reqrsp *req = &spdm_state->get_caps_req;
struct spdm_get_capabilities_reqrsp *rsp = &spdm_state->get_caps_rsp;
size_t *req_sz = &spdm_state->get_caps_req_sz;
size_t *rsp_sz = &spdm_state->get_caps_rsp_sz;
int rc, length;
req->code = SPDM_GET_CAPABILITIES;
req->ctexponent = SPDM_CTEXPONENT;
req->flags = cpu_to_le32(SPDM_CAPS);
if (spdm_state->version >= 0x12) {
req->data_transfer_size = cpu_to_le32(spdm_state->transport_sz),
req->max_spdm_msg_size = cpu_to_le32(UINT_MAX),
*req_sz = sizeof(*req);
*rsp_sz = sizeof(*rsp);
} else {
*req_sz = offsetof(typeof(*req), data_transfer_size);
*rsp_sz = offsetof(typeof(*rsp), data_transfer_size);
}
rc = spdm_exchange(spdm_state, req, *req_sz, rsp, *rsp_sz);
...
...
spdm_state->responder_caps = le32_to_cpu(rsp->flags);
...
}
spdm_negotiate_algs Link to heading
This is very important step, As this where both sides publish the supported algorithms and they both agree on these algorithms.
static int spdm_negotiate_algs(struct spdm_state *spdm_state)
{
req = kzalloc(req_sz, GFP_KERNEL);
req->code = SPDM_NEGOTIATE_ALGS;
req->measurement_specification = SPDM_MEAS_SPEC_DMTF;
req->base_asym_algo = cpu_to_le32(SPDM_ASYM_ALGOS);
req->base_hash_algo = cpu_to_le32(SPDM_HASH_ALGOS);
if (spdm_state->version >= 0x12)
req->other_params_support = SPDM_OPAQUE_DATA_FMT_GENERAL;
req_alg_struct = (struct spdm_req_alg_struct *)(req + 1);
if (spdm_state->responder_caps & SPDM_KEY_EX_CAP) {
req_alg_struct[i++] = (struct spdm_req_alg_struct) {
.alg_type = SPDM_REQ_ALG_STRUCT_DHE,
.alg_count = 0x20,
.alg_supported = cpu_to_le16(SPDM_DHE_ALGOS),
};
req_alg_struct[i++] = (struct spdm_req_alg_struct) {
.alg_type = SPDM_REQ_ALG_STRUCT_AEAD,
.alg_count = 0x20,
.alg_supported = cpu_to_le16(SPDM_AEAD_ALGOS),
};
}
if (spdm_state->responder_caps & SPDM_MUT_AUTH_CAP)
req_alg_struct[i++] = (struct spdm_req_alg_struct) {
.alg_type = SPDM_REQ_ALG_STRUCT_REQ_BASE_ASYM_ALG,
.alg_count = 0x20,
.alg_supported = cpu_to_le16(SPDM_ASYM_ALGOS),
};
if (spdm_state->responder_caps & SPDM_KEY_EX_CAP)
req_alg_struct[i++] = (struct spdm_req_alg_struct) {
.alg_type = SPDM_REQ_ALG_STRUCT_KEY_SCHEDULE,
.alg_count = 0x20,
.alg_supported = cpu_to_le16(SPDM_KEY_SCHEDULE_SPDM),
};
req->param1 = i;
req->length = cpu_to_le16(sizeof(*req) + i * sizeof(*req_alg_struct));
rsp = kzalloc(rsp_sz, GFP_KERNEL);
rc = spdm_exchange(spdm_state, req, req_sz, rsp, rsp_sz);
spdm_state->measurement_hash_alg =
le32_to_cpu(rsp->measurement_hash_algo);
spdm_state->base_asym_alg =
le32_to_cpu(rsp->base_asym_sel) & SPDM_ASYM_ALGOS;
spdm_state->base_hash_alg =
le32_to_cpu(rsp->base_hash_sel) & SPDM_HASH_ALGOS;
switch (spdm_state->base_asym_alg) {
case SPDM_ASYM_RSASSA_3072:
spdm_state->s = 384;
break;
case SPDM_ASYM_ECDSA_ECC_NIST_P256:
spdm_state->s = 64;
break;
case SPDM_ASYM_ECDSA_ECC_NIST_P384:
spdm_state->s = 96;
break;
default:
dev_err(spdm_state->dev, "Unknown asym algorithm\n");
rc = -EINVAL;
goto err_free_rsp;
}
rc = spdm_start_digest(spdm_state, req, req_sz, rsp, rsp_sz);
}
At this point, spdm_start_digest
is called from spdm_negotiate_algs
above to update the hash with messages sent so far (version, cap messages).
static int spdm_start_digest(struct spdm_state *spdm_state,
void *req, size_t req_sz, void *rsp, size_t rsp_sz)
{
char *alg_name;
int rc;
switch (spdm_state->base_hash_alg) {
case SPDM_HASH_SHA_256:
alg_name = "sha256";
break;
case SPDM_HASH_SHA_384:
alg_name = "sha384";
break;
default:
dev_err(spdm_state->dev, "Unknown hash algorithm\n");
return -EINVAL;
}
spdm_state->shash = crypto_alloc_shash(alg_name, 0, 0);
spdm_state->desc = kzalloc(sizeof(*spdm_state->desc) +
crypto_shash_descsize(spdm_state->shash),
GFP_KERNEL);
spdm_state->desc->tfm = spdm_state->shash;
/* Used frequently to compute offsets, so cache H */
spdm_state->h = crypto_shash_digestsize(spdm_state->shash);
rc = crypto_shash_init(spdm_state->desc);
rc = crypto_shash_update(spdm_state->desc,
(u8 *)&spdm_get_version_req,
sizeof(spdm_get_version_req));
rc = crypto_shash_update(spdm_state->desc,
(u8 *)spdm_state->get_version_rsp,
spdm_state->get_version_rsp_sz);
rc = crypto_shash_update(spdm_state->desc,
(u8 *)&spdm_state->get_caps_req,
spdm_state->get_caps_req_sz);
rc = crypto_shash_update(spdm_state->desc,
(u8 *)&spdm_state->get_caps_rsp,
spdm_state->get_caps_rsp_sz);
rc = crypto_shash_update(spdm_state->desc, (u8 *)req, req_sz);
rc = crypto_shash_update(spdm_state->desc, (u8 *)rsp, rsp_sz);
spdm_get_digests Link to heading
For GET_DIGESTS
response is slot_mask
to mark that which digest of cert is supported.
Slot mask. The bit in position K of this byte shall be set to 1b if and only if slot number K contains a certificate chain for the protocol version in the SPDMVersion field. (Bit 0 is the least significant bit of the byte.) The number of digests returned shall be equal to the number of bits set in this byte. The digests shall be returned in order of increasing slot number.
So, Responder sends back the slot mask in param2
thes digests are sent as well
4 Digest[0] Digest of the first certificate chain. This field shall be in Hash byte order.
4+(H * Digest[n-1] (n -1)) Digest of the last (nth) certificate chain. This field shall be in Hash byte order.
In spdm_get_digests
, It initially allocate memory for 8 slots but then the actual size from response. And finally, It updates the hash with req and rst for the digest.
static int spdm_get_digests(struct spdm_state *spdm_state)
{
struct spdm_get_digests_req req = { .code = SPDM_GET_DIGESTS };
rc = spdm_exchange(spdm_state, &req, sizeof(req), rsp, rsp_sz);
...
length = rc;
if (length < sizeof(*rsp) ||
length < sizeof(*rsp) + hweight8(rsp->param2) * spdm_state->h) {
dev_err(spdm_state->dev, "Truncated digests response\n");
rc = -EIO;
goto err_free_rsp;
}
...
rsp_sz = sizeof(*rsp) + hweight8(rsp->param2) * spdm_state->h;
/*
* Authentication-capable endpoints must carry at least 1 cert chain
* (SPDM 1.1.0 margin no 221).
*/
spdm_state->slot_mask = rsp->param2;
if (!spdm_state->slot_mask) {
dev_err(spdm_state->dev, "No certificates provisioned\n");
rc = -EPROTO;
goto err_free_rsp;
}
rc = crypto_shash_update(spdm_state->desc, (u8 *)&req, sizeof(req));
...
rc = crypto_shash_update(spdm_state->desc, (u8 *)rsp, rsp_sz);
...
}
spdm_get_certificate Link to heading
For each of the slots returned from get_digests
, spdm_get_certificate
is called with slot number.
for_each_set_bit(slot, &spdm_state->slot_mask, SPDM_SLOTS) {
rc = spdm_get_certificate(spdm_state, slot);
if (rc == 0)
break; /* success */
if ((rc != -ENOKEY && rc != -EKEYREJECTED) ||
slot == SPDM_SLOTS - 1)
return rc; /* try next slot only on signature error */
}
In spdm_get_certificate
, The GET_CERTIFICATE request can happen multiple times until the responder send back all the bytes in the cert. The do-while
loop uses the portion_length
and remainder_length
to check if it’s need another iteration and copies the portion it got in the buffer certs
. Then it calls spdm_validate_certificate
.
static int spdm_get_certificate(struct spdm_state *spdm_state, u8 slot)
{
struct spdm_get_certificate_req req = {
.code = SPDM_GET_CERTIFICATE,
.param1 = slot,
};
...
...
do {
req.offset = cpu_to_le16(offset);
req.length = cpu_to_le16(min_t(size_t, remainder_length,
rsp_sz - sizeof(*rsp)));
rc = spdm_exchange(spdm_state, &req, sizeof(req), rsp, rsp_sz);
...
portion_length = le16_to_cpu(rsp->portion_length);
remainder_length = le16_to_cpu(rsp->remainder_length);
...
if (!certs) {
total_length = min(portion_length + remainder_length,
0xffff);
certs = kvmalloc(total_length, GFP_KERNEL);
...
}
...
memcpy(certs + offset, rsp->cert_chain, portion_length);
offset += portion_length;
rc = crypto_shash_update(spdm_state->desc, (u8 *)&req,
sizeof(req));
rc = crypto_shash_update(spdm_state->desc, (u8 *)rsp,
sizeof(*rsp) + portion_length);
} while (remainder_length > 0);
header_length = sizeof(struct spdm_cert_chain) + spdm_state->h;
rc = spdm_validate_certificate(spdm_state, slot, certs + header_length,
total_length - header_length);
In spdm_validate_certificate
, keyring
is used for safe handling of certificate chains. function x509_get_certificate_length
is used to get the length ( ie offset for next cert) and call key_create_or_update
to create key. So, This loop will go over all the certs in the chain. If there is something rc
returns non-zero value.
static int spdm_validate_certificate(struct spdm_state *spdm_state, u8 slot,
u8 *certs, size_t total_length)
{
keyring = keyring_alloc(keyring_name, KUIDT_INIT(0), KGIDT_INIT(0),
current_cred(), KEY_POS_ALL,
KEY_ALLOC_NOT_IN_QUOTA | KEY_ALLOC_SET_KEEP,
NULL, NULL);
rc = keyring_restrict(make_key_ref(keyring, true), "asymmetric",
root_serial);
while (offset < total_length) {
rc = x509_get_certificate_length(certs + offset,
total_length - offset);
if (rc < 0)
goto err_put_keyring;
length = rc;
key_ref = key_create_or_update(make_key_ref(keyring, true),
"asymmetric", NULL,
certs + offset, length,
(KEY_POS_ALL & ~KEY_POS_SETATTR),
KEY_ALLOC_NOT_IN_QUOTA);
...
...
key_put(keyring->restrict_link->key);
keyring->restrict_link->key = key_ref_to_ptr(key_ref);
offset += length;
}
spdm_state->leaf_key = key_get(keyring->restrict_link->key);
return rc;
}
spdm_challenge Link to heading
In spdm_challenge
, The CHALLENGE request is sent with the slot number, measurement parameter and nonce value. Note that current implementation sends 0x for param2 in the request which means the measurement is not requested.
static int spdm_challenge(struct spdm_state *spdm_state)
{
const char spdm_prefix[] = {
'd', 'm', 't', 'f', '-', 's', 'p', 'd', 'm', '-', 'v', '1', '.', '2', '.', '*',
'd', 'm', 't', 'f', '-', 's', 'p', 'd', 'm', '-', 'v', '1', '.', '2', '.', '*',
'd', 'm', 't', 'f', '-', 's', 'p', 'd', 'm', '-', 'v', '1', '.', '2', '.', '*',
'd', 'm', 't', 'f', '-', 's', 'p', 'd', 'm', '-', 'v', '1', '.', '2', '.', '*',
0, 0, 0, 0, /* pad here so length 100 */
'r', 'e', 's', 'p', 'o', 'n', 'd', 'e', 'r', '-',
'c', 'h', 'a', 'l', 'l', 'e', 'n', 'g', 'e', '_', 'a', 'u', 't', 'h', ' ',
's', 'i', 'g', 'n', 'i', 'n', 'g' };
struct spdm_challenge_req req = {
.code = SPDM_CHALLENGE,
.param1 = 0, /* slot 0 for now */
.param2 = 0, /* no measurement summary hash */
};
struct spdm_challenge_rsp *rsp;
size_t sig_offset, rsp_max_size;
int length, rc;
u8 *digest, *message;
/*
* The response length is up to:
* 4 byte header
* H byte CertChainHash
* 32 byte nonce
* (H byte Measurement Summary Hash - not currently requested)
* 2 byte Opaque Length
* <= 1024 bytes Opaque Data
* S byte signature
*/
rsp_max_size = sizeof(*rsp) + spdm_state->h + 32 + 2 + 1024 + spdm_state->s;
rsp = kzalloc(rsp_max_size, GFP_KERNEL);
if (!rsp)
return -ENOMEM;
get_random_bytes(&req.nonce, sizeof(req.nonce));
rc = spdm_exchange(spdm_state, &req, sizeof(req), rsp, rsp_max_size);
/* Last step of building the digest */
rc = crypto_shash_update(spdm_state->desc, (u8 *)&req, sizeof(req));
/* The hash is complete + signature received; verify against leaf key */
sig_offset = spdm_challenge_rsp_signature_offset(spdm_state, &req, rsp);
rc = crypto_shash_update(spdm_state->desc, (u8 *)rsp, sig_offset);
...
}
Side, Note, In spdm_challenge_rsp_signature_offset
, THe offset of signature in the CHALLANGE response is calculated.
static size_t spdm_challenge_rsp_signature_offset(struct spdm_state *spdm_state,
struct spdm_challenge_req *req,
struct spdm_challenge_rsp *rsp)
{
u16 opaque_length;
size_t offset;
offset = sizeof(*rsp); /* Header offset */
offset += spdm_state->h; /* CertChain hash */
offset += 32; /* Nonce */
/* Measurement summary hash */
if (req->param2 &&
(spdm_state->responder_caps & SPDM_MAC_CAP))
offset += spdm_state->h;
/*
* This is almost certainly aligned, but that's not obvious from nearby code
* so play safe.
*/
opaque_length = get_unaligned_le16((u8 *)rsp + offset);
offset += sizeof(__le16);
offset += opaque_length;
return offset;
}
Then there are 2 things happening. First, The prefix is appended to the digest so far and crypto_shash_digest
is called. The second thing is that signature is verified by calling spdm_verify_signature
.
digest = kmalloc(spdm_state->h, GFP_KERNEL);
...
crypto_shash_final(spdm_state->desc, digest);
if (spdm_state->version >= 0x12) {
message = kmalloc(spdm_state->h + sizeof(spdm_prefix), GFP_KERNEL);
memcpy(message, spdm_prefix, sizeof(spdm_prefix));
memcpy(message + sizeof(spdm_prefix), digest, spdm_state->h);
rc = crypto_shash_digest(spdm_state->desc, message,
spdm_state->h + sizeof(spdm_prefix), digest);
spdm_state->h + sizeof(spdm_prefix), digest);
if (rc) {
dev_err(spdm_state->dev, "Could not digest prefix + message\n");
kfree(message);
goto err_free_digest;
}
}
rc = spdm_verify_signature(spdm_state, (u8 *)rsp + sig_offset, digest,
spdm_state->h);
In spdm_verify_signature
, sig
structure is populated depending on the type of hashing and public key management. verifiy_signauture
is called to verify the signature with the digest calculated over the responses sent from start of protocol.
static int spdm_verify_signature(struct spdm_state *spdm_state, u8 *sig_ptr,
u8 *digest, unsigned int digest_size)
{
const struct asymmetric_key_ids *ids;
struct public_key_signature sig = {};
/* Large enough for an ASN1 encoding of supported ECC signatures */
unsigned char buffer2[128] = {};
int rc;
/*
* The ecdsa signatures are raw concatentation of the two values.
* SPDM 1.2.1 section 2.2.3.4.1 refers to FIPS PUB 186-4 defining this
* ordering.
* In order to use verify_signature we need to reformat them into ASN1.
*/
switch (spdm_state->base_asym_alg) {
case SPDM_ASYM_ECDSA_ECC_NIST_P256:
case SPDM_ASYM_ECDSA_ECC_NIST_P384:
{
unsigned char buffer[128] = {};
unsigned char *p = buffer;
unsigned char *p2;
//TODO: test the ASN1 function rather more extensively.
/* First pack the two large integer values */
p = asn1_encode_integer_large_positive(p, buffer + sizeof(buffer),
ASN1_INT, sig_ptr,
spdm_state->s / 2);
p = asn1_encode_integer_large_positive(p, buffer + sizeof(buffer),
ASN1_INT,
sig_ptr + spdm_state->s / 2,
spdm_state->s / 2);
/* In turn pack those two large integer values into a sequence */
p2 = asn1_encode_sequence(buffer2, buffer2 + sizeof(buffer2),
buffer, p - buffer);
sig.s = buffer2;
sig.s_size = p2 - buffer2;
sig.encoding = "x962";
break;
}
case SPDM_ASYM_RSASSA_3072:
sig.s = sig_ptr;
sig.s_size = spdm_state->s;
sig.encoding = "pkcs1";
break;
default:
dev_err(spdm_state->dev,
"Signature algorithm not yet supported\n");
return -EINVAL;
}
sig.digest = digest;
sig.digest_size = digest_size;
ids = asymmetric_key_ids(spdm_state->leaf_key);
sig.auth_ids[0] = ids->id[0];
sig.auth_ids[1] = ids->id[1];
switch (spdm_state->base_hash_alg) {
case SPDM_HASH_SHA_384:
sig.hash_algo = "sha384";
break;
case SPDM_HASH_SHA_256:
sig.hash_algo = "sha256";
break;
default:
return -EINVAL;
}
rc = verify_signature(spdm_state->leaf_key, &sig);
return 0;
}