diff --git a/src/credentials/CHIPCert.cpp b/src/credentials/CHIPCert.cpp index d0c5d56d5a48b1..46451b45b34647 100644 --- a/src/credentials/CHIPCert.cpp +++ b/src/credentials/CHIPCert.cpp @@ -1433,6 +1433,13 @@ CHIP_ERROR CertificateValidityPolicy::ApplyDefaultPolicy(const ChipCertificateDa } } +void InitNetworkIdentitySubject(ChipDN & name) +{ + name.Clear(); + ChipError err = name.AddAttribute_CommonName(kNetworkIdentityCN, /* not printable */ false); + VerifyOrDie(err == CHIP_NO_ERROR); // AddAttribute can't fail in this case +} + static void CalculateKeyIdentifierSha256(const P256PublicKeySpan & publicKey, MutableCertificateKeyId keyId) { uint8_t hash[kSHA256_Hash_Length]; @@ -1445,7 +1452,7 @@ static CHIP_ERROR ValidateChipNetworkIdentity(const ChipCertificateData & certDa { ReturnErrorOnFailure(ValidateCertificateType(certData, CertType::kNetworkIdentity)); - VerifyOrReturnError(certData.mSerialNumber.data_equal(kNetworkIdentitySerialNumber), CHIP_ERROR_WRONG_CERT_TYPE); + VerifyOrReturnError(certData.mSerialNumber.data_equal(kNetworkIdentitySerialNumberBytes), CHIP_ERROR_WRONG_CERT_TYPE); VerifyOrReturnError(certData.mNotBeforeTime == kNetworkIdentityNotBeforeTime, CHIP_ERROR_WRONG_CERT_TYPE); VerifyOrReturnError(certData.mNotAfterTime == kNetworkIdentityNotAfterTime, CHIP_ERROR_WRONG_CERT_TYPE); VerifyOrReturnError(certData.mIssuerDN.IsEqual(certData.mSubjectDN), CHIP_ERROR_WRONG_CERT_TYPE); @@ -1490,5 +1497,52 @@ CHIP_ERROR ExtractIdentifierFromChipNetworkIdentity(const ByteSpan & cert, Mutab return CHIP_NO_ERROR; } +static CHIP_ERROR GenerateNetworkIdentitySignature(const P256Keypair & keypair, P256ECDSASignature & signature) +{ + // Create a buffer and writer to capture the TBS (to-be-signed) portion of the certificate. + chip::Platform::ScopedMemoryBuffer asn1TBSBuf; + VerifyOrReturnError(asn1TBSBuf.Alloc(kNetworkIdenitityTBSLength), CHIP_ERROR_NO_MEMORY); + + ASN1Writer writer; + writer.Init(asn1TBSBuf.Get(), kNetworkIdenitityTBSLength); + + // Generate the TBSCertificate and sign it + ReturnErrorOnFailure(EncodeNetworkIdentityTBSCert(keypair.Pubkey(), writer)); + ReturnErrorOnFailure(keypair.ECDSA_sign_msg(asn1TBSBuf.Get(), writer.GetLengthWritten(), signature)); + + return CHIP_NO_ERROR; +} + +static CHIP_ERROR EncodeCompactIdentityCert(TLVWriter & writer, Tag tag, P256PublicKeySpan const & publicKey, + P256ECDSASignatureSpan const & signature) +{ + TLVType containerType; + ReturnErrorOnFailure(writer.StartContainer(tag, kTLVType_Structure, containerType)); + ReturnErrorOnFailure(writer.Put(ContextTag(kTag_EllipticCurvePublicKey), publicKey)); + ReturnErrorOnFailure(writer.Put(ContextTag(kTag_ECDSASignature), signature)); + ReturnErrorOnFailure(writer.EndContainer(containerType)); + return CHIP_NO_ERROR; +} + +CHIP_ERROR NewChipNetworkIdentity(const Crypto::P256Keypair & keypair, MutableByteSpan & compactIdentityCert) +{ + VerifyOrReturnError(!compactIdentityCert.empty(), CHIP_ERROR_INVALID_ARGUMENT); + VerifyOrReturnError(CanCastTo(compactIdentityCert.size()), CHIP_ERROR_INVALID_ARGUMENT); + + Crypto::P256ECDSASignature signature; + ReturnErrorOnFailure(GenerateNetworkIdentitySignature(keypair, signature)); + + TLVWriter writer; + writer.Init(compactIdentityCert); + + P256PublicKeySpan publicKeySpan(keypair.Pubkey().ConstBytes()); + P256ECDSASignatureSpan signatureSpan(signature.ConstBytes()); + VerifyOrDie(signature.Length() == signatureSpan.size()); + ReturnErrorOnFailure(EncodeCompactIdentityCert(writer, AnonymousTag(), publicKeySpan, signatureSpan)); + + compactIdentityCert.reduce_size(writer.GetLengthWritten()); + return CHIP_NO_ERROR; +} + } // namespace Credentials } // namespace chip diff --git a/src/credentials/CHIPCert.h b/src/credentials/CHIPCert.h index ecf5f9f041d984..03f7fe70199601 100644 --- a/src/credentials/CHIPCert.h +++ b/src/credentials/CHIPCert.h @@ -53,8 +53,8 @@ static constexpr uint16_t kX509NoWellDefinedExpirationDateYear = 9999; static constexpr uint32_t kMaxCHIPCertLength = 400; static constexpr uint32_t kMaxDERCertLength = 600; -// The decode buffer is used to reconstruct TBS section of X.509 certificate, which doesn't include signature. -static constexpr uint32_t kMaxCHIPCertDecodeBufLength = kMaxDERCertLength - Crypto::kMax_ECDSA_Signature_Length_Der; +// As per spec section 11.24 (Wi-Fi Authentication with Per-Device Credentials) +inline constexpr uint32_t kMaxCHIPCompactNetworkIdentityLength = 140; /** Data Element Tags for the CHIP Certificate */ @@ -542,6 +542,7 @@ CHIP_ERROR ValidateChipRCAC(const ByteSpan & rcac); /** * Validates a Network (Client) Identity in TLV-encoded form. + * Accepts either a full certificate or the compact-pdc-identity format. * * This function parses the certificate, ensures the rigid fields have the values mandated by the * specification, and validates the certificate signature. @@ -618,6 +619,17 @@ CHIP_ERROR NewICAX509Cert(const X509CertRequestParams & requestParams, const Cry CHIP_ERROR NewNodeOperationalX509Cert(const X509CertRequestParams & requestParams, const Crypto::P256PublicKey & subjectPubkey, const Crypto::P256Keypair & issuerKeypair, MutableByteSpan & x509Cert); +/** + * @brief Generates a Network (Client) Identity certificate in TLV-encoded form. + * + * @param keypair The key pair underlying the identity. + * @param cert Buffer to store the signed certificate in compact-pdc-identity TLV format. + * Must be at least `kMaxCHIPCompactNetworkIdentityLength` bytes long. + * + * @return Returns a CHIP_ERROR on error, CHIP_NO_ERROR otherwise + **/ +CHIP_ERROR NewChipNetworkIdentity(const Crypto::P256Keypair & keypair, MutableByteSpan & cert); + /** * @brief * Convert a certificate date/time (in the form of an ASN.1 universal time structure) into a CHIP Epoch time. diff --git a/src/credentials/CHIPCertToX509.cpp b/src/credentials/CHIPCertToX509.cpp index 6341e2239e914c..ab520f2b5f4681 100644 --- a/src/credentials/CHIPCertToX509.cpp +++ b/src/credentials/CHIPCertToX509.cpp @@ -31,7 +31,7 @@ #include #include -#include +#include #include #include #include @@ -493,6 +493,8 @@ static CHIP_ERROR DecodeConvertECDSASignature(TLVReader & reader, ASN1Writer & w * @param certData Structure containing data extracted from the TBS portion of the * CHIP certificate. * + * Note: The reader must be positioned on the SerialNumber element. + * * @return Returns a CHIP_ERROR on error, CHIP_NO_ERROR otherwise **/ static CHIP_ERROR DecodeConvertTBSCert(TLVReader & reader, ASN1Writer & writer, ChipCertificateData & certData) @@ -513,7 +515,7 @@ static CHIP_ERROR DecodeConvertTBSCert(TLVReader & reader, ASN1Writer & writer, // serialNumber CertificateSerialNumber // CertificateSerialNumber ::= INTEGER - ReturnErrorOnFailure(reader.Next(kTLVType_ByteString, ContextTag(kTag_SerialNumber))); + ReturnErrorOnFailure(reader.Expect(kTLVType_ByteString, ContextTag(kTag_SerialNumber))); ReturnErrorOnFailure(reader.Get(certData.mSerialNumber)); ReturnErrorOnFailure(writer.PutValue(kASN1TagClass_Universal, kASN1UniversalTag_Integer, false, certData.mSerialNumber.data(), static_cast(certData.mSerialNumber.size()))); @@ -554,6 +556,41 @@ static CHIP_ERROR DecodeConvertTBSCert(TLVReader & reader, ASN1Writer & writer, return err; } +/** + * Variant of DecodeConvertTBSCert that handles reading a compact-pdc-identity + * where only the subject public key is actually encoded. All other values are + * populated / written as the well-known values mandated by the specification. + * + * Note: The reader must be positioned on the EllipticCurvePublicKey element. + */ +static CHIP_ERROR DecodeConvertTBSCertCompactIdentity(TLVReader & reader, ASN1Writer & writer, ChipCertificateData & certData) +{ + // Decode the public key, everything else is rigid + ReturnErrorOnFailure(reader.Expect(kTLVType_ByteString, ContextTag(kTag_EllipticCurvePublicKey))); + ReturnErrorOnFailure(reader.Get(certData.mPublicKey)); + + // Populate rigid ChipCertificateData fields + certData.mSerialNumber = kNetworkIdentitySerialNumberBytes; + certData.mSigAlgoOID = kOID_SigAlgo_ECDSAWithSHA256; + InitNetworkIdentitySubject(certData.mIssuerDN); + certData.mNotBeforeTime = kNetworkIdentityNotBeforeTime; + certData.mNotAfterTime = kNetworkIdentityNotAfterTime; + InitNetworkIdentitySubject(certData.mSubjectDN); + certData.mPubKeyAlgoOID = kOID_PubKeyAlgo_ECPublicKey; + certData.mPubKeyCurveOID = kOID_EllipticCurve_prime256v1; + certData.mCertFlags.Set(CertFlags::kExtPresent_BasicConstraints); + certData.mCertFlags.Set(CertFlags::kExtPresent_KeyUsage); + certData.mKeyUsageFlags = kNetworkIdentityKeyUsage; + certData.mCertFlags.Set(CertFlags::kExtPresent_ExtendedKeyUsage); + certData.mKeyPurposeFlags = kNetworkIdentityKeyPurpose; + + if (!writer.IsNullWriter()) + { + ReturnErrorOnFailure(EncodeNetworkIdentityTBSCert(certData.mPublicKey, writer)); + } + return CHIP_NO_ERROR; +} + /** * Decode a CHIP TLV certificate and convert it to X.509 DER form. * @@ -581,7 +618,17 @@ static CHIP_ERROR DecodeConvertCert(TLVReader & reader, ASN1Writer & writer, ASN ASN1_START_SEQUENCE { // tbsCertificate TBSCertificate, - ReturnErrorOnFailure(DecodeConvertTBSCert(reader, tbsWriter, certData)); + reader.Next(); + if (reader.GetTag() == ContextTag(kTag_EllipticCurvePublicKey)) + { + // If the struct starts with the ec-pub-key we're dealing with a + // Network (Client) Identity in compact-pdc-identity format. + DecodeConvertTBSCertCompactIdentity(reader, tbsWriter, certData); + } + else + { + ReturnErrorOnFailure(DecodeConvertTBSCert(reader, tbsWriter, certData)); + } // signatureAlgorithm AlgorithmIdentifier // AlgorithmIdentifier ::= SEQUENCE diff --git a/src/credentials/CHIPCert_Internal.h b/src/credentials/CHIPCert_Internal.h index 9e61905b538c6f..11557305f97e55 100644 --- a/src/credentials/CHIPCert_Internal.h +++ b/src/credentials/CHIPCert_Internal.h @@ -23,10 +23,17 @@ namespace chip { namespace Credentials { +// The decode buffer is used to reconstruct TBS section of X.509 certificate, which doesn't include signature. +inline constexpr size_t kMaxCHIPCertDecodeBufLength = kMaxDERCertLength - Crypto::kMax_ECDSA_Signature_Length_Der; + +// The TBSCerticate of a Network (Client) Identity has a fixed (smaller) size. +inline constexpr size_t kNetworkIdenitityTBSLength = 244; + // Constants for Network (Client) Identities as per section 11.24 (Wi-Fi // Authentication with Per-Device Credentials) of the Matter spec. -inline constexpr CharSpan kNetworkIdentityCN = "*"_span; -inline constexpr ByteSpan kNetworkIdentitySerialNumber = ByteSpan((uint8_t[1]){ 1 }); +inline constexpr CharSpan kNetworkIdentityCN = "*"_span; +inline constexpr uint8_t kNetworkIdentitySerialNumber = 1; +inline constexpr ByteSpan kNetworkIdentitySerialNumberBytes = ByteSpan((uint8_t[1]){ kNetworkIdentitySerialNumber }); inline constexpr uint32_t kNetworkIdentityNotBeforeTime = 1; inline constexpr uint32_t kNetworkIdentityNotAfterTime = kNullCertTime; @@ -35,5 +42,11 @@ inline constexpr auto kNetworkIdentityKeyUsage = BitFlags(KeyUsag inline constexpr auto kNetworkIdentityKeyPurpose = BitFlags(KeyPurposeFlags::kClientAuth, KeyPurposeFlags::kServerAuth); +// Initializes a ChipDN as CN=kNetworkIdentityCN +void InitNetworkIdentitySubject(ChipDN & name); + +// Emits a X.509 TBSCertificate for a Network (Client) Identity based on the specified key. +CHIP_ERROR EncodeNetworkIdentityTBSCert(const Crypto::P256PublicKey & pubkey, ASN1::ASN1Writer & writer); + } // namespace Credentials } // namespace chip diff --git a/src/credentials/GenerateChipX509Cert.cpp b/src/credentials/GenerateChipX509Cert.cpp index 8297b85c4c5909..0dccaaa3a49fa6 100644 --- a/src/credentials/GenerateChipX509Cert.cpp +++ b/src/credentials/GenerateChipX509Cert.cpp @@ -31,7 +31,7 @@ #include #include -#include +#include #include #include #include @@ -313,8 +313,6 @@ CHIP_ERROR EncodeChipECDSASignature(Crypto::P256ECDSASignature & signature, ASN1 return err; } -} // namespace - CHIP_ERROR EncodeTBSCert(const X509CertRequestParams & requestParams, const Crypto::P256PublicKey & subjectPubkey, const Crypto::P256PublicKey & issuerPubkey, ASN1Writer & writer) { @@ -367,6 +365,61 @@ CHIP_ERROR EncodeTBSCert(const X509CertRequestParams & requestParams, const Cryp return err; } +} // namespace + +CHIP_ERROR EncodeNetworkIdentityTBSCert(const P256PublicKey & pubkey, ASN1Writer & writer) +{ + CHIP_ERROR err = CHIP_NO_ERROR; + ChipDN issuerAndSubject; + InitNetworkIdentitySubject(issuerAndSubject); + + ASN1_START_SEQUENCE + { + // version [0] EXPLICIT Version DEFAULT v1 + ASN1_START_CONSTRUCTED(kASN1TagClass_ContextSpecific, 0) + { + ASN1_ENCODE_INTEGER(2); // Version ::= INTEGER { v1(0), v2(1), v3(2) } + } + ASN1_END_CONSTRUCTED; + + ReturnErrorOnFailure(writer.PutInteger(kNetworkIdentitySerialNumber)); + + ASN1_START_SEQUENCE + { + ASN1_ENCODE_OBJECT_ID(kOID_SigAlgo_ECDSAWithSHA256); + } + ASN1_END_SEQUENCE; + + // issuer Name + ReturnErrorOnFailure(issuerAndSubject.EncodeToASN1(writer)); + + // validity Validity, + ReturnErrorOnFailure(EncodeValidity(kNetworkIdentityNotBeforeTime, kNetworkIdentityNotAfterTime, writer)); + + // subject Name + ReturnErrorOnFailure(issuerAndSubject.EncodeToASN1(writer)); + + ReturnErrorOnFailure(EncodeSubjectPublicKeyInfo(pubkey, writer)); + + // certificate extensions + ASN1_START_CONSTRUCTED(kASN1TagClass_ContextSpecific, 3) + { + ASN1_START_SEQUENCE + { + EncodeIsCAExtension(kNotCACert, writer); + EncodeKeyUsageExtension(KeyUsageFlags::kDigitalSignature, writer); + EncodeExtKeyUsageExtension({ kOID_KeyPurpose_ClientAuth, kOID_KeyPurpose_ServerAuth }, writer); + } + ASN1_END_SEQUENCE; + } + ASN1_END_CONSTRUCTED; + } + ASN1_END_SEQUENCE; + +exit: + return err; +} + CHIP_ERROR NewChipX509Cert(const X509CertRequestParams & requestParams, const Crypto::P256PublicKey & subjectPubkey, const Crypto::P256Keypair & issuerKeypair, MutableByteSpan & x509Cert) { diff --git a/src/credentials/tests/TestChipCert.cpp b/src/credentials/tests/TestChipCert.cpp index 3e23f022387d0b..2179561367b9cc 100644 --- a/src/credentials/tests/TestChipCert.cpp +++ b/src/credentials/tests/TestChipCert.cpp @@ -2149,24 +2149,46 @@ static void TestChipCert_ExtractPublicKeyAndSKID(nlTestSuite * inSuite, void * i static void TestChipCert_PDCIdentityValidation(nlTestSuite * inSuite, void * inContext) { - CHIP_ERROR err; CertificateKeyIdStorage keyId; - // Validate only - err = ValidateChipNetworkIdentity(sTestCert_PDCID01_Chip); - NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + // Test with both the full and compact TLV representations + for (auto && cert : { sTestCert_PDCID01_Chip, sTestCert_PDCID01_ChipCompact }) + { + // Validate only + NL_TEST_ASSERT(inSuite, ValidateChipNetworkIdentity(cert) == CHIP_NO_ERROR); + + // Validate and calculate identifier + keyId.fill(0xaa); + NL_TEST_ASSERT(inSuite, ValidateChipNetworkIdentity(cert, keyId) == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, CertificateKeyId(keyId).data_equal(sTestCert_PDCID01_KeyId)); + + // Extract identifier only + keyId.fill(0xaa); + NL_TEST_ASSERT(inSuite, ExtractIdentifierFromChipNetworkIdentity(cert, keyId) == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, CertificateKeyId(keyId).data_equal(sTestCert_PDCID01_KeyId)); + } +} - // Validate and calculate identifier - keyId.fill(0xaa); - err = ValidateChipNetworkIdentity(sTestCert_PDCID01_Chip, keyId); - NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); - NL_TEST_ASSERT(inSuite, CertificateKeyId(keyId).data_equal(sTestCert_PDCID01_KeyId)); +static void TestChipCert_PDCIdentityGeneration(nlTestSuite * inSuite, void * inContext) +{ + // Generate a new keypair + P256Keypair keypair; + NL_TEST_ASSERT(inSuite, keypair.Initialize(ECPKeyTarget::ECDSA) == CHIP_NO_ERROR); - // Extract identifier only - keyId.fill(0xaa); - err = ExtractIdentifierFromChipNetworkIdentity(sTestCert_PDCID01_Chip, keyId); - NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); - NL_TEST_ASSERT(inSuite, CertificateKeyId(keyId).data_equal(sTestCert_PDCID01_KeyId)); + // Generate an identity certificate based on the keypair + uint8_t buffer[kMaxCHIPCompactNetworkIdentityLength]; + MutableByteSpan cert(buffer); + NL_TEST_ASSERT(inSuite, NewChipNetworkIdentity(keypair, cert) == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, ValidateChipNetworkIdentity(cert) == CHIP_NO_ERROR); + + // It should round-trip to X.509 DER and back, and remain valid. + uint8_t derBuffer[kMaxDERCertLength]; + MutableByteSpan derCert(derBuffer); + NL_TEST_ASSERT(inSuite, ConvertChipCertToX509Cert(cert, derCert) == CHIP_NO_ERROR); + uint8_t tlvBuffer[kMaxCHIPCertLength]; + MutableByteSpan tlvCert(tlvBuffer); // won't be compact after round-tripping + NL_TEST_ASSERT(inSuite, ConvertX509CertToChipCert(derCert, tlvCert) == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, ValidateChipNetworkIdentity(tlvCert) == CHIP_NO_ERROR); } /** @@ -2228,6 +2250,7 @@ static const nlTest sTests[] = { NL_TEST_DEF("Test extracting Subject DN from chip certificate", TestChipCert_ExtractSubjectDNFromChipCert), NL_TEST_DEF("Test extracting PublicKey and SKID from chip certificate", TestChipCert_ExtractPublicKeyAndSKID), NL_TEST_DEF("Test PDC Identity Validation", TestChipCert_PDCIdentityValidation), + NL_TEST_DEF("Test PDC Identity Generation", TestChipCert_PDCIdentityGeneration), NL_TEST_SENTINEL() }; // clang-format on