diff --git a/examples/pqc/pqc_mssim_e2e.c b/examples/pqc/pqc_mssim_e2e.c index ef3c2428a..247f70941 100644 --- a/examples/pqc/pqc_mssim_e2e.c +++ b/examples/pqc/pqc_mssim_e2e.c @@ -218,6 +218,157 @@ static int test_hash_mldsa_digest_roundtrip(WOLFTPM2_DEV* dev) return rc; } +/* MakeCredential -> ActivateCredential with an ML-KEM EK, through the client + * library. Proves the client marshals both credential commands and that the + * fwTPM unwraps the ML-KEM-encapsulated seed end-to-end (recovered secret + * matches). The EK is also the activate object, so no second key is needed. */ +static int test_mlkem_credential_roundtrip(WOLFTPM2_DEV* dev) +{ + WOLFTPM2_KEY ek; + TPMT_PUBLIC tpl; + MakeCredential_In makeCredIn; + MakeCredential_Out makeCredOut; + ActivateCredential_In activCredIn; + ActivateCredential_Out activCredOut; + byte secret[16]; + int rc; + + XMEMSET(&ek, 0, sizeof(ek)); + XMEMSET(&tpl, 0, sizeof(tpl)); + XMEMSET(&makeCredIn, 0, sizeof(makeCredIn)); + XMEMSET(&makeCredOut, 0, sizeof(makeCredOut)); + XMEMSET(&activCredIn, 0, sizeof(activCredIn)); + XMEMSET(&activCredOut, 0, sizeof(activCredOut)); + XMEMSET(secret, 0xC7, sizeof(secret)); + + /* Credential key must be a restricted decryption (Storage) key per Part 3 + * Sec.24; the template adds an AES symmetric for restricted. */ + rc = wolfTPM2_GetKeyTemplate_MLKEM(&tpl, + TPMA_OBJECT_decrypt | TPMA_OBJECT_restricted | TPMA_OBJECT_fixedTPM | + TPMA_OBJECT_fixedParent | TPMA_OBJECT_sensitiveDataOrigin | + TPMA_OBJECT_userWithAuth, + TPM_MLKEM_768); + if (rc != 0) { + printf("GetKeyTemplate_MLKEM rc=%d\n", rc); + return rc; + } + + rc = wolfTPM2_CreatePrimaryKey(dev, &ek, TPM_RH_OWNER, &tpl, NULL, 0); + if (rc != 0) { + printf("CreatePrimary(MLKEM-768) rc=%d\n", rc); + return rc; + } + + /* MakeCredential (no auth): encrypt the secret to the EK, bound to the + * EK's own Name. */ + makeCredIn.handle = ek.handle.hndl; + makeCredIn.credential.size = sizeof(secret); + XMEMCPY(makeCredIn.credential.buffer, secret, sizeof(secret)); + makeCredIn.objectName.size = ek.handle.name.size; + XMEMCPY(makeCredIn.objectName.name, ek.handle.name.name, + ek.handle.name.size); + rc = TPM2_MakeCredential(&makeCredIn, &makeCredOut); + if (rc != 0) { + printf("TPM2_MakeCredential rc=0x%x\n", rc); + goto cleanup; + } + + /* ActivateCredential: EK decrypts the seed (ML-KEM decap) and unwraps. + * Two auth slots: activateHandle then keyHandle, both the EK. */ + wolfTPM2_SetAuthHandle(dev, 0, &ek.handle); + wolfTPM2_SetAuthHandle(dev, 1, &ek.handle); + activCredIn.activateHandle = ek.handle.hndl; + activCredIn.keyHandle = ek.handle.hndl; + activCredIn.credentialBlob = makeCredOut.credentialBlob; + activCredIn.secret = makeCredOut.secret; + rc = TPM2_ActivateCredential(&activCredIn, &activCredOut); + if (rc != 0) { + printf("TPM2_ActivateCredential rc=0x%x\n", rc); + goto cleanup; + } + + if (activCredOut.certInfo.size != sizeof(secret) || + XMEMCMP(activCredOut.certInfo.buffer, secret, + sizeof(secret)) != 0) { + printf("Recovered credential mismatch: MLKEM activate path broken\n"); + rc = -1; + goto cleanup; + } + + printf("[E2E] MLKEM-768 MakeCredential/ActivateCredential over mssim: " + "recovered %u-byte secret matches\n", activCredOut.certInfo.size); + +cleanup: + wolfTPM2_UnloadHandle(dev, &ek.handle); + return rc; +} + +/* TPM2_Quote with a restricted Pure ML-DSA signing key, through the client + * library. Proves the client marshals the quote and parses the Pure ML-DSA + * signature off the wire (sigAlg + expected size). */ +static int test_mldsa_quote(WOLFTPM2_DEV* dev) +{ + WOLFTPM2_KEY ak; + TPMT_PUBLIC tpl; + Quote_In quoteIn; + Quote_Out quoteOut; + int rc; + + XMEMSET(&ak, 0, sizeof(ak)); + XMEMSET(&tpl, 0, sizeof(tpl)); + XMEMSET("eIn, 0, sizeof(quoteIn)); + XMEMSET("eOut, 0, sizeof(quoteOut)); + + rc = wolfTPM2_GetKeyTemplate_MLDSA(&tpl, + TPMA_OBJECT_sign | TPMA_OBJECT_restricted | TPMA_OBJECT_fixedTPM | + TPMA_OBJECT_fixedParent | TPMA_OBJECT_sensitiveDataOrigin | + TPMA_OBJECT_userWithAuth, + TPM_MLDSA_65, NO); + if (rc != 0) { + printf("GetKeyTemplate_MLDSA rc=%d\n", rc); + return rc; + } + + rc = wolfTPM2_CreatePrimaryKey(dev, &ak, TPM_RH_OWNER, &tpl, NULL, 0); + if (rc != 0) { + printf("CreatePrimary(MLDSA-65) rc=%d\n", rc); + return rc; + } + + /* Quote PCR0 (SHA-256) with the key's own scheme (inScheme NULL). */ + wolfTPM2_SetAuthHandle(dev, 0, &ak.handle); + quoteIn.signHandle = ak.handle.hndl; + quoteIn.inScheme.scheme = TPM_ALG_NULL; + TPM2_SetupPCRSel("eIn.PCRselect, TPM_ALG_SHA256, 0); + + rc = TPM2_Quote("eIn, "eOut); + if (rc != 0) { + printf("TPM2_Quote rc=0x%x\n", rc); + goto cleanup; + } + + if (quoteOut.signature.sigAlg != TPM_ALG_MLDSA) { + printf("Quote sigAlg=0x%x (expected MLDSA 0x%x)\n", + quoteOut.signature.sigAlg, TPM_ALG_MLDSA); + rc = -1; + goto cleanup; + } + if (quoteOut.signature.signature.mldsa.size != 3309) { + printf("MLDSA-65 quote sig size=%u (expected 3309)\n", + quoteOut.signature.signature.mldsa.size); + rc = -1; + goto cleanup; + } + + printf("[E2E] MLDSA-65 Quote over mssim: attest=%u bytes, " + "sig=%u bytes (sigAlg=MLDSA)\n", + quoteOut.quoted.size, quoteOut.signature.signature.mldsa.size); + +cleanup: + wolfTPM2_UnloadHandle(dev, &ak.handle); + return rc; +} + int main(int argc, char** argv) { WOLFTPM2_DEV dev; @@ -237,6 +388,12 @@ int main(int argc, char** argv) if (rc != 0) goto done; rc = test_hash_mldsa_digest_roundtrip(&dev); + if (rc != 0) goto done; + + rc = test_mlkem_credential_roundtrip(&dev); + if (rc != 0) goto done; + + rc = test_mldsa_quote(&dev); done: wolfTPM2_Cleanup(&dev); diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index f5affad02..acfdcd68f 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -14529,7 +14529,14 @@ static TPM_RC FwParseAttestParams(TPM2_Packet* cmd, int cmdSize, if (rc == 0) { TPM2_Packet_ParseU16(cmd, sigScheme); *sigHashAlg = TPM_ALG_NULL; - if (*sigScheme != TPM_ALG_NULL) + /* ML-DSA scheme arms are TPMS_EMPTY (TCG v185 errata): no trailing + * hash to consume. */ + if (*sigScheme != TPM_ALG_NULL +#ifdef WOLFTPM_PQC + && *sigScheme != TPM_ALG_MLDSA + && *sigScheme != TPM_ALG_HASH_MLDSA +#endif + ) TPM2_Packet_ParseU16(cmd, sigHashAlg); /* TPMS_SCHEME_ECDAA carries an additional UINT16 count after * hashAlg per Part 2 Sec. 11.2.1.5. */ @@ -14549,6 +14556,23 @@ static TPM_RC FwParseAttestParams(TPM2_Packet* cmd, int cmdSize, #endif /* !FWTPM_NO_ATTESTATION */ #ifndef FWTPM_NO_ATTESTATION +/* Inner measurement digest (Quote pcrDigest, NV_Certify nvDigest) uses the + * signing key's nameAlg for ML-DSA keys (TCG v185 errata: both scheme arms are + * TPMS_EMPTY). Returns TPM_ALG_NULL for classical keys. */ +static UINT16 FwAttestMldsaHashAlg(const FWTPM_Object* obj) +{ +#ifdef WOLFTPM_PQC + /* Hash-ML-DSA's hashAlg is the message pre-hash for the signature, not this + * inner digest, so both ML-DSA arms resolve to nameAlg here. */ + if (obj->pub.type == TPM_ALG_MLDSA || + obj->pub.type == TPM_ALG_HASH_MLDSA) { + return obj->pub.nameAlg; + } +#endif + (void)obj; + return TPM_ALG_NULL; +} + /* --- TPM2_Quote (CC 0x0158) --- * signHandle authHandle | qualifyingData | inScheme | PCRselect * Response: TPM2B_ATTEST + TPMT_SIGNATURE */ @@ -14645,9 +14669,14 @@ static TPM_RC FwCmd_Quote(FWTPM_CTX* ctx, TPM2_Packet* cmd, selections[s].pcrSelect[j]); } - /* pcrDigest = hash of concatenated selected PCR values */ - pcrHashAlg = (sigHashAlg != TPM_ALG_NULL) ? sigHashAlg : - (numSel > 0 ? selections[0].hashAlg : (UINT16)TPM_ALG_SHA256); + /* pcrDigest = hash of the selected PCR values under the signature's + * hash so a verifier agrees. ML-DSA keys resolve to nameAlg (no scheme + * hash on the wire); classical keys honor the wire hash then PCR bank. */ + pcrHashAlg = FwAttestMldsaHashAlg(sigObj); + if (pcrHashAlg == TPM_ALG_NULL) { + pcrHashAlg = (sigHashAlg != TPM_ALG_NULL) ? sigHashAlg : + (numSel > 0 ? selections[0].hashAlg : (UINT16)TPM_ALG_SHA256); + } wcH = FwGetWcHashType(pcrHashAlg); dSz = TPM2_GetHashDigestSize(pcrHashAlg); if (wcH != WC_HASH_TYPE_NONE && dSz > 0) { @@ -14854,7 +14883,14 @@ static TPM_RC FwCmd_CertifyCreation(FWTPM_CTX* ctx, TPM2_Packet* cmd, if (rc == 0) { TPM2_Packet_ParseU16(cmd, &sigScheme); sigHashAlg = TPM_ALG_NULL; - if (sigScheme != TPM_ALG_NULL) + /* ML-DSA scheme arms are TPMS_EMPTY (TCG v185 errata): no trailing + * hash to consume. */ + if (sigScheme != TPM_ALG_NULL +#ifdef WOLFTPM_PQC + && sigScheme != TPM_ALG_MLDSA + && sigScheme != TPM_ALG_HASH_MLDSA +#endif + ) TPM2_Packet_ParseU16(cmd, &sigHashAlg); /* TPMS_SCHEME_ECDAA carries an additional UINT16 count after * hashAlg per Part 2 Sec. 11.2.1.5. */ @@ -15169,9 +15205,12 @@ static TPM_RC FwCmd_NV_Certify(FWTPM_CTX* ctx, TPM2_Packet* cmd, int hashSz; enum wc_HashType wcDigH; - /* Resolve hash from signing key when scheme/hash is NULL. - * keyScheme is filled in by the by-pointer interface but - * unused here - we only need the resolved hashAlg. */ + /* ML-DSA keys have no scheme hash, so resolve nvDigest's hash from + * nameAlg (TCG v185 errata) before the classical + * FwResolveSignScheme fallback. */ + if (hashAlg == TPM_ALG_NULL) { + hashAlg = FwAttestMldsaHashAlg(sigObj); + } if (hashAlg == TPM_ALG_NULL) { UINT16 keyScheme = TPM_ALG_NULL; FwResolveSignScheme(sigObj, &keyScheme, &hashAlg); @@ -15221,6 +15260,37 @@ static TPM_RC FwCmd_NV_Certify(FWTPM_CTX* ctx, TPM2_Packet* cmd, #ifndef FWTPM_NO_CREDENTIAL +/* AES key size (bytes) for credential wrap from a Storage key's symmetric def. + * Requires AES-CFB (Part 1 Sec.24); returns 0 for any other cipher/mode. */ +static int FwCredentialAesKeyBytes(const FWTPM_Object* keyObj) +{ + const TPMT_SYM_DEF_OBJECT* sym; + + switch (keyObj->pub.type) { +#ifndef NO_RSA + case TPM_ALG_RSA: + sym = &keyObj->pub.parameters.rsaDetail.symmetric; + break; +#endif +#ifdef HAVE_ECC + case TPM_ALG_ECC: + sym = &keyObj->pub.parameters.eccDetail.symmetric; + break; +#endif +#ifdef WOLFTPM_MLKEM + case TPM_ALG_MLKEM: + sym = &keyObj->pub.parameters.mlkemDetail.symmetric; + break; +#endif + default: + return 0; + } + if (sym->algorithm != TPM_ALG_AES || sym->mode.aes != TPM_ALG_CFB) { + return 0; + } + return (int)sym->keyBits.aes / 8; +} + /* --- TPM2_MakeCredential (CC 0x0168) --- * handle (AIK public key used to wrap seed) | credential | objectName * Response: TPM2B_ID_OBJECT + TPM2B_ENCRYPTED_SECRET @@ -15245,11 +15315,13 @@ static TPM_RC FwCmd_MakeCredential(FWTPM_CTX* ctx, TPM2_Packet* cmd, int seedSz = 0; FWTPM_DECLARE_BUF(encSeed, FWTPM_MAX_PUB_BUF); int encSeedSz = 0; - byte symKey[16]; /* AES-128 */ - byte hmacKey[TPM_SHA256_DIGEST_SIZE]; + byte symKey[32]; /* up to AES-256 */ + byte hmacKey[TPM_MAX_DIGEST_SIZE]; + int symKeySz = 0; + int hmacKeySz = 0; FWTPM_DECLARE_BUF(encCred, FWTPM_MAX_NV_DATA + 2); word32 encCredSz = 0; - byte outerHmac[TPM_SHA256_DIGEST_SIZE]; + byte outerHmac[TPM_MAX_DIGEST_SIZE]; byte oaepLabel[64]; int oaepLabelSz = 0; @@ -15272,10 +15344,25 @@ static TPM_RC FwCmd_MakeCredential(FWTPM_CTX* ctx, TPM2_Packet* cmd, rc = TPM_RC_HANDLE; } } - /* Credential wrap/unwrap integrity is SHA-256 only; reject other - * nameAlgs rather than emit a mixed-hash (non-interoperable) blob. */ - if (rc == 0 && keyObj->pub.nameAlg != TPM_ALG_SHA256) { - rc = TPM_RC_HASH; + /* Part 3 Sec.24: the credential key must be a restricted decryption + * (Storage) key, so a blob is never produced for a key that could leak the + * seed via Decapsulate. */ + if (rc == 0 && + (((keyObj->pub.objectAttributes & TPMA_OBJECT_restricted) == 0) || + ((keyObj->pub.objectAttributes & TPMA_OBJECT_decrypt) == 0))) { + rc = TPM_RC_ATTRIBUTES; + } + /* Credential protection derives the HMAC under the key's nameAlg and the + * symmetric key at its declared AES-CFB size (Part 1 Sec.24). Reject a key + * whose nameAlg or symmetric is unsupported. */ + if (rc == 0) { + hmacKeySz = TPM2_GetHashDigestSize(keyObj->pub.nameAlg); + symKeySz = FwCredentialAesKeyBytes(keyObj); + if (FwGetWcHashType(keyObj->pub.nameAlg) == WC_HASH_TYPE_NONE || + hmacKeySz <= 0 || hmacKeySz > (int)sizeof(hmacKey) || + symKeySz <= 0 || symKeySz > (int)sizeof(symKey)) { + rc = TPM_RC_KEY; + } } /* MakeCredential has no auth area */ @@ -15347,15 +15434,15 @@ static TPM_RC FwCmd_MakeCredential(FWTPM_CTX* ctx, TPM2_Packet* cmd, if (rc == 0) { rc = FwCredentialDeriveKeys(keyObj->pub.nameAlg, seed, seedSz, objectName.name, objectName.size, - symKey, (int)sizeof(symKey), - hmacKey, (int)sizeof(hmacKey)); + symKey, symKeySz, + hmacKey, hmacKeySz); } /* Encrypt credential and compute outer HMAC */ if (rc == 0) { rc = FwCredentialWrap( - symKey, (int)sizeof(symKey), - hmacKey, (int)sizeof(hmacKey), + symKey, symKeySz, + hmacKey, hmacKeySz, keyObj->pub.nameAlg, credential.buffer, credential.size, objectName.name, objectName.size, encCred, &encCredSz, outerHmac); @@ -15373,9 +15460,9 @@ static TPM_RC FwCmd_MakeCredential(FWTPM_CTX* ctx, TPM2_Packet* cmd, blobSzPos = rsp->pos; TPM2_Packet_AppendU16(rsp, 0); /* placeholder */ blobStart = rsp->pos; - /* integrity HMAC as TPM2B */ - TPM2_Packet_AppendU16(rsp, TPM_SHA256_DIGEST_SIZE); - TPM2_Packet_AppendBytes(rsp, outerHmac, TPM_SHA256_DIGEST_SIZE); + /* integrity HMAC as TPM2B (sized by the key's nameAlg) */ + TPM2_Packet_AppendU16(rsp, (UINT16)hmacKeySz); + TPM2_Packet_AppendBytes(rsp, outerHmac, hmacKeySz); /* encIdentity as raw bytes (encCredential) */ TPM2_Packet_AppendBytes(rsp, encCred, (int)encCredSz); /* patch blob size */ @@ -15426,8 +15513,10 @@ static TPM_RC FwCmd_ActivateCredential(FWTPM_CTX* ctx, TPM2_Packet* cmd, int paramSzPos, paramStart; byte seed[64]; int seedSzInt = 0; - byte symKey[16]; - byte hmacKey[TPM_SHA256_DIGEST_SIZE]; + byte symKey[32]; /* up to AES-256 */ + byte hmacKey[TPM_MAX_DIGEST_SIZE]; + int symKeySz = 0; + int hmacKeySz = 0; byte oaepLabel[64]; int oaepLabelSz = 0; byte credOut[sizeof(TPMU_HA)]; @@ -15456,10 +15545,6 @@ static TPM_RC FwCmd_ActivateCredential(FWTPM_CTX* ctx, TPM2_Packet* cmd, rc = TPM_RC_HANDLE; } } - /* Credential wrap/unwrap integrity is SHA-256 only (see MakeCredential) */ - if (rc == 0 && keyObj->pub.nameAlg != TPM_ALG_SHA256) { - rc = TPM_RC_HASH; - } /* Skip auth area */ if (rc == 0 && cmdTag == TPM_ST_SESSIONS) { @@ -15498,12 +15583,37 @@ static TPM_RC FwCmd_ActivateCredential(FWTPM_CTX* ctx, TPM2_Packet* cmd, TPM2_Packet_ParseBytes(cmd, secretBuf, secretSz); } + /* keyHandle decrypts the credential seed, so any key type FwDecryptSeed + * can unwrap is valid, including an ML-KEM EK. */ if (rc == 0) { if (keyObj->pub.type != TPM_ALG_RSA && +#ifdef WOLFTPM_MLKEM_DECAP + keyObj->pub.type != TPM_ALG_MLKEM && +#endif keyObj->pub.type != TPM_ALG_ECC) { rc = TPM_RC_KEY; } } + /* Part 3 Sec.24: keyHandle MUST be restricted decryption (Storage). An + * unrestricted decrypt key can be driven through TPM2_Decapsulate to + * recover the seed outside the TPM, bypassing activateHandle's auth. */ + if (rc == 0 && + (((keyObj->pub.objectAttributes & TPMA_OBJECT_restricted) == 0) || + ((keyObj->pub.objectAttributes & TPMA_OBJECT_decrypt) == 0))) { + rc = TPM_RC_ATTRIBUTES; + } + /* Credential protection derives the HMAC under the key's nameAlg and the + * symmetric key at its declared AES-CFB size (Part 1 Sec.24). Reject a key + * whose nameAlg or symmetric is unsupported. */ + if (rc == 0) { + hmacKeySz = TPM2_GetHashDigestSize(keyObj->pub.nameAlg); + symKeySz = FwCredentialAesKeyBytes(keyObj); + if (FwGetWcHashType(keyObj->pub.nameAlg) == WC_HASH_TYPE_NONE || + hmacKeySz <= 0 || hmacKeySz > (int)sizeof(hmacKey) || + symKeySz <= 0 || symKeySz > (int)sizeof(symKey)) { + rc = TPM_RC_KEY; + } + } #ifdef DEBUG_WOLFTPM if (rc == 0) { @@ -15547,16 +15657,16 @@ static TPM_RC FwCmd_ActivateCredential(FWTPM_CTX* ctx, TPM2_Packet* cmd, objName = &activateObj->name; rc = FwCredentialDeriveKeys(keyObj->pub.nameAlg, seed, seedSzInt, objName->name, objName->size, - symKey, (int)sizeof(symKey), - hmacKey, (int)sizeof(hmacKey)); + symKey, symKeySz, + hmacKey, hmacKeySz); } /* Verify HMAC and decrypt credential */ if (rc == 0) { objName = &activateObj->name; rc = FwCredentialUnwrap( - symKey, (int)sizeof(symKey), - hmacKey, (int)sizeof(hmacKey), + symKey, symKeySz, + hmacKey, hmacKeySz, keyObj->pub.nameAlg, blobBuf, blobSz, objName->name, objName->size, credOut, (int)sizeof(credOut), &credSz); diff --git a/src/fwtpm/fwtpm_crypto.c b/src/fwtpm/fwtpm_crypto.c index 215ce863e..d0136ab9e 100644 --- a/src/fwtpm/fwtpm_crypto.c +++ b/src/fwtpm/fwtpm_crypto.c @@ -4110,6 +4110,72 @@ TPM_RC FwBuildAttestResponse(FWTPM_CTX* ctx, TPM2_Packet* rsp, return rc; } +#ifdef WOLFTPM_MLDSA_SIGN +/* Sign serialized TPMS_ATTEST bytes with an ML-DSA (or Hash-ML-DSA) key and + * append the TPMT_SIGNATURE. Split out of FwSignAttest so the large ML-DSA + * signature buffer only occupies the stack when an ML-DSA key is in use. */ +static TPM_RC FwSignAttestMldsa(FWTPM_CTX* ctx, FWTPM_Object* obj, + const byte* attestBuf, int attestSz, TPM2_Packet* rsp) +{ + TPM_RC rc = TPM_RC_SUCCESS; + FWTPM_DECLARE_VAR(sigOut, TPM2B_MLDSA_SIGNATURE); + byte digest[TPM_MAX_DIGEST_SIZE]; + + /* Reject a public-only object (privKeySize 0): its all-zero seed derives a + * universally reproducible key that would forge attestations. Mirrors the + * ML-KEM seed guard in FwDecryptSeed. */ + if (obj->privKeySize != MAX_MLDSA_PRIV_SEED_SIZE) { + return TPM_RC_KEY; + } + + FWTPM_CALLOC_VAR(sigOut, TPM2B_MLDSA_SIGNATURE); + + if (rc == 0 && obj->pub.type == TPM_ALG_MLDSA) { + /* Pure ML-DSA: sign the message with an empty context. */ + rc = FwSignMldsaMessage(&ctx->rng, + obj->pub.parameters.mldsaDetail.parameterSet, + obj->privKey, NULL, 0, + attestBuf, attestSz, sigOut); + if (rc == 0) { + TPM2_Packet_AppendU16(rsp, TPM_ALG_MLDSA); + TPM2_Packet_AppendU16(rsp, sigOut->size); + TPM2_Packet_AppendBytes(rsp, sigOut->buffer, sigOut->size); + } + } + else if (rc == 0) { + /* Hash-ML-DSA: pre-hash the message under the key's hashAlg, then + * sign the digest (FIPS 204 Algorithm 4). */ + TPMI_ALG_HASH phAlg = obj->pub.parameters.hash_mldsaDetail.hashAlg; + enum wc_HashType phWc = FwGetWcHashType(phAlg); + int phSz = TPM2_GetHashDigestSize(phAlg); + + if (phWc == WC_HASH_TYPE_NONE || phSz == 0) { + rc = TPM_RC_HASH; + } + if (rc == 0 && + wc_Hash(phWc, attestBuf, attestSz, digest, phSz) != 0) { + rc = TPM_RC_FAILURE; + } + if (rc == 0) { + rc = FwSignMldsaHash(&ctx->rng, + obj->pub.parameters.hash_mldsaDetail.parameterSet, + obj->privKey, NULL, 0, phAlg, + digest, phSz, sigOut); + } + if (rc == 0) { + TPM2_Packet_AppendU16(rsp, TPM_ALG_HASH_MLDSA); + TPM2_Packet_AppendU16(rsp, phAlg); + TPM2_Packet_AppendU16(rsp, sigOut->size); + TPM2_Packet_AppendBytes(rsp, sigOut->buffer, sigOut->size); + } + TPM2_ForceZero(digest, sizeof(digest)); + } + + FWTPM_FREE_VAR(sigOut); + return rc; +} +#endif /* WOLFTPM_MLDSA_SIGN */ + /* Helper: sign attestation buffer with signing key. * attestBuf/attestSz: serialized TPMS_ATTEST bytes * obj: signing key object @@ -4131,6 +4197,28 @@ TPM_RC FwSignAttest(FWTPM_CTX* ctx, FWTPM_Object* obj, return TPM_RC_KEY; } +#ifdef WOLFTPM_PQC + /* An ML-DSA selector on a classical key is invalid (Part 3 Sec.18.1): + * reject so an ECDSA/RSA signature is not emitted under an ML-DSA tag. */ + if ((sigScheme == TPM_ALG_MLDSA || sigScheme == TPM_ALG_HASH_MLDSA) && + sigScheme != obj->pub.type) { + return TPM_RC_SCHEME; + } +#endif + +#ifdef WOLFTPM_MLDSA_SIGN + /* ML-DSA keys sign TPMS_ATTEST bytes directly (FIPS 204); the key type + * fixes the scheme, so a non-null requested scheme must match it (Part 3 + * Sec.18.1) or the sign is rejected with TPM_RC_SCHEME. */ + if (obj->pub.type == TPM_ALG_MLDSA || + obj->pub.type == TPM_ALG_HASH_MLDSA) { + if (sigScheme != TPM_ALG_NULL && sigScheme != obj->pub.type) { + return TPM_RC_SCHEME; + } + return FwSignAttestMldsa(ctx, obj, attestBuf, attestSz, rsp); + } +#endif /* WOLFTPM_MLDSA_SIGN */ + /* Resolve scheme/hash from key if NULL */ FwResolveSignScheme(obj, &sigScheme, &sigHashAlg); @@ -4191,11 +4279,12 @@ TPM_RC FwCredentialDeriveKeys( } /* Encrypt credential and compute outer HMAC (MakeCredential direction). - * encCred = AES-128-CFB(symKey, 0-IV, size(2) || credential) + * encCred = AES-CFB(symKey, 0-IV, size(2) || credential) * outerHmac = HMAC(hmacKey, encCred || name) */ TPM_RC FwCredentialWrap( const byte* symKey, int symKeySz, const byte* hmacKey, int hmacKeySz, + TPMI_ALG_HASH nameAlg, const byte* credential, UINT16 credSz, const byte* name, int nameSz, byte* encCred, word32* encCredSz, @@ -4229,7 +4318,8 @@ TPM_RC FwCredentialWrap( if (rc == 0) { rc = wc_HmacInit(hmac, NULL, INVALID_DEVID); if (rc == 0) - rc = wc_HmacSetKey(hmac, WC_SHA256, hmacKey, (word32)hmacKeySz); + rc = wc_HmacSetKey(hmac, FwGetWcHashType(nameAlg), + hmacKey, (word32)hmacKeySz); if (rc == 0) rc = wc_HmacUpdate(hmac, encCred, *encCredSz); if (rc == 0) @@ -4252,6 +4342,7 @@ TPM_RC FwCredentialWrap( TPM_RC FwCredentialUnwrap( const byte* symKey, int symKeySz, const byte* hmacKey, int hmacKeySz, + TPMI_ALG_HASH nameAlg, const byte* blobBuf, UINT16 blobSz, const byte* name, int nameSz, byte* credOut, int credBufSz, UINT16* credSzOut) @@ -4259,8 +4350,9 @@ TPM_RC FwCredentialUnwrap( TPM_RC rc = TPM_RC_SUCCESS; TPM2_Packet blobPkt; UINT16 integrityHmacSz = 0; - byte integrityHmac[TPM_SHA256_DIGEST_SIZE]; - byte computedHmac[TPM_SHA256_DIGEST_SIZE]; + int hmacDigestSz = TPM2_GetHashDigestSize(nameAlg); + byte integrityHmac[TPM_MAX_DIGEST_SIZE]; + byte computedHmac[TPM_MAX_DIGEST_SIZE]; const byte* encIdentity; int encIdentitySz; byte iv[AES_BLOCK_SIZE]; @@ -4283,7 +4375,10 @@ TPM_RC FwCredentialUnwrap( blobPkt.pos = 0; blobPkt.size = blobSz; TPM2_Packet_ParseU16(&blobPkt, &integrityHmacSz); - if (integrityHmacSz > TPM_SHA256_DIGEST_SIZE) { + if (hmacDigestSz <= 0 || hmacDigestSz > TPM_MAX_DIGEST_SIZE) { + rc = TPM_RC_HASH; + } + else if (integrityHmacSz > (UINT16)hmacDigestSz) { rc = TPM_RC_SIZE; } } @@ -4300,7 +4395,8 @@ TPM_RC FwCredentialUnwrap( if (rc == 0) { rc = wc_HmacInit(hmac, NULL, INVALID_DEVID); if (rc == 0) - rc = wc_HmacSetKey(hmac, WC_SHA256, hmacKey, (word32)hmacKeySz); + rc = wc_HmacSetKey(hmac, FwGetWcHashType(nameAlg), + hmacKey, (word32)hmacKeySz); if (rc == 0) rc = wc_HmacUpdate(hmac, encIdentity, encIdentitySz); if (rc == 0) @@ -4314,9 +4410,9 @@ TPM_RC FwCredentialUnwrap( } if (rc == 0) { /* Always run TPM2_ConstantCompare so timing doesn't leak size match */ - sizeMismatch = (integrityHmacSz != TPM_SHA256_DIGEST_SIZE); + sizeMismatch = (integrityHmacSz != (UINT16)hmacDigestSz); hmacDiff = TPM2_ConstantCompare(computedHmac, integrityHmac, - TPM_SHA256_DIGEST_SIZE); + (word32)hmacDigestSz); if (sizeMismatch | hmacDiff) { rc = TPM_RC_INTEGRITY; } diff --git a/src/tpm2_packet.c b/src/tpm2_packet.c index 5a2da3812..5dee7e983 100644 --- a/src/tpm2_packet.c +++ b/src/tpm2_packet.c @@ -703,6 +703,13 @@ void TPM2_Packet_AppendEccScheme(TPM2_Packet* packet, TPMT_SIG_SCHEME* scheme) TPM2_Packet_AppendU16(packet, scheme->details.ecdaa.hashAlg); TPM2_Packet_AppendU16(packet, scheme->details.ecdaa.count); } +#ifdef WOLFTPM_PQC + else if (scheme->scheme == TPM_ALG_MLDSA || + scheme->scheme == TPM_ALG_HASH_MLDSA) { + /* ML-DSA scheme union arms are TPMS_EMPTY (TCG v185 errata): the + * selector carries no trailing hash. */ + } +#endif else if (scheme->scheme != TPM_ALG_NULL) { TPM2_Packet_AppendU16(packet, scheme->details.any.hashAlg); } diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index 43b3c82fd..1db624485 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -2239,17 +2239,29 @@ static int BuildCreatePrimaryCmdEx(byte* buf, TPM_ALG_ID algType, } #ifdef WOLFTPM_V185 else if (algType == TPM_ALG_MLKEM) { - /* MLKEM-768 decrypt-only primary. Attributes: + /* MLKEM-768 decrypt primary. Default attributes: * fixedTPM|fixedParent|sensitiveDataOrigin|userWithAuth|decrypt */ + int mlkemRestricted; PutU16BE(buf + pos, TPM_ALG_MLKEM); pos += 2; PutU16BE(buf + pos, TPM_ALG_SHA256); pos += 2; if (objectAttributes == 0) { objectAttributes = 0x00020072; } + mlkemRestricted = + (objectAttributes & TPMA_OBJECT_restricted) && + (objectAttributes & TPMA_OBJECT_decrypt); PutU32BE(buf + pos, objectAttributes); pos += 4; PutU16BE(buf + pos, 0); pos += 2; /* authPolicy */ - /* TPMS_MLKEM_PARMS: symmetric(TPM_ALG_NULL) + parameterSet */ - PutU16BE(buf + pos, TPM_ALG_NULL); pos += 2; + /* TPMS_MLKEM_PARMS: symmetric + parameterSet. A restricted decryption + * (Storage) key requires a non-NULL symmetric; unrestricted uses NULL. */ + if (mlkemRestricted) { + PutU16BE(buf + pos, TPM_ALG_AES); pos += 2; + PutU16BE(buf + pos, 128); pos += 2; + PutU16BE(buf + pos, TPM_ALG_CFB); pos += 2; + } + else { + PutU16BE(buf + pos, TPM_ALG_NULL); pos += 2; + } PutU16BE(buf + pos, TPM_MLKEM_768); pos += 2; /* unique.mlkem (TPM2B): size=0 — TPM derives */ PutU16BE(buf + pos, 0); pos += 2; @@ -2851,107 +2863,957 @@ static void RunEccDhkemRoundtrip(UINT16 curveID, UINT16 hashAlg, pos = 0; PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; PutU32BE(gCmd + pos, 0); pos += 4; - PutU32BE(gCmd + pos, TPM_CC_Decapsulate); pos += 4; - PutU32BE(gCmd + pos, keyHandle); pos += 4; - PutU32BE(gCmd + pos, 9); pos += 4; - PutU32BE(gCmd + pos, TPM_RS_PW); pos += 4; - PutU16BE(gCmd + pos, 0); pos += 2; - gCmd[pos++] = 0; - PutU16BE(gCmd + pos, 0); pos += 2; - PutU16BE(gCmd + pos, ctSz); pos += 2; - memcpy(gCmd + pos, ct, ctSz); pos += ctSz; + PutU32BE(gCmd + pos, TPM_CC_Decapsulate); pos += 4; + PutU32BE(gCmd + pos, keyHandle); pos += 4; + PutU32BE(gCmd + pos, 9); pos += 4; + PutU32BE(gCmd + pos, TPM_RS_PW); pos += 4; + PutU16BE(gCmd + pos, 0); pos += 2; + gCmd[pos++] = 0; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + pos, ctSz); pos += 2; + memcpy(gCmd + pos, ct, ctSz); pos += ctSz; + PutU32BE(gCmd + 2, (UINT32)pos); + rspSize = 0; + rc = FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); + AssertIntEQ(rc, TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + pos = TPM2_HEADER_SIZE + 4; + ssSz2 = GetU16BE(gRsp + pos); pos += 2; + AssertIntEQ((int)ssSz1, (int)ssSz2); + memcpy(ss2, gRsp + pos, ssSz2); + + AssertIntEQ(XMEMCMP(ss1, ss2, ssSz1), 0); + + FWTPM_Cleanup(&ctx); + fwtpm_pass(label, 1); +} + +static void test_fwtpm_ecc_dhkem_roundtrip(void) +{ + RunEccDhkemRoundtrip(TPM_ECC_NIST_P256, TPM_ALG_SHA256, + "Encap/Decap ECC DHKEM (P-256/HKDF-SHA256) Roundtrip:"); +} + +static void test_fwtpm_ecc_dhkem_p384_roundtrip(void) +{ + RunEccDhkemRoundtrip(TPM_ECC_NIST_P384, TPM_ALG_SHA384, + "Encap/Decap ECC DHKEM (P-384/HKDF-SHA384) Roundtrip:"); +} + +#ifdef FWTPM_HAVE_ECC521 +static void test_fwtpm_ecc_dhkem_p521_roundtrip(void) +{ + RunEccDhkemRoundtrip(TPM_ECC_NIST_P521, TPM_ALG_SHA512, + "Encap/Decap ECC DHKEM (P-521/HKDF-SHA512) Roundtrip:"); +} +#endif + +/* ML-KEM Labeled KEM seed roundtrip per Part 1 Sec.47.4 Eq.66: + * seed = KDFa(nameAlg, K, label, ciphertext, publicKey, bits). + * FwEncryptSeed(MLKEM) outputs (seed1, ciphertext); FwDecryptSeed(MLKEM) + * reconstructs seed2 from ciphertext and the same key. seed1 must equal seed2. */ +static void test_fwtpm_encseed_decseed_mlkem_roundtrip(void) +{ + FWTPM_CTX ctx; + int rc, rspSize, cmdSz; + UINT32 keyHandle; + FWTPM_Object* keyObj = NULL; + int oi; + byte seed1[64], seed2[64]; + int seed1Sz = 0, seed2Sz = 0; + FWTPM_DECLARE_BUF(encSeed, 2048); + int encSeedSz = 0; + + FWTPM_ALLOC_BUF(encSeed, 2048); + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + + cmdSz = BuildCreatePrimaryCmd(gCmd, TPM_ALG_MLKEM); + rspSize = 0; + rc = FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0); + AssertIntEQ(rc, TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + keyHandle = GetU32BE(gRsp + TPM2_HEADER_SIZE); + AssertIntNE(keyHandle, 0); + + for (oi = 0; oi < FWTPM_MAX_OBJECTS; oi++) { + if (ctx.objects[oi].handle == keyHandle) { + keyObj = &ctx.objects[oi]; + break; + } + } + AssertNotNull(keyObj); + + rc = FwEncryptSeed(&ctx, keyObj, + NULL, 0, "SECRET", + seed1, (int)sizeof(seed1), &seed1Sz, + encSeed, 2048, &encSeedSz); + AssertIntEQ(rc, TPM_RC_SUCCESS); + AssertIntGT(seed1Sz, 0); + AssertIntGT(encSeedSz, 0); + + rc = FwDecryptSeed(&ctx, keyObj, + encSeed, (UINT16)encSeedSz, + NULL, 0, "SECRET", + seed2, (int)sizeof(seed2), &seed2Sz); + AssertIntEQ(rc, TPM_RC_SUCCESS); + AssertIntEQ(seed1Sz, seed2Sz); + AssertIntEQ(XMEMCMP(seed1, seed2, seed1Sz), 0); + + FWTPM_Cleanup(&ctx); + FWTPM_FREE_BUF(encSeed); + fwtpm_pass("FwEncryptSeed/FwDecryptSeed MLKEM Roundtrip:", 1); +} + +/* FlushHandle is defined later in this file. */ +static void FlushHandle(FWTPM_CTX* ctx, UINT32 handle); + +#if defined(WOLFTPM_MLKEM_ENCAP) && defined(WOLFTPM_MLKEM_DECAP) && \ + !defined(FWTPM_NO_CREDENTIAL) +/* Run MakeCredential(ekHandle, credential, ekName) and copy the returned + * credentialBlob and secret TPM2Bs out for a follow-up ActivateCredential. */ +static void MlkemMakeCredential(FWTPM_CTX* ctx, UINT32 ekHandle, + const byte* credential, int credSz, + const byte* name, UINT16 nameSz, + byte* blob, UINT16* blobSzOut, + byte* secret, UINT16* secretSzOut) +{ + int pos, rspSize = 0; + UINT16 blobSz, secretSz; + + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_NO_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_MakeCredential); pos += 4; + PutU32BE(gCmd + pos, ekHandle); pos += 4; + PutU16BE(gCmd + pos, (UINT16)credSz); pos += 2; + memcpy(gCmd + pos, credential, credSz); pos += credSz; + PutU16BE(gCmd + pos, nameSz); pos += 2; + memcpy(gCmd + pos, name, nameSz); pos += nameSz; + PutU32BE(gCmd + 2, (UINT32)pos); + AssertIntEQ(FWTPM_ProcessCommand(ctx, gCmd, pos, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + + /* Response (NO_SESSIONS): credentialBlob TPM2B | secret TPM2B. */ + pos = TPM2_HEADER_SIZE; + blobSz = GetU16BE(gRsp + pos); pos += 2; + AssertIntGT(blobSz, 0); + AssertIntEQ(pos + blobSz + 2 <= rspSize, 1); + memcpy(blob, gRsp + pos, blobSz); pos += blobSz; + secretSz = GetU16BE(gRsp + pos); pos += 2; + AssertIntGT(secretSz, 0); + AssertIntEQ(pos + secretSz <= rspSize, 1); + memcpy(secret, gRsp + pos, secretSz); + *blobSzOut = blobSz; + *secretSzOut = secretSz; +} + +/* Build ActivateCredential(activate=EK, key=EK, blob, secret) with two empty + * password auth sessions. Returns the command length. */ +static int BuildMlkemActivateCredentialCmd(UINT32 ekHandle, + const byte* blob, UINT16 blobSz, const byte* secret, UINT16 secretSz) +{ + int pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_ActivateCredential); pos += 4; + PutU32BE(gCmd + pos, ekHandle); pos += 4; /* activateHandle */ + PutU32BE(gCmd + pos, ekHandle); pos += 4; /* keyHandle */ + PutU32BE(gCmd + pos, 18); pos += 4; /* authAreaSize: 2 PW sessions */ + PutU32BE(gCmd + pos, TPM_RS_PW); pos += 4; + PutU16BE(gCmd + pos, 0); pos += 2; gCmd[pos++] = 0; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU32BE(gCmd + pos, TPM_RS_PW); pos += 4; + PutU16BE(gCmd + pos, 0); pos += 2; gCmd[pos++] = 0; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + pos, blobSz); pos += 2; + memcpy(gCmd + pos, blob, blobSz); pos += blobSz; + PutU16BE(gCmd + pos, secretSz); pos += 2; + memcpy(gCmd + pos, secret, secretSz); pos += secretSz; + PutU32BE(gCmd + 2, (UINT32)pos); + return pos; +} + +/* Create a restricted ML-KEM-768 Storage Key (a valid credential key per + * Part 3 Sec.24) and capture its Name. The EK is also the activate object, so + * ActivateCredential recomputes the same Name. */ +static UINT32 CreateMlkemEkWithName(FWTPM_CTX* ctx, byte* name, UINT16* nameSzOut) +{ + int cmdSz, rspSize = 0, oi; + UINT32 ekHandle; + FWTPM_Object* ek = NULL; + + /* restricted|decrypt|fixedTPM|fixedParent|sensitiveDataOrigin| + * userWithAuth|noDA (AES-128-CFB symmetric added by the template). */ + cmdSz = BuildCreatePrimaryCmdEx(gCmd, TPM_ALG_MLKEM, 0x00030472); + AssertIntGT(cmdSz, 0); + AssertIntEQ(FWTPM_ProcessCommand(ctx, gCmd, cmdSz, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + ekHandle = GetU32BE(gRsp + TPM2_HEADER_SIZE); + AssertIntNE(ekHandle, 0); + + for (oi = 0; oi < FWTPM_MAX_OBJECTS; oi++) { + if (ctx->objects[oi].handle == ekHandle) { + ek = &ctx->objects[oi]; + break; + } + } + AssertNotNull(ek); + if (ek->name.size == 0) { + FwComputeObjectName(ek); + } + AssertIntGT(ek->name.size, 0); + memcpy(name, ek->name.name, ek->name.size); + *nameSzOut = ek->name.size; + return ekHandle; +} + +/* End-to-end MakeCredential -> ActivateCredential with an ML-KEM decrypt key + * (issue #589: ActivateCredential rejected ML-KEM with TPM_RC_KEY). Confirms + * the credential round trip recovers the original secret. */ +static void test_fwtpm_mlkem_credential_roundtrip(void) +{ + FWTPM_CTX ctx; + int rspSize = 0, cmdSz, pos; + UINT32 ekHandle; + byte credential[16]; + UINT16 nameSz; + byte name[sizeof(TPM2B_NAME)]; + byte blob[sizeof(TPM2B_ID_OBJECT)]; + UINT16 blobSz; + byte secret[sizeof(TPM2B_ENCRYPTED_SECRET)]; + UINT16 secretSz, recoveredSz; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + + ekHandle = CreateMlkemEkWithName(&ctx, name, &nameSz); + memset(credential, 0xC7, sizeof(credential)); + MlkemMakeCredential(&ctx, ekHandle, credential, (int)sizeof(credential), + name, nameSz, blob, &blobSz, secret, &secretSz); + + /* Before the fix the ML-KEM key type returns TPM_RC_KEY here. */ + cmdSz = BuildMlkemActivateCredentialCmd(ekHandle, blob, blobSz, + secret, secretSz); + AssertIntEQ(FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + + /* Response (SESSIONS): paramSize(4) | certInfo TPM2B_DIGEST. */ + pos = TPM2_HEADER_SIZE + 4; + recoveredSz = GetU16BE(gRsp + pos); pos += 2; + AssertIntEQ((int)recoveredSz, (int)sizeof(credential)); + AssertIntEQ(pos + (int)recoveredSz <= rspSize, 1); + AssertIntEQ(XMEMCMP(gRsp + pos, credential, sizeof(credential)), 0); + + FlushHandle(&ctx, ekHandle); + FWTPM_Cleanup(&ctx); + fwtpm_pass("MLKEM MakeCredential/ActivateCredential roundtrip:", 1); +} + +/* Negative: a tampered credentialBlob must be rejected by the outer HMAC, + * proving ActivateCredential does not blindly trust the ML-KEM-decapsulated + * seed. Flipping a byte inside the integrity HMAC yields a non-success RC. */ +static void test_fwtpm_mlkem_activatecredential_tampered_rejected(void) +{ + FWTPM_CTX ctx; + int rspSize = 0, cmdSz; + UINT32 ekHandle; + byte credential[16]; + UINT16 nameSz; + byte name[sizeof(TPM2B_NAME)]; + byte blob[sizeof(TPM2B_ID_OBJECT)]; + UINT16 blobSz; + byte secret[sizeof(TPM2B_ENCRYPTED_SECRET)]; + UINT16 secretSz; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + + ekHandle = CreateMlkemEkWithName(&ctx, name, &nameSz); + memset(credential, 0x5A, sizeof(credential)); + MlkemMakeCredential(&ctx, ekHandle, credential, (int)sizeof(credential), + name, nameSz, blob, &blobSz, secret, &secretSz); + + /* Flip a byte inside the integrity HMAC (blob layout: size(2) | + * HMAC(TPM2B) | encIdentity). */ + AssertIntGT(blobSz, 8); + blob[8] ^= 0xFF; + + cmdSz = BuildMlkemActivateCredentialCmd(ekHandle, blob, blobSz, + secret, secretSz); + AssertIntEQ(FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); + AssertIntNE(GetRspRC(gRsp), TPM_RC_SUCCESS); + + FlushHandle(&ctx, ekHandle); + FWTPM_Cleanup(&ctx); + fwtpm_pass("MLKEM ActivateCredential tamper rejected:", 1); +} + +/* An unrestricted ML-KEM decrypt key is not a Storage Key: ActivateCredential + * must reject it (TPM_RC_ATTRIBUTES) before any decryption, so it cannot be + * paired with an out-of-band Decapsulate to recover the credential seed. */ +static void test_fwtpm_mlkem_activatecredential_unrestricted_rejected(void) +{ + FWTPM_CTX ctx; + int rspSize = 0, cmdSz; + UINT32 ekHandle; + byte blob[36]; + byte secret[64]; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + + /* Unrestricted ML-KEM decrypt key (default template, symmetric NULL). */ + cmdSz = BuildCreatePrimaryCmd(gCmd, TPM_ALG_MLKEM); + AssertIntGT(cmdSz, 0); + AssertIntEQ(FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + ekHandle = GetU32BE(gRsp + TPM2_HEADER_SIZE); + AssertIntNE(ekHandle, 0); + + /* Dummy blob/secret: the attribute gate fires before they are used. */ + memset(blob, 0, sizeof(blob)); + memset(secret, 0, sizeof(secret)); + cmdSz = BuildMlkemActivateCredentialCmd(ekHandle, blob, sizeof(blob), + secret, sizeof(secret)); + AssertIntEQ(FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_ATTRIBUTES); + + FlushHandle(&ctx, ekHandle); + FWTPM_Cleanup(&ctx); + fwtpm_pass("MLKEM ActivateCredential unrestricted rejected:", 1); +} + +#ifdef WOLFSSL_SHA384 +/* The EK Credential Profile defines the ML-KEM-768 EK as SHA-384/AES-256-CFB. + * The credential path derives the HMAC under the key's nameAlg (SHA-384) and + * the symmetric key at its declared AES-256 size, so a full round trip + * recovers the secret with a standard-parameter EK. */ +static void test_fwtpm_mlkem_credential_sha384_aes256_roundtrip(void) +{ + FWTPM_CTX ctx; + int rspSize = 0, cmdSz, pos, pubStart, oi; + UINT32 ekHandle; + FWTPM_Object* ek = NULL; + byte credential[16]; + UINT16 nameSz; + byte name[sizeof(TPM2B_NAME)]; + byte blob[sizeof(TPM2B_ID_OBJECT)]; + UINT16 blobSz; + byte secret[sizeof(TPM2B_ENCRYPTED_SECRET)]; + UINT16 secretSz, recoveredSz; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + + /* CreatePrimary restricted ML-KEM-768 Storage Key: SHA-384 nameAlg, + * AES-256-CFB symmetric. */ + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_CreatePrimary); pos += 4; + PutU32BE(gCmd + pos, TPM_RH_OWNER); pos += 4; + PutU32BE(gCmd + pos, 9); pos += 4; + PutU32BE(gCmd + pos, TPM_RS_PW); pos += 4; + PutU16BE(gCmd + pos, 0); pos += 2; gCmd[pos++] = 0; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + pos, 4); pos += 2; /* inSensitive */ + PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + pos, 0); pos += 2; + pubStart = pos; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + pos, TPM_ALG_MLKEM); pos += 2; + PutU16BE(gCmd + pos, TPM_ALG_SHA384); pos += 2; /* nameAlg */ + PutU32BE(gCmd + pos, 0x00030472); pos += 4; /* restricted|decrypt|... */ + PutU16BE(gCmd + pos, 0); pos += 2; /* authPolicy */ + PutU16BE(gCmd + pos, TPM_ALG_AES); pos += 2; + PutU16BE(gCmd + pos, 256); pos += 2; /* AES-256 */ + PutU16BE(gCmd + pos, TPM_ALG_CFB); pos += 2; + PutU16BE(gCmd + pos, TPM_MLKEM_768); pos += 2; + PutU16BE(gCmd + pos, 0); pos += 2; /* unique */ + PutU16BE(gCmd + pubStart, (UINT16)(pos - pubStart - 2)); + PutU16BE(gCmd + pos, 0); pos += 2; /* outsideInfo */ + PutU32BE(gCmd + pos, 0); pos += 4; /* creationPCR */ + PutU32BE(gCmd + 2, (UINT32)pos); + AssertIntEQ(FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + ekHandle = GetU32BE(gRsp + TPM2_HEADER_SIZE); + AssertIntNE(ekHandle, 0); + + for (oi = 0; oi < FWTPM_MAX_OBJECTS; oi++) { + if (ctx.objects[oi].handle == ekHandle) { + ek = &ctx.objects[oi]; + break; + } + } + AssertNotNull(ek); + if (ek->name.size == 0) { + FwComputeObjectName(ek); + } + nameSz = ek->name.size; + AssertIntGT(nameSz, 0); + memcpy(name, ek->name.name, nameSz); + + memset(credential, 0x3C, sizeof(credential)); + MlkemMakeCredential(&ctx, ekHandle, credential, (int)sizeof(credential), + name, nameSz, blob, &blobSz, secret, &secretSz); + + cmdSz = BuildMlkemActivateCredentialCmd(ekHandle, blob, blobSz, + secret, secretSz); + AssertIntEQ(FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + + pos = TPM2_HEADER_SIZE + 4; + recoveredSz = GetU16BE(gRsp + pos); pos += 2; + AssertIntEQ((int)recoveredSz, (int)sizeof(credential)); + AssertIntEQ(pos + (int)recoveredSz <= rspSize, 1); + AssertIntEQ(XMEMCMP(gRsp + pos, credential, sizeof(credential)), 0); + + FlushHandle(&ctx, ekHandle); + FWTPM_Cleanup(&ctx); + fwtpm_pass("MLKEM SHA-384/AES-256 credential roundtrip:", 1); +} +#endif /* WOLFSSL_SHA384 */ +#endif /* WOLFTPM_MLKEM && !FWTPM_NO_CREDENTIAL */ + +#if defined(WOLFTPM_MLDSA_SIGN) && defined(WOLFTPM_MLDSA_VERIFY) && \ + !defined(FWTPM_NO_ATTESTATION) +/* Create a restricted ML-DSA (or Hash-ML-DSA) attestation key and return its + * handle; *akOut points at the in-memory object for public-key access during + * verification. */ +static UINT32 CreatePrimaryMldsaAkHelper(FWTPM_CTX* ctx, TPM_ALG_ID algType, + FWTPM_Object** akOut) +{ + int cmdSz, rspSize = 0, oi; + UINT32 h; + + *akOut = NULL; + cmdSz = BuildCreatePrimaryCmdEx(gCmd, algType, 0x00050072); + AssertIntGT(cmdSz, 0); + AssertIntEQ(FWTPM_ProcessCommand(ctx, gCmd, cmdSz, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + h = GetU32BE(gRsp + TPM2_HEADER_SIZE); + AssertIntNE(h, 0); + for (oi = 0; oi < FWTPM_MAX_OBJECTS; oi++) { + if (ctx->objects[oi].handle == h) { + *akOut = &ctx->objects[oi]; + break; + } + } + AssertNotNull(*akOut); + return h; +} + +/* Append two empty password auth sessions (signHandle + object/priv auth). */ +static int AppendTwoPwAuth(byte* buf, int pos) +{ + PutU32BE(buf + pos, 18); pos += 4; /* authAreaSize: 2 * 9 */ + PutU32BE(buf + pos, TPM_RS_PW); pos += 4; + PutU16BE(buf + pos, 0); pos += 2; buf[pos++] = 0; PutU16BE(buf + pos, 0); + pos += 2; + PutU32BE(buf + pos, TPM_RS_PW); pos += 4; + PutU16BE(buf + pos, 0); pos += 2; buf[pos++] = 0; PutU16BE(buf + pos, 0); + pos += 2; + return pos; +} + +/* Parse a SESSIONS attestation response (paramSize | TPM2B_ATTEST | + * TPMT_SIGNATURE) currently in gRsp and verify the Pure ML-DSA signature over + * the attest bytes with the wolfCrypt verifier, as an external verifier would. + * All offsets are bound-checked against rspSize before use. */ +static void VerifyMldsaAttestResponse(int rspSize, FWTPM_Object* ak) +{ + int pos = TPM2_HEADER_SIZE + 4; /* skip paramSize */ + UINT16 attestSz, sigAlg, sigSz; + const byte* attestBuf; + const byte* sig; + + AssertIntEQ(pos + 2 <= rspSize, 1); + attestSz = GetU16BE(gRsp + pos); pos += 2; + AssertIntGT(attestSz, 0); + AssertIntEQ(pos + attestSz + 4 <= rspSize, 1); + attestBuf = gRsp + pos; pos += attestSz; + sigAlg = GetU16BE(gRsp + pos); pos += 2; + AssertIntEQ(sigAlg, TPM_ALG_MLDSA); + sigSz = GetU16BE(gRsp + pos); pos += 2; + AssertIntGT(sigSz, 0); + AssertIntEQ(pos + (int)sigSz <= rspSize, 1); + sig = gRsp + pos; + AssertIntEQ(FwVerifyMldsaMessage( + ak->pub.parameters.mldsaDetail.parameterSet, + &ak->pub.unique.mldsa, NULL, 0, + attestBuf, (int)attestSz, sig, (int)sigSz), TPM_RC_SUCCESS); +} + +/* End-to-end TPM2_Quote with a restricted Pure ML-DSA signing key, verified + * with the wolfCrypt ML-DSA verifier (issue #589: Quote returned TPM_RC_KEY + * before FwSignAttest signed the TPMS_ATTEST bytes with ML-DSA). */ +static void test_fwtpm_mldsa_quote_sign_and_verify(void) +{ + FWTPM_CTX ctx; + int rspSize = 0, pos; + UINT32 akHandle; + FWTPM_Object* ak = NULL; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + + akHandle = CreatePrimaryMldsaAkHelper(&ctx, TPM_ALG_MLDSA, &ak); + + /* Quote with the key's own scheme (inScheme NULL) over PCR0/1 (SHA-256). + * One PW auth session; qualifyingData empty. */ + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_Quote); pos += 4; + PutU32BE(gCmd + pos, akHandle); pos += 4; + PutU32BE(gCmd + pos, 9); pos += 4; /* authAreaSize: 1 PW session */ + PutU32BE(gCmd + pos, TPM_RS_PW); pos += 4; + PutU16BE(gCmd + pos, 0); pos += 2; gCmd[pos++] = 0; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + pos, 0); pos += 2; /* qualifyingData */ + PutU16BE(gCmd + pos, TPM_ALG_NULL); pos += 2; /* inScheme */ + PutU32BE(gCmd + pos, 1); pos += 4; /* PCRselect count */ + PutU16BE(gCmd + pos, TPM_ALG_SHA256); pos += 2; + gCmd[pos++] = 3; gCmd[pos++] = 0; gCmd[pos++] = 0; gCmd[pos++] = 0; + PutU32BE(gCmd + 2, (UINT32)pos); + AssertIntEQ(FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + VerifyMldsaAttestResponse(rspSize, ak); + + FlushHandle(&ctx, akHandle); + FWTPM_Cleanup(&ctx); + fwtpm_pass("MLDSA Quote sign and verify:", 1); +} + +/* An explicit wire scheme that does not match the ML-DSA key must be rejected + * with TPM_RC_SCHEME (Part 3 Sec.18.1), not silently signed with ML-DSA. */ +static void test_fwtpm_mldsa_quote_scheme_mismatch_rejected(void) +{ + FWTPM_CTX ctx; + int rspSize = 0, pos; + UINT32 akHandle; + FWTPM_Object* ak = NULL; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + + akHandle = CreatePrimaryMldsaAkHelper(&ctx, TPM_ALG_MLDSA, &ak); + + /* Quote with an explicit ECDSA-SHA256 inScheme on an ML-DSA key. */ + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_Quote); pos += 4; + PutU32BE(gCmd + pos, akHandle); pos += 4; + PutU32BE(gCmd + pos, 9); pos += 4; + PutU32BE(gCmd + pos, TPM_RS_PW); pos += 4; + PutU16BE(gCmd + pos, 0); pos += 2; gCmd[pos++] = 0; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + pos, 0); pos += 2; /* qualifyingData */ + PutU16BE(gCmd + pos, TPM_ALG_ECDSA); pos += 2; /* mismatched inScheme */ + PutU16BE(gCmd + pos, TPM_ALG_SHA256); pos += 2; + PutU32BE(gCmd + pos, 1); pos += 4; + PutU16BE(gCmd + pos, TPM_ALG_SHA256); pos += 2; + gCmd[pos++] = 3; gCmd[pos++] = 0; gCmd[pos++] = 0; gCmd[pos++] = 0; + PutU32BE(gCmd + 2, (UINT32)pos); + AssertIntEQ(FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SCHEME); + + FlushHandle(&ctx, akHandle); + FWTPM_Cleanup(&ctx); + fwtpm_pass("MLDSA Quote scheme mismatch rejected:", 1); +} + +/* An explicit TPM_ALG_MLDSA inScheme is TPMS_EMPTY (no trailing hash per TCG + * v185 errata): the parser must not consume a hash, so the PCRselect that + * follows stays aligned and the Quote succeeds and verifies. */ +static void test_fwtpm_mldsa_quote_explicit_empty_scheme(void) +{ + FWTPM_CTX ctx; + int rspSize = 0, pos; + UINT32 akHandle; + FWTPM_Object* ak = NULL; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + + akHandle = CreatePrimaryMldsaAkHelper(&ctx, TPM_ALG_MLDSA, &ak); + + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_Quote); pos += 4; + PutU32BE(gCmd + pos, akHandle); pos += 4; + PutU32BE(gCmd + pos, 9); pos += 4; + PutU32BE(gCmd + pos, TPM_RS_PW); pos += 4; + PutU16BE(gCmd + pos, 0); pos += 2; gCmd[pos++] = 0; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + pos, 0); pos += 2; /* qualifyingData */ + PutU16BE(gCmd + pos, TPM_ALG_MLDSA); pos += 2; /* explicit, no hashAlg */ + PutU32BE(gCmd + pos, 1); pos += 4; /* PCRselect count */ + PutU16BE(gCmd + pos, TPM_ALG_SHA256); pos += 2; + gCmd[pos++] = 3; gCmd[pos++] = 0; gCmd[pos++] = 0; gCmd[pos++] = 0; + PutU32BE(gCmd + 2, (UINT32)pos); + AssertIntEQ(FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + VerifyMldsaAttestResponse(rspSize, ak); + + FlushHandle(&ctx, akHandle); + FWTPM_Cleanup(&ctx); + fwtpm_pass("MLDSA Quote explicit empty scheme:", 1); +} + +#ifdef HAVE_ECC +/* An explicit ML-DSA selector paired with a classical (ECC) attestation key + * must be rejected with TPM_RC_SCHEME, not signed and mis-tagged as ML-DSA. */ +static void test_fwtpm_quote_mldsa_scheme_on_ecc_rejected(void) +{ + FWTPM_CTX ctx; + int rspSize = 0, pos, pubStart, sensStart; + UINT32 keyH; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + + /* CreatePrimary ECC P-256 restricted signing key (ECDSA-SHA256). */ + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_CreatePrimary); pos += 4; + PutU32BE(gCmd + pos, TPM_RH_OWNER); pos += 4; + PutU32BE(gCmd + pos, 9); pos += 4; + PutU32BE(gCmd + pos, TPM_RS_PW); pos += 4; + PutU16BE(gCmd + pos, 0); pos += 2; gCmd[pos++] = 0; + PutU16BE(gCmd + pos, 0); pos += 2; + sensStart = pos; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + sensStart, (UINT16)(pos - sensStart - 2)); + pubStart = pos; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + pos, TPM_ALG_ECC); pos += 2; + PutU16BE(gCmd + pos, TPM_ALG_SHA256); pos += 2; /* nameAlg */ + PutU32BE(gCmd + pos, 0x00050072); pos += 4; /* restricted|sign|... */ + PutU16BE(gCmd + pos, 0); pos += 2; /* authPolicy */ + PutU16BE(gCmd + pos, TPM_ALG_NULL); pos += 2; /* symmetric (sign) */ + PutU16BE(gCmd + pos, TPM_ALG_ECDSA); pos += 2; /* scheme */ + PutU16BE(gCmd + pos, TPM_ALG_SHA256); pos += 2; /* scheme hash */ + PutU16BE(gCmd + pos, TPM_ECC_NIST_P256); pos += 2; + PutU16BE(gCmd + pos, TPM_ALG_NULL); pos += 2; /* kdf */ + PutU16BE(gCmd + pos, 0); pos += 2; /* unique.x */ + PutU16BE(gCmd + pos, 0); pos += 2; /* unique.y */ + PutU16BE(gCmd + pubStart, (UINT16)(pos - pubStart - 2)); + PutU16BE(gCmd + pos, 0); pos += 2; /* outsideInfo */ + PutU32BE(gCmd + pos, 0); pos += 4; /* creationPCR */ + PutU32BE(gCmd + 2, (UINT32)pos); + AssertIntEQ(FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + keyH = GetU32BE(gRsp + TPM2_HEADER_SIZE); + AssertIntNE(keyH, 0); + + /* Quote with an explicit ML-DSA inScheme (TPMS_EMPTY, no hashAlg). */ + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_Quote); pos += 4; + PutU32BE(gCmd + pos, keyH); pos += 4; + PutU32BE(gCmd + pos, 9); pos += 4; + PutU32BE(gCmd + pos, TPM_RS_PW); pos += 4; + PutU16BE(gCmd + pos, 0); pos += 2; gCmd[pos++] = 0; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + pos, 0); pos += 2; /* qualifyingData */ + PutU16BE(gCmd + pos, TPM_ALG_MLDSA); pos += 2; /* ML-DSA selector */ + PutU32BE(gCmd + pos, 1); pos += 4; + PutU16BE(gCmd + pos, TPM_ALG_SHA256); pos += 2; + gCmd[pos++] = 1; gCmd[pos++] = 0; gCmd[pos++] = 0; gCmd[pos++] = 0; + PutU32BE(gCmd + 2, (UINT32)pos); + AssertIntEQ(FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SCHEME); + + FlushHandle(&ctx, keyH); + FWTPM_Cleanup(&ctx); + fwtpm_pass("Quote MLDSA scheme on ECC rejected:", 1); +} +#endif /* HAVE_ECC */ + +/* TPM2_Certify signed by a Pure ML-DSA key (self-certify), verified with the + * wolfCrypt ML-DSA verifier. Exercises the same FwSignAttest path as Quote. */ +static void test_fwtpm_mldsa_certify_sign_and_verify(void) +{ + FWTPM_CTX ctx; + int rspSize = 0, pos; + UINT32 akHandle; + FWTPM_Object* ak = NULL; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + + akHandle = CreatePrimaryMldsaAkHelper(&ctx, TPM_ALG_MLDSA, &ak); + + /* Certify(objectHandle=AK, signHandle=AK): two PW auth sessions. */ + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_Certify); pos += 4; + PutU32BE(gCmd + pos, akHandle); pos += 4; /* objectHandle */ + PutU32BE(gCmd + pos, akHandle); pos += 4; /* signHandle */ + pos = AppendTwoPwAuth(gCmd, pos); + PutU16BE(gCmd + pos, 0); pos += 2; /* qualifyingData */ + PutU16BE(gCmd + pos, TPM_ALG_NULL); pos += 2; /* inScheme */ + PutU32BE(gCmd + 2, (UINT32)pos); + AssertIntEQ(FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + VerifyMldsaAttestResponse(rspSize, ak); + + FlushHandle(&ctx, akHandle); + FWTPM_Cleanup(&ctx); + fwtpm_pass("MLDSA Certify sign and verify:", 1); +} + +/* TPM2_GetTime signed by a Pure ML-DSA key, verified with the wolfCrypt + * ML-DSA verifier. Exercises the same FwSignAttest path as Quote. */ +static void test_fwtpm_mldsa_gettime_sign_and_verify(void) +{ + FWTPM_CTX ctx; + int rspSize = 0, pos; + UINT32 akHandle; + FWTPM_Object* ak = NULL; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + + akHandle = CreatePrimaryMldsaAkHelper(&ctx, TPM_ALG_MLDSA, &ak); + + /* GetTime(privacyAdmin=ENDORSEMENT, signHandle=AK): two PW auth. */ + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_GetTime); pos += 4; + PutU32BE(gCmd + pos, TPM_RH_ENDORSEMENT); pos += 4; /* privacyAdmin */ + PutU32BE(gCmd + pos, akHandle); pos += 4; /* signHandle */ + pos = AppendTwoPwAuth(gCmd, pos); + PutU16BE(gCmd + pos, 0); pos += 2; /* qualifyingData */ + PutU16BE(gCmd + pos, TPM_ALG_NULL); pos += 2; /* inScheme */ PutU32BE(gCmd + 2, (UINT32)pos); - rspSize = 0; - rc = FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); - AssertIntEQ(rc, TPM_RC_SUCCESS); + AssertIntEQ(FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); - pos = TPM2_HEADER_SIZE + 4; - ssSz2 = GetU16BE(gRsp + pos); pos += 2; - AssertIntEQ((int)ssSz1, (int)ssSz2); - memcpy(ss2, gRsp + pos, ssSz2); - - AssertIntEQ(XMEMCMP(ss1, ss2, ssSz1), 0); + VerifyMldsaAttestResponse(rspSize, ak); + FlushHandle(&ctx, akHandle); FWTPM_Cleanup(&ctx); - fwtpm_pass(label, 1); + fwtpm_pass("MLDSA GetTime sign and verify:", 1); } -static void test_fwtpm_ecc_dhkem_roundtrip(void) +#ifdef WOLFTPM_HASH_MLDSA +/* TPM2_Quote with a restricted Hash-ML-DSA signing key. The signer pre-hashes + * the TPMS_ATTEST under the key's hashAlg and emits the 4-field Hash-ML-DSA + * signature (sigAlg | hashAlg | size | bytes); verify it by pre-hashing the + * same bytes and calling the wolfCrypt Hash-ML-DSA verifier. */ +static void test_fwtpm_hash_mldsa_quote_sign_and_verify(void) { - RunEccDhkemRoundtrip(TPM_ECC_NIST_P256, TPM_ALG_SHA256, - "Encap/Decap ECC DHKEM (P-256/HKDF-SHA256) Roundtrip:"); -} + FWTPM_CTX ctx; + int rspSize = 0, pos, digestSz; + UINT32 akHandle; + FWTPM_Object* ak = NULL; + UINT16 attestSz, sigAlg, sigHashAlg, sigSz; + TPMI_ALG_HASH phAlg; + const byte* attestBuf; + const byte* sig; + byte digest[TPM_MAX_DIGEST_SIZE]; -static void test_fwtpm_ecc_dhkem_p384_roundtrip(void) -{ - RunEccDhkemRoundtrip(TPM_ECC_NIST_P384, TPM_ALG_SHA384, - "Encap/Decap ECC DHKEM (P-384/HKDF-SHA384) Roundtrip:"); -} + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); -#ifdef FWTPM_HAVE_ECC521 -static void test_fwtpm_ecc_dhkem_p521_roundtrip(void) -{ - RunEccDhkemRoundtrip(TPM_ECC_NIST_P521, TPM_ALG_SHA512, - "Encap/Decap ECC DHKEM (P-521/HKDF-SHA512) Roundtrip:"); + akHandle = CreatePrimaryMldsaAkHelper(&ctx, TPM_ALG_HASH_MLDSA, &ak); + phAlg = ak->pub.parameters.hash_mldsaDetail.hashAlg; + + /* Quote over PCR0/1 (SHA-256), key's own scheme (inScheme NULL). */ + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_Quote); pos += 4; + PutU32BE(gCmd + pos, akHandle); pos += 4; + PutU32BE(gCmd + pos, 9); pos += 4; + PutU32BE(gCmd + pos, TPM_RS_PW); pos += 4; + PutU16BE(gCmd + pos, 0); pos += 2; gCmd[pos++] = 0; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + pos, 0); pos += 2; /* qualifyingData */ + PutU16BE(gCmd + pos, TPM_ALG_NULL); pos += 2; /* inScheme */ + PutU32BE(gCmd + pos, 1); pos += 4; /* PCRselect count */ + PutU16BE(gCmd + pos, TPM_ALG_SHA256); pos += 2; + gCmd[pos++] = 3; gCmd[pos++] = 0; gCmd[pos++] = 0; gCmd[pos++] = 0; + PutU32BE(gCmd + 2, (UINT32)pos); + AssertIntEQ(FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + + /* Response: paramSize(4) | TPM2B_ATTEST | sigAlg(2) | hashAlg(2) | + * sig TPM2B. */ + pos = TPM2_HEADER_SIZE + 4; + AssertIntEQ(pos + 2 <= rspSize, 1); + attestSz = GetU16BE(gRsp + pos); pos += 2; + AssertIntGT(attestSz, 0); + AssertIntEQ(pos + attestSz + 6 <= rspSize, 1); + attestBuf = gRsp + pos; pos += attestSz; + sigAlg = GetU16BE(gRsp + pos); pos += 2; + AssertIntEQ(sigAlg, TPM_ALG_HASH_MLDSA); + sigHashAlg = GetU16BE(gRsp + pos); pos += 2; + AssertIntEQ(sigHashAlg, phAlg); + sigSz = GetU16BE(gRsp + pos); pos += 2; + AssertIntGT(sigSz, 0); + AssertIntEQ(pos + (int)sigSz <= rspSize, 1); + sig = gRsp + pos; + + /* Pre-hash the attest bytes under phAlg, then verify the digest. */ + digestSz = TPM2_GetHashDigestSize(phAlg); + AssertIntEQ(wc_Hash(FwGetWcHashType(phAlg), attestBuf, (word32)attestSz, + digest, (word32)digestSz), 0); + AssertIntEQ(FwVerifyMldsaHash( + ak->pub.parameters.hash_mldsaDetail.parameterSet, + &ak->pub.unique.mldsa, NULL, 0, phAlg, + digest, digestSz, sig, (int)sigSz), TPM_RC_SUCCESS); + + FlushHandle(&ctx, akHandle); + FWTPM_Cleanup(&ctx); + fwtpm_pass("HashMLDSA Quote sign and verify:", 1); +} + +#ifdef WOLFSSL_SHA384 +/* Walk a TPMS_ATTEST (QUOTE) and return the pcrDigest TPM2B size. Returns 0 on + * a truncated or malformed buffer. */ +static UINT16 QuoteAttestPcrDigestSize(const byte* a, UINT16 aSz) +{ + int p = 6; /* magic(4) + type(2) */ + UINT16 sz; + UINT32 cnt, i; + + if (p + 2 > aSz) return 0; + sz = GetU16BE(a + p); p += 2 + sz; /* qualifiedSigner */ + if (p + 2 > aSz) return 0; + sz = GetU16BE(a + p); p += 2 + sz; /* extraData */ + p += 17 + 8; /* clockInfo + firmwareVersion */ + if (p + 4 > aSz) return 0; + cnt = GetU32BE(a + p); p += 4; /* pcrSelect count */ + for (i = 0; i < cnt; i++) { + UINT8 selSz; + if (p + 3 > aSz) return 0; + p += 2; /* hashAlg */ + selSz = a[p]; p += 1 + selSz; /* sizeOfSelect + select */ + } + if (p + 2 > aSz) return 0; + return GetU16BE(a + p); /* pcrDigest size */ } -#endif -/* ML-KEM Labeled KEM seed roundtrip per Part 1 Sec.47.4 Eq.66: - * seed = KDFa(nameAlg, K, label, ciphertext, publicKey, bits). - * FwEncryptSeed(MLKEM) outputs (seed1, ciphertext); FwDecryptSeed(MLKEM) - * reconstructs seed2 from ciphertext and the same key. seed1 must equal seed2. */ -static void test_fwtpm_encseed_decseed_mlkem_roundtrip(void) +/* A Hash-ML-DSA Quote must hash the PCR measurements under the key's nameAlg + * (TCG v185 errata), independent of the ML-DSA message pre-hash. With + * nameAlg=SHA-384 and pre-hash=SHA-256 the inner pcrDigest is 48 bytes while + * the signature still declares SHA-256. */ +static void test_fwtpm_hash_mldsa_quote_namealg_digest(void) { FWTPM_CTX ctx; - int rc, rspSize, cmdSz; - UINT32 keyHandle; - FWTPM_Object* keyObj = NULL; - int oi; - byte seed1[64], seed2[64]; - int seed1Sz = 0, seed2Sz = 0; - FWTPM_DECLARE_BUF(encSeed, 2048); - int encSeedSz = 0; + int rspSize = 0, pos, pubStart, sensStart; + UINT32 akHandle; + UINT16 attestSz, sigAlg, sigHash, pcrDigestSz; + const byte* attestBuf; - FWTPM_ALLOC_BUF(encSeed, 2048); memset(&ctx, 0, sizeof(ctx)); AssertIntEQ(fwtpm_test_startup(&ctx), 0); - cmdSz = BuildCreatePrimaryCmd(gCmd, TPM_ALG_MLKEM); - rspSize = 0; - rc = FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0); - AssertIntEQ(rc, TPM_RC_SUCCESS); + /* CreatePrimary Hash-ML-DSA-65: nameAlg SHA-384, pre-hash SHA-256, + * restricted sign. */ + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_CreatePrimary); pos += 4; + PutU32BE(gCmd + pos, TPM_RH_OWNER); pos += 4; + PutU32BE(gCmd + pos, 9); pos += 4; + PutU32BE(gCmd + pos, TPM_RS_PW); pos += 4; + PutU16BE(gCmd + pos, 0); pos += 2; gCmd[pos++] = 0; + PutU16BE(gCmd + pos, 0); pos += 2; + sensStart = pos; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + sensStart, (UINT16)(pos - sensStart - 2)); + pubStart = pos; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + pos, TPM_ALG_HASH_MLDSA); pos += 2; + PutU16BE(gCmd + pos, TPM_ALG_SHA384); pos += 2; /* nameAlg */ + PutU32BE(gCmd + pos, 0x00050072); pos += 4; /* restricted|sign|... */ + PutU16BE(gCmd + pos, 0); pos += 2; /* authPolicy */ + PutU16BE(gCmd + pos, TPM_MLDSA_65); pos += 2; /* parameterSet */ + PutU16BE(gCmd + pos, TPM_ALG_SHA256); pos += 2; /* pre-hash */ + PutU16BE(gCmd + pos, 0); pos += 2; /* unique */ + PutU16BE(gCmd + pubStart, (UINT16)(pos - pubStart - 2)); + PutU16BE(gCmd + pos, 0); pos += 2; /* outsideInfo */ + PutU32BE(gCmd + pos, 0); pos += 4; /* creationPCR */ + PutU32BE(gCmd + 2, (UINT32)pos); + AssertIntEQ(FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); - keyHandle = GetU32BE(gRsp + TPM2_HEADER_SIZE); - AssertIntNE(keyHandle, 0); + akHandle = GetU32BE(gRsp + TPM2_HEADER_SIZE); + AssertIntNE(akHandle, 0); - for (oi = 0; oi < FWTPM_MAX_OBJECTS; oi++) { - if (ctx.objects[oi].handle == keyHandle) { - keyObj = &ctx.objects[oi]; - break; - } - } - AssertNotNull(keyObj); + /* Quote over PCR0 (SHA-256 bank), NULL scheme. */ + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_Quote); pos += 4; + PutU32BE(gCmd + pos, akHandle); pos += 4; + PutU32BE(gCmd + pos, 9); pos += 4; + PutU32BE(gCmd + pos, TPM_RS_PW); pos += 4; + PutU16BE(gCmd + pos, 0); pos += 2; gCmd[pos++] = 0; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + pos, 0); pos += 2; /* qualifyingData */ + PutU16BE(gCmd + pos, TPM_ALG_NULL); pos += 2; /* inScheme */ + PutU32BE(gCmd + pos, 1); pos += 4; + PutU16BE(gCmd + pos, TPM_ALG_SHA256); pos += 2; + gCmd[pos++] = 1; gCmd[pos++] = 0; gCmd[pos++] = 0; gCmd[pos++] = 0; + PutU32BE(gCmd + 2, (UINT32)pos); + AssertIntEQ(FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); - rc = FwEncryptSeed(&ctx, keyObj, - NULL, 0, "SECRET", - seed1, (int)sizeof(seed1), &seed1Sz, - encSeed, 2048, &encSeedSz); - AssertIntEQ(rc, TPM_RC_SUCCESS); - AssertIntGT(seed1Sz, 0); - AssertIntGT(encSeedSz, 0); + /* paramSize(4) | TPM2B_ATTEST | sigAlg(2) | hashAlg(2) | sig TPM2B */ + pos = TPM2_HEADER_SIZE + 4; + attestSz = GetU16BE(gRsp + pos); pos += 2; + attestBuf = gRsp + pos; pos += attestSz; + sigAlg = GetU16BE(gRsp + pos); pos += 2; + sigHash = GetU16BE(gRsp + pos); pos += 2; + AssertIntEQ(sigAlg, TPM_ALG_HASH_MLDSA); + AssertIntEQ(sigHash, TPM_ALG_SHA256); /* signature pre-hash */ - rc = FwDecryptSeed(&ctx, keyObj, - encSeed, (UINT16)encSeedSz, - NULL, 0, "SECRET", - seed2, (int)sizeof(seed2), &seed2Sz); - AssertIntEQ(rc, TPM_RC_SUCCESS); - AssertIntEQ(seed1Sz, seed2Sz); - AssertIntEQ(XMEMCMP(seed1, seed2, seed1Sz), 0); + /* Inner pcrDigest uses the key's nameAlg (SHA-384 => 48 bytes), not the + * SHA-256 pre-hash. */ + pcrDigestSz = QuoteAttestPcrDigestSize(attestBuf, attestSz); + AssertIntEQ((int)pcrDigestSz, + TPM2_GetHashDigestSize(TPM_ALG_SHA384)); + FlushHandle(&ctx, akHandle); FWTPM_Cleanup(&ctx); - FWTPM_FREE_BUF(encSeed); - fwtpm_pass("FwEncryptSeed/FwDecryptSeed MLKEM Roundtrip:", 1); + fwtpm_pass("HashMLDSA Quote nameAlg digest:", 1); } +#endif /* WOLFSSL_SHA384 */ +#endif /* WOLFTPM_HASH_MLDSA */ +#endif /* WOLFTPM_MLDSA_SIGN && WOLFTPM_MLDSA_VERIFY && !FWTPM_NO_ATTESTATION */ /* TPM2_SignDigest + TPM2_VerifyDigestSignature roundtrip with ECC P-256 * (ECDSA + SHA-256) per Part 3 Sec.20.7 / Sec.20.4. Confirms the v1.85 @@ -4566,7 +5428,75 @@ static void test_fwtpm_mldsa_loadexternal_verify(void) FWTPM_Cleanup(&ctx); fwtpm_pass("MLDSA LoadExternal (NIST pub):", 1); } -static void FlushHandle(FWTPM_CTX* ctx, UINT32 handle); + +#if defined(WOLFTPM_MLDSA_SIGN) && defined(WOLFTPM_MLDSA_VERIFY) && \ + !defined(FWTPM_NO_ATTESTATION) +/* A public-only ML-DSA key (loaded via TPM2_LoadExternal, no private seed) + * must not be usable for attestation: signing from its all-zero seed would + * derive a universally reproducible key unrelated to the public key. Quote + * must return TPM_RC_KEY. */ +static void test_fwtpm_mldsa_quote_public_only_returns_key(void) +{ + FWTPM_CTX ctx; + int rspSize = 0, pos, pubStart; + UINT32 handle; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + + /* LoadExternal a public-only ML-DSA-44 key with sign|restricted so it + * clears the Quote attribute gates and reaches the ML-DSA signer. */ + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_NO_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_LoadExternal); pos += 4; + PutU16BE(gCmd + pos, 0); pos += 2; /* inPrivate empty */ + pubStart = pos; + PutU16BE(gCmd + pos, 0); pos += 2; /* size placeholder */ + PutU16BE(gCmd + pos, TPM_ALG_MLDSA); pos += 2; /* type */ + PutU16BE(gCmd + pos, TPM_ALG_SHA256); pos += 2; /* nameAlg */ + PutU32BE(gCmd + pos, 0x00050040); pos += 4; /* sign|restricted|userWithAuth */ + PutU16BE(gCmd + pos, 0); pos += 2; /* authPolicy */ + PutU16BE(gCmd + pos, TPM_MLDSA_44); pos += 2; /* parameterSet */ + gCmd[pos++] = NO; /* allowExternalMu */ + PutU16BE(gCmd + pos, sizeof(gNistMldsa44Pk)); pos += 2; + memcpy(gCmd + pos, gNistMldsa44Pk, sizeof(gNistMldsa44Pk)); + pos += sizeof(gNistMldsa44Pk); + PutU16BE(gCmd + pubStart, (UINT16)(pos - pubStart - 2)); + PutU32BE(gCmd + pos, TPM_RH_NULL); pos += 4; /* hierarchy */ + PutU32BE(gCmd + 2, (UINT32)pos); + AssertIntEQ(FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + handle = GetU32BE(gRsp + TPM2_HEADER_SIZE); + AssertIntNE(handle, 0); + + /* Quote must refuse: the TPM holds no private seed for this key. */ + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_Quote); pos += 4; + PutU32BE(gCmd + pos, handle); pos += 4; + PutU32BE(gCmd + pos, 9); pos += 4; + PutU32BE(gCmd + pos, TPM_RS_PW); pos += 4; + PutU16BE(gCmd + pos, 0); pos += 2; gCmd[pos++] = 0; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + pos, 0); pos += 2; /* qualifyingData */ + PutU16BE(gCmd + pos, TPM_ALG_NULL); pos += 2; /* inScheme */ + PutU32BE(gCmd + pos, 1); pos += 4; /* PCRselect count */ + PutU16BE(gCmd + pos, TPM_ALG_SHA256); pos += 2; + gCmd[pos++] = 3; gCmd[pos++] = 0; gCmd[pos++] = 0; gCmd[pos++] = 0; + PutU32BE(gCmd + 2, (UINT32)pos); + rspSize = 0; + AssertIntEQ(FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_KEY); + + FlushHandle(&ctx, handle); + FWTPM_Cleanup(&ctx); + fwtpm_pass("MLDSA Quote public-only key rejected:", 1); +} +#endif /* WOLFTPM_MLDSA_SIGN && WOLFTPM_MLDSA_VERIFY && !FWTPM_NO_ATTESTATION */ /* TPM2_LoadExternal must reject ML public areas with an unsupported parameter * set, an out-of-range allowExternalMu, an invalid Hash-ML-DSA hash, invalid @@ -10682,6 +11612,84 @@ static void test_fwtpm_nv_certify_digest_mode(void) FWTPM_Cleanup(&ctx); printf("Test fwTPM:\tNV_Certify (digest mode):\tPassed\n"); } + +#if defined(WOLFTPM_MLDSA_SIGN) && defined(WOLFTPM_MLDSA_VERIFY) +/* TPM2_NV_Certify signed by a Pure ML-DSA key, verified with the wolfCrypt + * ML-DSA verifier. Exercises the same FwSignAttest path as Quote/Certify. */ +static void test_fwtpm_mldsa_nv_certify_sign_and_verify(void) +{ + FWTPM_CTX ctx; + int rspSize = 0, cmdSz, pos; + UINT32 akHandle; + UINT32 nvIdx = 0x01500009; + UINT32 attrs = TPMA_NV_OWNERWRITE | TPMA_NV_OWNERREAD | TPMA_NV_NO_DA; + byte nvData[8]; + FWTPM_Object* ak = NULL; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + + akHandle = CreatePrimaryMldsaAkHelper(&ctx, TPM_ALG_MLDSA, &ak); + + /* Define + write an NV index (must be written before it is certified). */ + cmdSz = BuildNvDefineCmd(gCmd, nvIdx, (UINT16)sizeof(nvData), attrs); + AssertIntGT(cmdSz, 0); + AssertIntEQ(FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + + memset(nvData, 0xA5, sizeof(nvData)); + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_NV_Write); pos += 4; + PutU32BE(gCmd + pos, TPM_RH_OWNER); pos += 4; + PutU32BE(gCmd + pos, nvIdx); pos += 4; + pos = AppendPwAuth(gCmd, pos, NULL, 0); + PutU16BE(gCmd + pos, (UINT16)sizeof(nvData)); pos += 2; + memcpy(gCmd + pos, nvData, sizeof(nvData)); pos += sizeof(nvData); + PutU16BE(gCmd + pos, 0); pos += 2; + PutU32BE(gCmd + 2, (UINT32)pos); + AssertIntEQ(FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + + /* NV_Certify(signHandle=AK, authHandle=OWNER, nvIndex), full-read mode. */ + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_NV_Certify); pos += 4; + PutU32BE(gCmd + pos, akHandle); pos += 4; /* signHandle */ + PutU32BE(gCmd + pos, TPM_RH_OWNER); pos += 4; /* authHandle */ + PutU32BE(gCmd + pos, nvIdx); pos += 4; /* nvIndex */ + pos = AppendTwoPwAuth(gCmd, pos); + PutU16BE(gCmd + pos, 0); pos += 2; /* qualifyingData */ + PutU16BE(gCmd + pos, TPM_ALG_NULL); pos += 2; /* inScheme */ + PutU16BE(gCmd + pos, (UINT16)sizeof(nvData)); pos += 2; /* size */ + PutU16BE(gCmd + pos, 0); pos += 2; /* offset */ + PutU32BE(gCmd + 2, (UINT32)pos); + AssertIntEQ(FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + VerifyMldsaAttestResponse(rspSize, ak); + + FlushHandle(&ctx, akHandle); + /* Undefine NV. */ + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_NV_UndefineSpace); pos += 4; + PutU32BE(gCmd + pos, TPM_RH_OWNER); pos += 4; + PutU32BE(gCmd + pos, nvIdx); pos += 4; + pos = AppendPwAuth(gCmd, pos, NULL, 0); + PutU32BE(gCmd + 2, (UINT32)pos); + rspSize = 0; + FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); + + FWTPM_Cleanup(&ctx); + fwtpm_pass("MLDSA NV_Certify sign and verify:", 1); +} +#endif /* WOLFTPM_MLDSA_SIGN && WOLFTPM_MLDSA_VERIFY */ #endif /* !FWTPM_NO_ATTESTATION */ #endif /* !FWTPM_NO_NV */ @@ -13820,6 +14828,36 @@ int fwtpm_unit_tests(int argc, char *argv[]) test_fwtpm_mlkem_roundtrip(); test_fwtpm_ecc_dhkem_roundtrip(); test_fwtpm_encseed_decseed_mlkem_roundtrip(); +#if defined(WOLFTPM_MLKEM_ENCAP) && defined(WOLFTPM_MLKEM_DECAP) && \ + !defined(FWTPM_NO_CREDENTIAL) + test_fwtpm_mlkem_credential_roundtrip(); + test_fwtpm_mlkem_activatecredential_tampered_rejected(); + test_fwtpm_mlkem_activatecredential_unrestricted_rejected(); +#ifdef WOLFSSL_SHA384 + test_fwtpm_mlkem_credential_sha384_aes256_roundtrip(); +#endif +#endif +#if defined(WOLFTPM_MLDSA_SIGN) && defined(WOLFTPM_MLDSA_VERIFY) && \ + !defined(FWTPM_NO_ATTESTATION) + test_fwtpm_mldsa_quote_sign_and_verify(); + test_fwtpm_mldsa_quote_scheme_mismatch_rejected(); + test_fwtpm_mldsa_quote_explicit_empty_scheme(); +#ifdef HAVE_ECC + test_fwtpm_quote_mldsa_scheme_on_ecc_rejected(); +#endif + test_fwtpm_mldsa_quote_public_only_returns_key(); + test_fwtpm_mldsa_certify_sign_and_verify(); + test_fwtpm_mldsa_gettime_sign_and_verify(); +#ifdef WOLFTPM_HASH_MLDSA + test_fwtpm_hash_mldsa_quote_sign_and_verify(); +#ifdef WOLFSSL_SHA384 + test_fwtpm_hash_mldsa_quote_namealg_digest(); +#endif +#endif +#ifndef FWTPM_NO_NV + test_fwtpm_mldsa_nv_certify_sign_and_verify(); +#endif +#endif test_fwtpm_signdigest_classical_ecdsa_roundtrip(); test_fwtpm_signsequence_classical_ecdsa_roundtrip(); test_fwtpm_signdigest_null_scheme_rejected(); diff --git a/wolftpm/fwtpm/fwtpm_crypto.h b/wolftpm/fwtpm/fwtpm_crypto.h index c6b2a8a70..1596cbc08 100644 --- a/wolftpm/fwtpm/fwtpm_crypto.h +++ b/wolftpm/fwtpm/fwtpm_crypto.h @@ -378,6 +378,7 @@ TPM_RC FwCredentialDeriveKeys( TPM_RC FwCredentialWrap( const byte* symKey, int symKeySz, const byte* hmacKey, int hmacKeySz, + TPMI_ALG_HASH nameAlg, const byte* credential, UINT16 credSz, const byte* name, int nameSz, byte* encCred, word32* encCredSz, @@ -386,6 +387,7 @@ TPM_RC FwCredentialWrap( TPM_RC FwCredentialUnwrap( const byte* symKey, int symKeySz, const byte* hmacKey, int hmacKeySz, + TPMI_ALG_HASH nameAlg, const byte* blobBuf, UINT16 blobSz, const byte* name, int nameSz, byte* credOut, int credBufSz, UINT16* credSzOut);