From 22888b014ed1e56dec3f567ae1614222afc4c6d2 Mon Sep 17 00:00:00 2001 From: Tennessee Carmel-Veilleux Date: Thu, 16 Jun 2022 08:36:07 -0400 Subject: [PATCH] Introduce fail-safe compliant Operational Cert storage (#19643) * Introduce fail-safe compliant Operational Cert storage - This PR is an intermediate fully unit-tested step towards replacing all NOC/ICAC/RCAC storage from Fabric-Table to allow spec-compliant fail-safe implementation - Right now, we have hacky code in opcreds cluster server and in FabricTable to attempt to affect fail-safe properly. It doesn't actually work and prevents the TrustedRootCertificates, NOCs and Fabrics attributes from being implemented properly, and prevents UpdateNOC from being implemented to spec Issue #18633 Issue #17208 Issue #15585 This PR implements an operational cert storage interface and provides an implementation based on exact storage from existing FabricTable for backwards compatibility. Its usage in FabricTable and via opcreds cluster is a follow-up. Testing done: - Added large-scale new code unit test - Unit tests pass - Not hooked-up to device software yet, so just the unit tests need to pass * Restyled by clang-format * Remove 2 unused variables in unit test to fix CI Co-authored-by: Restyled.io --- src/credentials/BUILD.gn | 3 + src/credentials/OperationalCertificateStore.h | 248 ++++++ .../PersistentStorageOpCertStore.cpp | 509 +++++++++++ .../PersistentStorageOpCertStore.h | 131 +++ src/credentials/tests/BUILD.gn | 1 + .../TestPersistentStorageOpCertStore.cpp | 806 ++++++++++++++++++ src/crypto/OperationalKeystore.h | 19 +- 7 files changed, 1716 insertions(+), 1 deletion(-) create mode 100644 src/credentials/OperationalCertificateStore.h create mode 100644 src/credentials/PersistentStorageOpCertStore.cpp create mode 100644 src/credentials/PersistentStorageOpCertStore.h create mode 100644 src/credentials/tests/TestPersistentStorageOpCertStore.cpp diff --git a/src/credentials/BUILD.gn b/src/credentials/BUILD.gn index c907b0aaa83996..501a42718723fa 100644 --- a/src/credentials/BUILD.gn +++ b/src/credentials/BUILD.gn @@ -40,6 +40,9 @@ static_library("credentials") { "GroupDataProviderImpl.cpp", "LastKnownGoodTime.cpp", "LastKnownGoodTime.h", + "OperationalCertificateStore.h", + "PersistentStorageOpCertStore.cpp", + "PersistentStorageOpCertStore.h", "attestation_verifier/DeviceAttestationDelegate.h", "attestation_verifier/DeviceAttestationVerifier.cpp", "attestation_verifier/DeviceAttestationVerifier.h", diff --git a/src/credentials/OperationalCertificateStore.h b/src/credentials/OperationalCertificateStore.h new file mode 100644 index 00000000000000..4540da9d99621e --- /dev/null +++ b/src/credentials/OperationalCertificateStore.h @@ -0,0 +1,248 @@ +/* + * Copyright (c) 2022 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +namespace chip { +namespace Credentials { + +class OperationalCertificateStore +{ +public: + enum class CertChainElement : uint8_t + { + kRcac = 0, + kIcac = 1, + kNoc = 2 + }; + + virtual ~OperationalCertificateStore() {} + + // ==== API designed for commisionables to support fail-safe (although can be used by controllers) ==== + + /** + * @brief Returns true if a pending root certificate exists and is active from a previous + * `AddNewTrustedRootCertForFabric`. + */ + virtual bool HasPendingRootCert() const = 0; + + /** + * @brief Returns true if a pending operational certificate chain exists and is active from a previous + * `AddNewOpCertsForFabric` or `UpdateOpCertsForFabric`. + */ + virtual bool HasPendingNocChain() const = 0; + + /** + * @brief Returns whether a usable operational certificates chain exists for the given fabric. + * + * Returns true even if the certificates are not persisted yet. Only returns true if a certificate + * is presently usable such that `GetCertificate` would succeed for the fabric. + * + * @param fabricIndex - FabricIndex for which availability of certificate will be checked. + * @param element - Element of the certificate chain whose presence needs to be checked + * @return true if there an active obtainable operational certificate of the given type for the given FabricIndex, + * false otherwise. + */ + virtual bool HasCertificateForFabric(FabricIndex fabricIndex, CertChainElement element) const = 0; + + /** + * @brief Add and temporarily activate a new Trusted Root Certificate to storage for the given fabric + * + * The certificate is temporary until committed or reverted. + * The certificate is committed to storage on `CommitOpCertsForFabric`. + * The certificate is destroyed if `RevertPendingOpCerts` is called before `CommitOpCertsForFabric`. + * + * Only one pending trusted root certificate is supported at a time and it is illegal + * to call this method if there is already a persisted root certificate for the given + * fabric. + * + * Uniqueness constraints for roots (see AddTrustedRootCertificate command in spec) is not + * enforced by this method and must be done as a more holistic check elsewhere. Cryptographic + * signature verification or path validation is not enforced by this method. + * + * If `UpdateOpCertsForFabric` had been called before this method, this method will return + * CHIP_ERROR_INCORRECT_STATE since it is illegal to update trusted roots when updating an + * existing NOC chain. + * + * @param fabricIndex - FabricIndex for which a new trusted root certificate should be added + * @param rcac - Buffer containing the root certificate to add. + * + * @retval CHIP_NO_ERROR on success + * @retval CHIP_ERROR_NO_MEMORY if there is insufficient memory to maintain the temporary root cert + * @retval CHIP_ERROR_INVALID_ARGUMENT if the certificate is empty or too large + * @retval CHIP_ERROR_INCORRECT_STATE if the certificate store is not properly initialized, if this method + * is called after `UpdateOpCertsForFabric`, or if there was + * already a pending or persisted root certificate for the given `fabricIndex`. + * @retval other CHIP_ERROR value on internal errors + */ + virtual CHIP_ERROR AddNewTrustedRootCertForFabric(FabricIndex fabricIndex, const ByteSpan & rcac) = 0; + + /** + * @brief Add and temporarily activate an operational certificate chain for the given fabric. + * + * The certificate chain is temporary until committed or reverted. + * The certificate chain is committed to storage on `CommitOpCertsForFabric`. + * The certificate chain is destroyed if `RevertPendingOpCerts` is called before `CommitOpCertsForFabric`. + * + * Only one pending operational certificate chain is supported at a time and it is illegal + * to call this method if there is already a persisted certificate chain for the given + * fabric. + * + * Cryptographic signature verification or path validation is not enforced by this method. + * + * If `UpdateOpCertsForFabric` had been called before this method, this method will return + * CHIP_ERROR_INCORRECT_STATE since it is illegal to add a certificate chain after + * updating an existing NOC updating prior to a commit. + * + * If `AddNewTrustedRootCertForFabric` had not been called before this method, this method will + * return CHIP_ERROR_INCORRECT_STATE since it is illegal in this implementation to store an + * NOC chain without associated root. + * + * NOTE: The Matter spec allows AddNOC without AddTrustedRootCertificate if the NOC + * chains to an existing root, to support root reuse. In this implementation, we expect each + * fabric to store the root with the rest of the chain. Because of this, callers must ensure + * that if an AddNOC command is done and no trusted root was added, that the requisite existing + * root be "copied over" to match. + * + * @param fabricIndex - FabricIndex for which to add a new operational certificate chain + * @param noc - Buffer containing the NOC certificate to add + * @param icac - Buffer containing the ICAC certificate to add. If no ICAC is needed, `icac.empty()` must be true. + * + * @retval CHIP_NO_ERROR on success + * @retval CHIP_ERROR_NO_MEMORY if there is insufficient memory to maintain the temporary `noc` and `icac` cert copies + * @retval CHIP_ERROR_INVALID_ARGUMENT if either the noc or icac are invalid sizes + * @retval CHIP_ERROR_INVALID_FABRIC_INDEX if `fabricIndex` mismatches the one from previous successful + * `AddNewTrustedRootCertForFabric`. + * @retval CHIP_ERROR_INCORRECT_STATE if the certificate store is not properly initialized, if this method + * is called after `UpdateOpCertsForFabric`, or if there was + * already a pending or persisted operational cert chain for the given `fabricIndex`, + * or if AddNewTrustedRootCertForFabric had not yet been called for the given `fabricIndex`. + * @retval other CHIP_ERROR value on internal errors + */ + virtual CHIP_ERROR AddNewOpCertsForFabric(FabricIndex fabricIndex, const ByteSpan & noc, const ByteSpan & icac) = 0; + + /** + * @brief Update and temporarily activate an existing operational certificate chain for the given fabric. + * + * The certificate chain is temporary until committed or reverted. + * The certificate chain is committed to storage on `CommitOpCertsForFabric`. + * The certificate chain is reverted to prior storage if `RevertPendingOpCerts` is called + * before `CommitOpCertsForFabric`. + * + * Only one pending operational certificate chain is supported at a time and it is illegal + * to call this method if there was not already a persisted certificate chain for the given + * fabric. + * + * Cryptographic signature verification or path validation is not enforced by this method. + * + * If `AddNewOpCertsForFabric` had been called before this method, this method will return + * CHIP_ERROR_INCORRECT_STATE since it is illegal to update a certificate chain after + * adding an existing NOC updating prior to a commit. + * + * If there is no existing persisted trusted root certificate for the given fabricIndex, this + * method will method will eturn CHIP_ERROR_INCORRECT_STATE since it is illegal in this + * implementation to store an NOC chain without associated root. + * + * @param fabricIndex - FabricIndex for which to add a new operational certificate chain + * @param noc - Buffer containing the NOC certificate to update + * @param icac - Buffer containing the ICAC certificate to update. If no ICAC is needed, `icac.empty()` must be true. + * + * @retval CHIP_NO_ERROR on success + * @retval CHIP_ERROR_NO_MEMORY if there is insufficient memory to maintain the temporary `noc` and `icac` cert copies + * @retval CHIP_ERROR_INVALID_ARGUMENT if either the noc or icac are invalid sizes + * @retval CHIP_ERROR_INCORRECT_STATE if the certificate store is not properly initialized, if this method + * is called after `AddNewOpCertsForFabric`, or if there was + * already a pending cert chain for the given `fabricIndex`, + * or if there is no associated persisted root for for the given `fabricIndex`. + * @retval other CHIP_ERROR value on internal errors + */ + virtual CHIP_ERROR UpdateOpCertsForFabric(FabricIndex fabricIndex, const ByteSpan & noc, const ByteSpan & icac) = 0; + + /** + * @brief Permanently commit the certificate chain last setup via successful calls to + * legal combinations of `AddNewTrustedRootCertForFabric`, `AddNewOpCertsForFabric` or + * `UpdateOpCertsForFabric`, replacing previously committed data, if any. + * + * This is to be used when CommissioningComplete is successfully received + * + * @param fabricIndex - FabricIndex for which to commit the certificate chain, used for security cross-checking + * + * @retval CHIP_NO_ERROR on success + * @retval CHIP_ERROR_INCORRECT_STATE if the certificate store is not properly initialized, + * or if not valid pending state is available. + * @retval CHIP_ERROR_INVALID_FABRIC_INDEX if there are no pending certificate chain for `fabricIndex` + * @retval other CHIP_ERROR value on internal storage errors + */ + virtual CHIP_ERROR CommitOpCertsForFabric(FabricIndex fabricIndex) = 0; + + /** + * @brief Permanently remove the certificate chain associated with a fabric. + * + * This is to be used for fail-safe handling and RemoveFabric. Removes both the + * pending operational cert chain elements for the fabricIndex (if any) and the committed + * ones (if any). + * + * @param fabricIndex - FabricIndex for which to remove the operational cert chain + * + * @retval CHIP_NO_ERROR on success + * @retval CHIP_ERROR_INCORRECT_STATE if the certificate store is not properly initialized. + * @retval CHIP_ERROR_INVALID_FABRIC_INDEX if there was not operational certificate data at all for `fabricIndex` + * @retval other CHIP_ERROR value on internal storage errors + */ + virtual CHIP_ERROR RemoveOpCertsForFabric(FabricIndex fabricIndex) = 0; + + /** + * @brief Permanently release the operational certificate chain made via successful calls to + * legal combinations of `AddNewTrustedRootCertForFabric`, `AddNewOpCertsForFabric` or + * `UpdateOpCertsForFabric`, if any. + * + * This is to be used when a fail-safe expires prior to CommissioningComplete. + * + * This method cannot error-out and must always succeed, even on a no-op. This should + * be safe to do given that `CommitOpCertsForFabric` must succeed to make an operation + * certificate chain usable. + */ + virtual void RevertPendingOpCerts() = 0; + + /** + * @brief Get the operational certificate element requested, giving the pending data or committed + * data depending on prior `AddNewTrustedRootCertForFabric`, `AddNewOpCertsForFabric` or + * `UpdateOpCertsForFabric` calls. + * + * On success, the `outCertificate` span is resized to the size of the actual certificate read-back. + * + * @param fabricIndex - fabricIndex for which to get the certificate + * @param element - which element of the cerficate chain to get + * @param outCertificate - buffer to contain the certificate obtained from persistent or temporary storage + * + * @retval CHIP_NO_ERROR on success. + * @retval CHIP_ERROR_BUFFER_TOO_SMALL if `outCertificate` does not fit the certificate. + * @retval CHIP_ERROR_INCORRECT_STATE if the certificate store is not properly initialized. + * @retval CHIP_ERROR_NOT_FOUND if the element cannot be found. + * @retval CHIP_ERROR_INVALID_FABRIC_INDEX if the fabricIndex is invalid. + * @retval other CHIP_ERROR value on internal storage errors. + */ + virtual CHIP_ERROR GetCertificate(FabricIndex fabricIndex, CertChainElement element, + MutableByteSpan & outCertificate) const = 0; +}; + +} // namespace Credentials +} // namespace chip diff --git a/src/credentials/PersistentStorageOpCertStore.cpp b/src/credentials/PersistentStorageOpCertStore.cpp new file mode 100644 index 00000000000000..5cebd3adc430a1 --- /dev/null +++ b/src/credentials/PersistentStorageOpCertStore.cpp @@ -0,0 +1,509 @@ +/* + * Copyright (c) 2022 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "PersistentStorageOpCertStore.h" + +namespace chip { +namespace Credentials { + +namespace { + +using CertChainElement = OperationalCertificateStore::CertChainElement; + +const char * GetStorageKeyForCert(DefaultStorageKeyAllocator & keyAllocator, FabricIndex fabricIndex, CertChainElement element) +{ + const char * storageKey = nullptr; + + switch (element) + { + case CertChainElement::kNoc: + storageKey = keyAllocator.FabricNOC(fabricIndex); + break; + case CertChainElement::kIcac: + storageKey = keyAllocator.FabricICAC(fabricIndex); + break; + case CertChainElement::kRcac: + storageKey = keyAllocator.FabricRCAC(fabricIndex); + break; + default: + break; + } + + return storageKey; +} + +bool StorageHasCertificate(PersistentStorageDelegate * storage, FabricIndex fabricIndex, CertChainElement element) +{ + DefaultStorageKeyAllocator keyAllocator; + const char * storageKey = GetStorageKeyForCert(keyAllocator, fabricIndex, element); + + if (storageKey == nullptr) + { + return false; + } + + uint16_t keySize = 0; + CHIP_ERROR err = storage->SyncGetKeyValue(storageKey, nullptr, keySize); + + if (err == CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND) + { + // Obviously not found + return false; + } + + if (err == CHIP_ERROR_BUFFER_TOO_SMALL) + { + // On found, we actually expect an "error", since we didn't want to read it out. + return true; + } + + return false; +} + +CHIP_ERROR LoadCertFromStorage(PersistentStorageDelegate * storage, FabricIndex fabricIndex, CertChainElement element, + MutableByteSpan & outCert) +{ + DefaultStorageKeyAllocator keyAllocator; + const char * storageKey = GetStorageKeyForCert(keyAllocator, fabricIndex, element); + + uint16_t keySize = static_cast(outCert.size()); + CHIP_ERROR err = storage->SyncGetKeyValue(storageKey, outCert.data(), keySize); + + // Not finding an ICAC means we don't have one, so adjust to meet the API contract, where + // outCert.empty() will be true; + if ((element == CertChainElement::kIcac) && (err == CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND)) + { + outCert.reduce_size(0); + return CHIP_ERROR_NOT_FOUND; + } + + if (err == CHIP_NO_ERROR) + { + outCert.reduce_size(keySize); + } + else if (err == CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND) + { + // Convert persisted storage error to CHIP_ERROR_NOT_FOUND so that + // `PersistentStorageOpCertStore::GetCertificate` doesn't need to convert. + err = CHIP_ERROR_NOT_FOUND; + } + + return err; +} + +CHIP_ERROR SaveCertToStorage(PersistentStorageDelegate * storage, FabricIndex fabricIndex, CertChainElement element, + const ByteSpan & cert) +{ + DefaultStorageKeyAllocator keyAllocator; + const char * storageKey = GetStorageKeyForCert(keyAllocator, fabricIndex, element); + + // If provided an empty ICAC, we delete the ICAC key previously used. If not there, it's OK + if ((element == CertChainElement::kIcac) && (cert.empty())) + { + CHIP_ERROR err = storage->SyncDeleteKeyValue(storageKey); + if ((err == CHIP_NO_ERROR) || (err == CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND)) + { + return CHIP_NO_ERROR; + } + return err; + } + + return storage->SyncSetKeyValue(storageKey, cert.data(), static_cast(cert.size())); +} + +CHIP_ERROR DeleteCertFromStorage(PersistentStorageDelegate * storage, FabricIndex fabricIndex, CertChainElement element) +{ + DefaultStorageKeyAllocator keyAllocator; + const char * storageKey = GetStorageKeyForCert(keyAllocator, fabricIndex, element); + return storage->SyncDeleteKeyValue(storageKey); +} + +} // namespace + +bool PersistentStorageOpCertStore::HasPendingRootCert() const +{ + if (mStorage == nullptr) + { + return false; + } + + return (mPendingRcac.Get() != nullptr) && mStateFlags.Has(StateFlags::kAddNewTrustedRootCalled); +} + +bool PersistentStorageOpCertStore::HasPendingNocChain() const +{ + if (mStorage == nullptr) + { + return false; + } + + return (mPendingNoc.Get() != nullptr) && mStateFlags.HasAny(StateFlags::kAddNewOpCertsCalled, StateFlags::kUpdateOpCertsCalled); +} + +bool PersistentStorageOpCertStore::HasCertificateForFabric(FabricIndex fabricIndex, CertChainElement element) const +{ + if ((mStorage == nullptr) || !IsValidFabricIndex(fabricIndex)) + { + return false; + } + + // FabricIndex matches pending, we MAY have some pending data + if (fabricIndex == mPendingFabricIndex) + { + switch (element) + { + case CertChainElement::kRcac: + if (mPendingRcac.Get() != nullptr) + { + return true; + } + break; + case CertChainElement::kIcac: + if (mPendingIcac.Get() != nullptr) + { + return true; + } + // If we have a pending NOC and no pending ICAC, don't delegate to storage, return not found here + // since in the pending state, there truly is nothing. + if (mPendingNoc.Get() != nullptr) + { + return false; + } + break; + case CertChainElement::kNoc: + if (mPendingNoc.Get() != nullptr) + { + return true; + } + break; + default: + return false; + } + } + + return StorageHasCertificate(mStorage, fabricIndex, element); +} + +CHIP_ERROR PersistentStorageOpCertStore::AddNewTrustedRootCertForFabric(FabricIndex fabricIndex, const ByteSpan & rcac) +{ + ReturnErrorCodeIf(mStorage == nullptr, CHIP_ERROR_INCORRECT_STATE); + ReturnErrorCodeIf(!IsValidFabricIndex(fabricIndex), CHIP_ERROR_INVALID_FABRIC_INDEX); + ReturnErrorCodeIf(rcac.empty() || (rcac.size() > Credentials::kMaxCHIPCertLength), CHIP_ERROR_INVALID_ARGUMENT); + + ReturnErrorCodeIf(mStateFlags.HasAny(StateFlags::kUpdateOpCertsCalled, StateFlags::kAddNewTrustedRootCalled, + StateFlags::kAddNewOpCertsCalled), + CHIP_ERROR_INCORRECT_STATE); + ReturnErrorCodeIf(StorageHasCertificate(mStorage, fabricIndex, CertChainElement::kRcac), CHIP_ERROR_INCORRECT_STATE); + + Platform::ScopedMemoryBuffer rcacBuf; + ReturnErrorCodeIf(!rcacBuf.Alloc(rcac.size()), CHIP_ERROR_NO_MEMORY); + memcpy(rcacBuf.Get(), rcac.data(), rcac.size()); + + mPendingRcac = std::move(rcacBuf); + mPendingRcacSize = static_cast(rcac.size()); + + mPendingFabricIndex = fabricIndex; + mStateFlags.Set(StateFlags::kAddNewTrustedRootCalled); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR PersistentStorageOpCertStore::AddNewOpCertsForFabric(FabricIndex fabricIndex, const ByteSpan & noc, + const ByteSpan & icac) +{ + ReturnErrorCodeIf(mStorage == nullptr, CHIP_ERROR_INCORRECT_STATE); + ReturnErrorCodeIf(!IsValidFabricIndex(fabricIndex), CHIP_ERROR_INVALID_FABRIC_INDEX); + ReturnErrorCodeIf(noc.empty() || (noc.size() > Credentials::kMaxCHIPCertLength), CHIP_ERROR_INVALID_ARGUMENT); + ReturnErrorCodeIf(icac.size() > Credentials::kMaxCHIPCertLength, CHIP_ERROR_INVALID_ARGUMENT); + + // Can't have called UpdateOpCertsForFabric first, or called with pending certs + ReturnErrorCodeIf(mStateFlags.HasAny(StateFlags::kUpdateOpCertsCalled, StateFlags::kAddNewOpCertsCalled), + CHIP_ERROR_INCORRECT_STATE); + + // Need to have trusted roots installed to make the chain valid + ReturnErrorCodeIf(!mStateFlags.Has(StateFlags::kAddNewTrustedRootCalled), CHIP_ERROR_INCORRECT_STATE); + + // fabricIndex must match the current pending fabric + ReturnErrorCodeIf(fabricIndex != mPendingFabricIndex, CHIP_ERROR_INVALID_FABRIC_INDEX); + + // Can't have persisted NOC/ICAC for same fabric if adding + ReturnErrorCodeIf(StorageHasCertificate(mStorage, fabricIndex, CertChainElement::kNoc), CHIP_ERROR_INCORRECT_STATE); + ReturnErrorCodeIf(StorageHasCertificate(mStorage, fabricIndex, CertChainElement::kIcac), CHIP_ERROR_INCORRECT_STATE); + + Platform::ScopedMemoryBuffer nocBuf; + ReturnErrorCodeIf(!nocBuf.Alloc(noc.size()), CHIP_ERROR_NO_MEMORY); + memcpy(nocBuf.Get(), noc.data(), noc.size()); + + Platform::ScopedMemoryBuffer icacBuf; + if (icac.size() > 0) + { + ReturnErrorCodeIf(!icacBuf.Alloc(icac.size()), CHIP_ERROR_NO_MEMORY); + memcpy(icacBuf.Get(), icac.data(), icac.size()); + } + + mPendingNoc = std::move(nocBuf); + mPendingNocSize = static_cast(noc.size()); + + mPendingIcac = std::move(icacBuf); + mPendingIcacSize = static_cast(icac.size()); + + mPendingFabricIndex = fabricIndex; + + mStateFlags.Set(StateFlags::kAddNewOpCertsCalled); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR PersistentStorageOpCertStore::UpdateOpCertsForFabric(FabricIndex fabricIndex, const ByteSpan & noc, + const ByteSpan & icac) +{ + ReturnErrorCodeIf(mStorage == nullptr, CHIP_ERROR_INCORRECT_STATE); + ReturnErrorCodeIf(!IsValidFabricIndex(fabricIndex), CHIP_ERROR_INVALID_FABRIC_INDEX); + ReturnErrorCodeIf(noc.empty() || (noc.size() > Credentials::kMaxCHIPCertLength), CHIP_ERROR_INVALID_ARGUMENT); + ReturnErrorCodeIf(icac.size() > Credentials::kMaxCHIPCertLength, CHIP_ERROR_INVALID_ARGUMENT); + + // Can't have called AddNewOpCertsForFabric first, and should never get here after AddNewTrustedRootCertForFabric. + ReturnErrorCodeIf(mStateFlags.HasAny(StateFlags::kAddNewOpCertsCalled, StateFlags::kAddNewTrustedRootCalled), + CHIP_ERROR_INCORRECT_STATE); + + // Can't have already pending NOC from UpdateOpCerts not yet committed + ReturnErrorCodeIf(mStateFlags.HasAny(StateFlags::kUpdateOpCertsCalled), CHIP_ERROR_INCORRECT_STATE); + + // Need to have trusted roots installed to make the chain valid + ReturnErrorCodeIf(!StorageHasCertificate(mStorage, fabricIndex, CertChainElement::kRcac), CHIP_ERROR_INCORRECT_STATE); + + // Must have persisted NOC for same fabric if updating + ReturnErrorCodeIf(!StorageHasCertificate(mStorage, fabricIndex, CertChainElement::kNoc), CHIP_ERROR_INCORRECT_STATE); + + // Don't check for ICAC, we may not have had one before, but assume that if NOC is there, a + // previous chain was at least partially there + + Platform::ScopedMemoryBuffer nocBuf; + ReturnErrorCodeIf(!nocBuf.Alloc(noc.size()), CHIP_ERROR_NO_MEMORY); + memcpy(nocBuf.Get(), noc.data(), noc.size()); + + Platform::ScopedMemoryBuffer icacBuf; + if (icac.size() > 0) + { + ReturnErrorCodeIf(!icacBuf.Alloc(icac.size()), CHIP_ERROR_NO_MEMORY); + memcpy(icacBuf.Get(), icac.data(), icac.size()); + } + + mPendingNoc = std::move(nocBuf); + mPendingNocSize = static_cast(noc.size()); + + mPendingIcac = std::move(icacBuf); + mPendingIcacSize = static_cast(icac.size()); + + // For NOC update, UpdateOpCertsForFabric is what determines the pending fabric index, + // not a previous AddNewTrustedRootCertForFabric call. + mPendingFabricIndex = fabricIndex; + + mStateFlags.Set(StateFlags::kUpdateOpCertsCalled); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR PersistentStorageOpCertStore::CommitOpCertsForFabric(FabricIndex fabricIndex) +{ + VerifyOrReturnError(mStorage != nullptr, CHIP_ERROR_INCORRECT_STATE); + VerifyOrReturnError(IsValidFabricIndex(fabricIndex) && (fabricIndex == mPendingFabricIndex), CHIP_ERROR_INVALID_FABRIC_INDEX); + + VerifyOrReturnError(HasPendingNocChain(), CHIP_ERROR_INCORRECT_STATE); + if (HasPendingRootCert()) + { + // Neither of these conditions should have occurred based on other interlocks, but since + // committing certificates is a dangerous operation, we absolutely validate our assumptions. + ReturnErrorCodeIf(mStateFlags.Has(StateFlags::kUpdateOpCertsCalled), CHIP_ERROR_INCORRECT_STATE); + ReturnErrorCodeIf(!mStateFlags.Has(StateFlags::kAddNewTrustedRootCalled), CHIP_ERROR_INCORRECT_STATE); + } + + // TODO: Handle transaction marking to revert partial certs at next boot if we get interrupted by reboot. + + // Start committing NOC first so we don't have dangling roots if one was added. + ByteSpan pendingNocSpan{ mPendingNoc.Get(), mPendingNocSize }; + CHIP_ERROR nocErr = SaveCertToStorage(mStorage, mPendingFabricIndex, CertChainElement::kNoc, pendingNocSpan); + + // ICAC storage handles deleting on empty/missing + ByteSpan pendingIcacSpan{ mPendingIcac.Get(), mPendingIcacSize }; + CHIP_ERROR icacErr = SaveCertToStorage(mStorage, mPendingFabricIndex, CertChainElement::kIcac, pendingIcacSpan); + + CHIP_ERROR rcacErr = CHIP_NO_ERROR; + if (HasPendingRootCert()) + { + ByteSpan pendingRcacSpan{ mPendingRcac.Get(), mPendingRcacSize }; + rcacErr = SaveCertToStorage(mStorage, mPendingFabricIndex, CertChainElement::kRcac, pendingRcacSpan); + } + + // Remember which was the first error, and if any error occurred. + CHIP_ERROR stickyErr = nocErr; + stickyErr = (stickyErr != CHIP_NO_ERROR) ? stickyErr : icacErr; + stickyErr = (stickyErr != CHIP_NO_ERROR) ? stickyErr : rcacErr; + + if (stickyErr != CHIP_NO_ERROR) + { + // On Adds rather than updates, remove anything possibly stored for the new fabric on partial + // failure. + if (mStateFlags.Has(StateFlags::kAddNewOpCertsCalled)) + { + (void) DeleteCertFromStorage(mStorage, mPendingFabricIndex, CertChainElement::kNoc); + (void) DeleteCertFromStorage(mStorage, mPendingFabricIndex, CertChainElement::kIcac); + } + if (mStateFlags.Has(StateFlags::kAddNewTrustedRootCalled)) + { + (void) DeleteCertFromStorage(mStorage, mPendingFabricIndex, CertChainElement::kRcac); + } + if (mStateFlags.Has(StateFlags::kUpdateOpCertsCalled)) + { + // Can't do anything to clean-up here, but pretty sure the fabric is broken now... + // TODO: Handle transaction marking to revert certs if somehow failing store on update by pre-backing-up opcerts + } + + return stickyErr; + } + + // If we got here, we succeeded and can reset the pending certs: next `GetCertificate` will use the stored certs + RevertPendingOpCerts(); + return CHIP_NO_ERROR; +} + +bool PersistentStorageOpCertStore::HasAnyCertificateForFabric(FabricIndex fabricIndex) const +{ + VerifyOrReturnError(IsValidFabricIndex(fabricIndex), false); + + bool rcacMissing = !StorageHasCertificate(mStorage, fabricIndex, CertChainElement::kRcac); + bool icacMissing = !StorageHasCertificate(mStorage, fabricIndex, CertChainElement::kIcac); + bool nocMissing = !StorageHasCertificate(mStorage, fabricIndex, CertChainElement::kNoc); + bool anyPending = (mPendingRcac.Get() != nullptr) || (mPendingIcac.Get() != nullptr) || (mPendingNoc.Get() != nullptr); + + // If there was *no* state, pending or persisted, we have an error + if (rcacMissing && icacMissing && nocMissing && !anyPending) + { + return false; + } + + return true; +} + +CHIP_ERROR PersistentStorageOpCertStore::RemoveOpCertsForFabric(FabricIndex fabricIndex) +{ + VerifyOrReturnError(mStorage != nullptr, CHIP_ERROR_INCORRECT_STATE); + VerifyOrReturnError(IsValidFabricIndex(fabricIndex), CHIP_ERROR_INVALID_FABRIC_INDEX); + + // If there was *no* state, pending or persisted, we have an error + ReturnErrorCodeIf(!HasAnyCertificateForFabric(fabricIndex), CHIP_ERROR_INVALID_FABRIC_INDEX); + + // Clear any pending state + RevertPendingOpCerts(); + + // Remove all persisted certs for the given fabric, blindly + CHIP_ERROR nocErr = DeleteCertFromStorage(mStorage, fabricIndex, CertChainElement::kNoc); + CHIP_ERROR icacErr = DeleteCertFromStorage(mStorage, fabricIndex, CertChainElement::kIcac); + CHIP_ERROR rcacErr = DeleteCertFromStorage(mStorage, fabricIndex, CertChainElement::kRcac); + + // Ignore missing cert errors + nocErr = (nocErr == CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND) ? CHIP_NO_ERROR : nocErr; + icacErr = (icacErr == CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND) ? CHIP_NO_ERROR : icacErr; + rcacErr = (rcacErr == CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND) ? CHIP_NO_ERROR : rcacErr; + + // Find the first error and return that + CHIP_ERROR stickyErr = nocErr; + stickyErr = (stickyErr != CHIP_NO_ERROR) ? stickyErr : icacErr; + stickyErr = (stickyErr != CHIP_NO_ERROR) ? stickyErr : rcacErr; + + return stickyErr; +} + +CHIP_ERROR PersistentStorageOpCertStore::GetPendingCertificate(FabricIndex fabricIndex, CertChainElement element, + MutableByteSpan & outCertificate) const +{ + if (fabricIndex != mPendingFabricIndex) + { + return CHIP_ERROR_NOT_FOUND; + } + + // FabricIndex matches pending, we MAY have some pending data + switch (element) + { + case CertChainElement::kRcac: + if (mPendingRcac.Get() != nullptr) + { + ByteSpan rcacSpan{ mPendingRcac.Get(), static_cast(mPendingRcacSize) }; + return CopySpanToMutableSpan(rcacSpan, outCertificate); + } + break; + case CertChainElement::kIcac: + if (mPendingIcac.Get() != nullptr) + { + ByteSpan icacSpan{ mPendingIcac.Get(), static_cast(mPendingIcacSize) }; + return CopySpanToMutableSpan(icacSpan, outCertificate); + } + break; + case CertChainElement::kNoc: + if (mPendingNoc.Get() != nullptr) + { + ByteSpan nocSpan{ mPendingNoc.Get(), static_cast(mPendingNocSize) }; + return CopySpanToMutableSpan(nocSpan, outCertificate); + } + break; + default: + return CHIP_ERROR_INVALID_ARGUMENT; + } + + return CHIP_ERROR_NOT_FOUND; +} + +CHIP_ERROR PersistentStorageOpCertStore::GetCertificate(FabricIndex fabricIndex, CertChainElement element, + MutableByteSpan & outCertificate) const +{ + VerifyOrReturnError(mStorage != nullptr, CHIP_ERROR_INCORRECT_STATE); + VerifyOrReturnError(IsValidFabricIndex(fabricIndex), CHIP_ERROR_INVALID_FABRIC_INDEX); + + // Handle case of pending data + CHIP_ERROR err = GetPendingCertificate(fabricIndex, element, outCertificate); + if ((err == CHIP_NO_ERROR) || (err != CHIP_ERROR_NOT_FOUND)) + { + // Found in pending, or got a deeper error: return the pending cert status. + return err; + } + + // If we have a pending NOC and no pending ICAC, don't delegate to storage, return not found here + // since in the pending state, there truly is nothing. + + if ((err == CHIP_ERROR_NOT_FOUND) && (element == CertChainElement::kIcac) && (mPendingNoc.Get() != nullptr)) + { + // Don't delegate to storage if we just have a pending NOC and are missing the ICAC + return CHIP_ERROR_NOT_FOUND; + } + + // Not found in pending, let's look in persisted + return LoadCertFromStorage(mStorage, fabricIndex, element, outCertificate); +} + +} // namespace Credentials +} // namespace chip diff --git a/src/credentials/PersistentStorageOpCertStore.h b/src/credentials/PersistentStorageOpCertStore.h new file mode 100644 index 00000000000000..3c2f36e7c53802 --- /dev/null +++ b/src/credentials/PersistentStorageOpCertStore.h @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2022 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "OperationalCertificateStore.h" + +namespace chip { +namespace Credentials { + +/** + * @brief OperationalCertificateStore implementation making use of PersistentStorageDelegate + * to load/store certificates. This is the legacy behavior of `FabricTable` prior + * to refactors to use `OperationalCertificateStore` and exists as a baseline example + * of how to use the interface. + */ +class PersistentStorageOpCertStore : public OperationalCertificateStore +{ +public: + PersistentStorageOpCertStore() = default; + virtual ~PersistentStorageOpCertStore() { Finish(); } + + /** + * @brief Initialize the certificate store to map to a given storage delegate. + * + * @param storage Pointer to persistent storage delegate to use. Must outlive this instance. + * @retval CHIP_NO_ERROR on success + * @retval CHIP_ERROR_INCORRECT_STATE if already initialized + */ + CHIP_ERROR Init(PersistentStorageDelegate * storage) + { + VerifyOrReturnError(mStorage == nullptr, CHIP_ERROR_INCORRECT_STATE); + RevertPendingOpCerts(); + mStorage = storage; + return CHIP_NO_ERROR; + } + + /** + * @brief Finalize the certificate sotre, so that subsequent operations fail + */ + void Finish() + { + VerifyOrReturn(mStorage != nullptr); + + RevertPendingOpCerts(); + mStorage = nullptr; + } + + bool HasPendingRootCert() const override; + bool HasPendingNocChain() const override; + + bool HasCertificateForFabric(FabricIndex fabricIndex, CertChainElement element) const override; + + CHIP_ERROR AddNewTrustedRootCertForFabric(FabricIndex fabricIndex, const ByteSpan & rcac) override; + CHIP_ERROR AddNewOpCertsForFabric(FabricIndex fabricIndex, const ByteSpan & noc, const ByteSpan & icac) override; + CHIP_ERROR UpdateOpCertsForFabric(FabricIndex fabricIndex, const ByteSpan & noc, const ByteSpan & icac) override; + + CHIP_ERROR CommitOpCertsForFabric(FabricIndex fabricIndex) override; + CHIP_ERROR RemoveOpCertsForFabric(FabricIndex fabricIndex) override; + + void RevertPendingOpCerts() override + { + mPendingRcac.Free(); + mPendingIcac.Free(); + mPendingNoc.Free(); + + mPendingRcacSize = 0; + mPendingIcacSize = 0; + mPendingNocSize = 0; + + mPendingFabricIndex = kUndefinedFabricIndex; + mStateFlags.ClearAll(); + } + + CHIP_ERROR GetCertificate(FabricIndex fabricIndex, CertChainElement element, MutableByteSpan & outCertificate) const override; + +protected: + enum class StateFlags : uint8_t + { + // Below are flags to assist interlock logic + kAddNewOpCertsCalled = (1u << 0), + kAddNewTrustedRootCalled = (1u << 1), + kUpdateOpCertsCalled = (1u << 2), + }; + + // Returns CHIP_ERROR_NOT_FOUND if a pending certificate couldn't be found, otherwise status of pending copy + CHIP_ERROR GetPendingCertificate(FabricIndex fabricIndex, CertChainElement element, MutableByteSpan & outCertificate) const; + + // Returns true if any pending or persisted state exists for the fabricIndex, false if nothing at all is found. + bool HasAnyCertificateForFabric(FabricIndex fabricIndex) const; + + PersistentStorageDelegate * mStorage = nullptr; + + // This pending fabric index is `kUndefinedFabricIndex` if there are no pending certs at all for the fabric + FabricIndex mPendingFabricIndex = kUndefinedFabricIndex; + + Platform::ScopedMemoryBuffer mPendingRcac; + Platform::ScopedMemoryBuffer mPendingIcac; + Platform::ScopedMemoryBuffer mPendingNoc; + + uint16_t mPendingRcacSize = 0; + uint16_t mPendingIcacSize = 0; + uint16_t mPendingNocSize = 0; + + BitFlags mStateFlags; +}; + +} // namespace Credentials +} // namespace chip diff --git a/src/credentials/tests/BUILD.gn b/src/credentials/tests/BUILD.gn index 5a31b6003b1c3e..164712b1dceb8c 100644 --- a/src/credentials/tests/BUILD.gn +++ b/src/credentials/tests/BUILD.gn @@ -47,6 +47,7 @@ chip_test_suite("tests") { "TestDeviceAttestationCredentials.cpp", "TestFabricTable.cpp", "TestGroupDataProvider.cpp", + "TestPersistentStorageOpCertStore.cpp", ] cflags = [ "-Wconversion" ] diff --git a/src/credentials/tests/TestPersistentStorageOpCertStore.cpp b/src/credentials/tests/TestPersistentStorageOpCertStore.cpp new file mode 100644 index 00000000000000..7920a063eb29c7 --- /dev/null +++ b/src/credentials/tests/TestPersistentStorageOpCertStore.cpp @@ -0,0 +1,806 @@ +/* + * + * Copyright (c) 2022 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace chip; +using namespace chip::Credentials; +using CertChainElement = OperationalCertificateStore::CertChainElement; + +namespace { + +constexpr FabricIndex kFabricIndex1 = 1; +constexpr FabricIndex kFabricIndex2 = 2; +constexpr FabricIndex kOtherFabricIndex = static_cast(kFabricIndex1 + 10u); + +// The PersistentStorageOpCertStore does not validate cert contents, so we can use simple constants +const uint8_t kTestRcacBuf[] = { 'r', 'c', 'a', 'c' }; +const ByteSpan kTestRcacSpan{ kTestRcacBuf }; + +const uint8_t kTestIcacBuf[] = { 'i', 'c', 'a', 'c' }; +const ByteSpan kTestIcacSpan{ kTestIcacBuf }; + +const uint8_t kTestNocBuf[] = { 'n', 'o', 'c' }; +const ByteSpan kTestNocSpan{ kTestNocBuf }; + +void TestAddNocFlow(nlTestSuite * inSuite, void * inContext) +{ + TestPersistentStorageDelegate storageDelegate; + PersistentStorageOpCertStore opCertStore; + DefaultStorageKeyAllocator keyAllocator; + + // Failure before Init + CHIP_ERROR err = opCertStore.AddNewTrustedRootCertForFabric(kFabricIndex1, kTestRcacSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_INCORRECT_STATE); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 0); + + // Init succeeds + err = opCertStore.Init(&storageDelegate); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + + // Manually add existing root for the FabricIndex, should fail AddNewTrustedRootCertForFabric for + // same fabric but succeed GetCertificate. + const uint8_t kTestRcacBufExists[] = { 'r', 'c', 'a', 'c', ' ', 'e', 'x', 'i', 's', 't', 's' }; + + err = storageDelegate.SyncSetKeyValue(keyAllocator.FabricRCAC(kFabricIndex1), kTestRcacBufExists, sizeof(kTestRcacBufExists)); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 1); + NL_TEST_ASSERT(inSuite, + opCertStore.HasCertificateForFabric(kFabricIndex1, CertChainElement::kRcac) == true); //< From manual add + + err = opCertStore.AddNewTrustedRootCertForFabric(kFabricIndex1, kTestRcacSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_INCORRECT_STATE); + + uint8_t largeBuf[400]; + MutableByteSpan largeSpan{ largeBuf }; + + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kNoc, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_NOT_FOUND); + + largeSpan = MutableByteSpan{ largeBuf }; + + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kRcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestRcacBufExists })); + + // Adding root for another FabricIndex should work + err = opCertStore.AddNewTrustedRootCertForFabric(kUndefinedFabricIndex, kTestRcacSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_INVALID_FABRIC_INDEX); + + err = opCertStore.AddNewTrustedRootCertForFabric(kFabricIndex2, kTestRcacSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 1); //< Storage count did not yet increase + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingNocChain()); + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingRootCert() == true); + + // Should be able to read pending RCAC right away + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex2, CertChainElement::kRcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestRcacSpan })); + + // Trying to commit with pending RCAC but no NOC should fail but leave everything as-is + err = opCertStore.CommitOpCertsForFabric(kFabricIndex2); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_INCORRECT_STATE); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingNocChain()); + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingRootCert() == true); + + // Trying to do AddNewOpCertsForFabric for fabric different that with pending RCAC should fail + err = opCertStore.AddNewOpCertsForFabric(kOtherFabricIndex, kTestNocSpan, kTestIcacSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_INVALID_FABRIC_INDEX); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingNocChain()); + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingRootCert() == true); + + // Clear other bad cases from storage for now + storageDelegate.ClearStorage(); + + // Trying to do AddNewOpCertsForFabric for same fabric as that with pending RCAC should fail + // if there are already existing NOC chain elements for the given fabric. + const uint8_t kTestIcacBufExists[] = { 'i', 'c', 'a', 'c', ' ', 'e', 'x', 'i', 's', 't', 's' }; + const uint8_t kTestNocBufExists[] = { 'n', 'o', 'c', ' ', 'e', 'x', 'i', 's', 't', 's' }; + + err = storageDelegate.SyncSetKeyValue(keyAllocator.FabricICAC(kFabricIndex2), kTestIcacBufExists, sizeof(kTestIcacBufExists)); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 1); + + err = storageDelegate.SyncSetKeyValue(keyAllocator.FabricNOC(kFabricIndex2), kTestNocBufExists, sizeof(kTestNocBufExists)); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 2); + + NL_TEST_ASSERT(inSuite, + opCertStore.HasCertificateForFabric(kFabricIndex2, CertChainElement::kIcac) == true); //< From manual add + NL_TEST_ASSERT(inSuite, opCertStore.HasCertificateForFabric(kFabricIndex2, CertChainElement::kNoc) == true); //< From manual add + + err = opCertStore.AddNewOpCertsForFabric(kFabricIndex2, kTestNocSpan, kTestIcacSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_INCORRECT_STATE); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingNocChain()); + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingRootCert() == true); + + storageDelegate.SyncDeleteKeyValue(keyAllocator.FabricICAC(kFabricIndex2)); + storageDelegate.SyncDeleteKeyValue(keyAllocator.FabricNOC(kFabricIndex2)); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 0); + + // Trying to do AddNewOpCertsForFabric for same fabric as that with pending RCAC should succeed + err = opCertStore.AddNewOpCertsForFabric(kFabricIndex2, kTestNocSpan, kTestIcacSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingNocChain()); + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingRootCert() == true); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 0); //< Storage count did not yet increase + + // Should be able to get the pending cert even if not in persisted storage + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex2, CertChainElement::kIcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestIcacSpan })); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex2, CertChainElement::kNoc, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestNocSpan })); + + // Trying to do AddNewOpCertsForFabric a second time after success before commit should fail, + // but leave state as-is + err = opCertStore.AddNewOpCertsForFabric(kFabricIndex2, kTestNocSpan, kTestIcacSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_INCORRECT_STATE); + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingNocChain()); + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingRootCert() == true); + + // Should be able to get the pending cert even if not in persisted storage, after an API error + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex2, CertChainElement::kIcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestIcacSpan })); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex2, CertChainElement::kNoc, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestNocSpan })); + + // Trying to commit with wrong FabricIndex should fail + err = opCertStore.CommitOpCertsForFabric(kOtherFabricIndex); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_INVALID_FABRIC_INDEX); + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingNocChain()); + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingRootCert() == true); + + // Commiting new certs should succeed on correct fabric + err = opCertStore.CommitOpCertsForFabric(kFabricIndex2); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingNocChain()); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingRootCert()); + + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 3); //< All certs now committed + + // Should be able to get the committed certs + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex2, CertChainElement::kRcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestRcacSpan })); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex2, CertChainElement::kIcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestIcacSpan })); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex2, CertChainElement::kNoc, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestNocSpan })); + + opCertStore.Finish(); +} + +void TestUpdateNocFlow(nlTestSuite * inSuite, void * inContext) +{ + TestPersistentStorageDelegate storageDelegate; + PersistentStorageOpCertStore opCertStore; + DefaultStorageKeyAllocator keyAllocator; + + // Failure before Init + CHIP_ERROR err = opCertStore.UpdateOpCertsForFabric(kFabricIndex1, kTestNocSpan, kTestIcacSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_INCORRECT_STATE); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 0); + + // Init succeeds + err = opCertStore.Init(&storageDelegate); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + + // Add a new pending trusted root to test for UpdateOpCertsForFabric failure on new root present + err = opCertStore.AddNewTrustedRootCertForFabric(kFabricIndex1, kTestRcacSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 0); //< Storage count did not yet increase + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingRootCert() == true); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingNocChain()); + + // Trying to do an UpdateOpCertsForFabric with new root pending should fail + err = opCertStore.UpdateOpCertsForFabric(kFabricIndex1, kTestNocSpan, kTestIcacSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_INCORRECT_STATE); + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingRootCert() == true); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingNocChain()); + + // Revert state for next tests + opCertStore.RevertPendingOpCerts(); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingNocChain()); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingRootCert()); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 0); //< Storage count did not yet increase + + // Manually add root, ICAC and NOC to validate update since existing chain required + const uint8_t kTestRcacBufExists[] = { 'r', 'c', 'a', 'c', ' ', 'e', 'x', 'i', 's', 't', 's' }; + const uint8_t kTestIcacBufExists[] = { 'i', 'c', 'a', 'c', ' ', 'e', 'x', 'i', 's', 't', 's' }; + const uint8_t kTestNocBufExists[] = { 'n', 'o', 'c', ' ', 'e', 'x', 'i', 's', 't', 's' }; + + uint8_t largeBuf[400]; + MutableByteSpan largeSpan{ largeBuf }; + + { + err = + storageDelegate.SyncSetKeyValue(keyAllocator.FabricRCAC(kFabricIndex1), kTestRcacBufExists, sizeof(kTestRcacBufExists)); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 1); + NL_TEST_ASSERT(inSuite, + opCertStore.HasCertificateForFabric(kFabricIndex1, CertChainElement::kRcac) == true); //< From manual add + + err = + storageDelegate.SyncSetKeyValue(keyAllocator.FabricICAC(kFabricIndex1), kTestIcacBufExists, sizeof(kTestIcacBufExists)); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 2); + NL_TEST_ASSERT(inSuite, + opCertStore.HasCertificateForFabric(kFabricIndex1, CertChainElement::kIcac) == true); //< From manual add + + err = storageDelegate.SyncSetKeyValue(keyAllocator.FabricNOC(kFabricIndex1), kTestNocBufExists, sizeof(kTestNocBufExists)); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 3); + NL_TEST_ASSERT(inSuite, + opCertStore.HasCertificateForFabric(kFabricIndex1, CertChainElement::kNoc) == true); //< From manual add + + // Test that we can manually stored certs + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kRcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestRcacBufExists })); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kIcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestIcacBufExists })); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kNoc, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestNocBufExists })); + } + + // Update fails on fabric with wrong FabricIndex + err = opCertStore.UpdateOpCertsForFabric(kOtherFabricIndex, kTestNocSpan, kTestIcacSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_INCORRECT_STATE); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingRootCert()); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingNocChain()); + + // Update succeeds on fabric with existing data + err = opCertStore.UpdateOpCertsForFabric(kFabricIndex1, kTestNocSpan, kTestIcacSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingRootCert()); + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingNocChain() == true); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 3); + + // Can read back existing root unchanged + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kRcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestRcacBufExists })); + + // NOC chain elements see the pending updated certs + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kIcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestIcacBuf })); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kNoc, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestNocBuf })); + + // Trying update again fails + err = opCertStore.UpdateOpCertsForFabric(kFabricIndex1, kTestNocSpan, kTestIcacSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_INCORRECT_STATE); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingRootCert()); + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingNocChain() == true); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 3); + + // Trying to add a new root after update, before commit/revert fails + err = opCertStore.AddNewTrustedRootCertForFabric(kFabricIndex1, kTestRcacSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_INCORRECT_STATE); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingRootCert()); + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingNocChain() == true); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 3); + + // Trying to add new opcerts for any fabric after update, before commit/revert fails + err = opCertStore.AddNewOpCertsForFabric(kFabricIndex1, kTestNocSpan, kTestIcacSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_INCORRECT_STATE); + + err = opCertStore.AddNewOpCertsForFabric(kFabricIndex2, kTestNocSpan, kTestIcacSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_INCORRECT_STATE); + + // Committing writes the new values (we even "background-remove" the old ICAC/NOC before commit) + storageDelegate.SyncDeleteKeyValue(keyAllocator.FabricICAC(kFabricIndex1)); + storageDelegate.SyncDeleteKeyValue(keyAllocator.FabricNOC(kFabricIndex1)); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 1); //< Root remains + + err = opCertStore.CommitOpCertsForFabric(kFabricIndex1); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingNocChain()); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingRootCert()); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 3); //< All certs now committed + + // Should be able to get the committed cert even if not in persisted storage, after an API error + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kRcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestRcacBufExists })); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kIcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestIcacSpan })); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kNoc, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestNocSpan })); + + // Calling revert doesn't undo the work we just did + opCertStore.RevertPendingOpCerts(); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 3); //< All certs now committed + + // Verify the revert after commit left all data alone + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kRcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestRcacBufExists })); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kIcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestIcacSpan })); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kNoc, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestNocSpan })); + + // Verify that RemoveOpCertsForFabric fails on fabric with no data + err = opCertStore.RemoveOpCertsForFabric(kFabricIndex2); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_INVALID_FABRIC_INDEX); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 3); + + // Verify that RemoveOpCertsForFabric works for fabric we just updated + err = opCertStore.RemoveOpCertsForFabric(kFabricIndex1); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 0); // All keys gone + + opCertStore.Finish(); +} + +void TestReverts(nlTestSuite * inSuite, void * inContext) +{ + TestPersistentStorageDelegate storageDelegate; + PersistentStorageOpCertStore opCertStore; + + // Failure before Init + CHIP_ERROR err = opCertStore.RemoveOpCertsForFabric(kFabricIndex1); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_INCORRECT_STATE); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 0); + + // Init succeeds + err = opCertStore.Init(&storageDelegate); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + + // Add a new pending trusted root + uint8_t largeBuf[400]; + MutableByteSpan largeSpan{ largeBuf }; + + err = opCertStore.AddNewTrustedRootCertForFabric(kFabricIndex1, kTestRcacSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 0); //< Storage count did not yet increase + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingRootCert() == true); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingNocChain()); + + // Verify we can see the new trusted root + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kRcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(kTestRcacSpan)); + + // Verify that after revert, we can't see the root anymore + opCertStore.RevertPendingOpCerts(); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingRootCert()); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 0); //< Storage count did not yet increase + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kRcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_NOT_FOUND); + + { + // Add new root again, to then test review of AddNewTrustedCertificates + err = opCertStore.AddNewTrustedRootCertForFabric(kFabricIndex1, kTestRcacSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 0); //< Storage count did not yet increase + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingRootCert() == true); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingNocChain()); + + // Add NOC chain + err = opCertStore.AddNewOpCertsForFabric(kFabricIndex1, kTestNocSpan, kTestIcacSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingNocChain()); + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingRootCert() == true); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 0); //< Storage count did not yet increase + } + + // Make sure we can see all pending certs before revert + { + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kRcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(kTestRcacSpan)); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kIcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(kTestIcacSpan)); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kNoc, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(kTestNocSpan)); + } + + // Revert + opCertStore.RevertPendingOpCerts(); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingRootCert()); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingNocChain()); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 0); //< Storage count did not yet increase + + // Verify that after revert, we can't see the root or chain anymore + { + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kRcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_NOT_FOUND); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kIcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_NOT_FOUND); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kNoc, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_NOT_FOUND); + } + + // Start again to add a new set, but then let's commit + { + // Add new root again, to then test review of AddNewTrustedCertificates + err = opCertStore.AddNewTrustedRootCertForFabric(kFabricIndex1, kTestRcacSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 0); //< Storage count did not yet increase + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingRootCert() == true); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingNocChain()); + + // Add NOC chain + err = opCertStore.AddNewOpCertsForFabric(kFabricIndex1, kTestNocSpan, kTestIcacSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingNocChain()); + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingRootCert() == true); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 0); //< Storage count did not yet increase + } + + // Commiting new certs should succeed on correct fabric + err = opCertStore.CommitOpCertsForFabric(kFabricIndex1); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingNocChain()); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingRootCert()); + + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 3); //< All certs now committed + + // Should be able to get the committed certs + { + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kRcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestRcacSpan })); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kIcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestIcacSpan })); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kNoc, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestNocSpan })); + } + + const uint8_t kNewNoc[] = { 'n', 'o', 'c', ' ', 'n', 'e', 'w' }; + + // Updating certs should work (NO ICAC) + err = opCertStore.UpdateOpCertsForFabric(kFabricIndex1, ByteSpan{ kNewNoc }, ByteSpan{}); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingRootCert()); + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingNocChain()); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 3); //< No change to keys + + // Should see committed root, pending NOC, absent ICAC + { + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kRcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestRcacSpan })); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kIcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_NOT_FOUND); + NL_TEST_ASSERT(inSuite, !opCertStore.HasCertificateForFabric(kFabricIndex1, CertChainElement::kIcac)); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kNoc, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kNewNoc })); + } + + // Revert, should be back at previous state + opCertStore.RevertPendingOpCerts(); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingRootCert()); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingNocChain()); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 3); //< Storage count did not yet change + + // Should be able to get the previously committed certs + { + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kRcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestRcacSpan })); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kIcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestIcacSpan })); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kNoc, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestNocSpan })); + } + + // Try again to update with missing ICAC and commit + err = opCertStore.UpdateOpCertsForFabric(kFabricIndex1, ByteSpan{ kNewNoc }, ByteSpan{}); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingRootCert()); + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingNocChain()); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 3); //< No change to keys + + err = opCertStore.CommitOpCertsForFabric(kFabricIndex1); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingRootCert()); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingNocChain()); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 2); //< ICAC cert should be gone + + // Should see committed root, new NOC, absent ICAC + { + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kRcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestRcacSpan })); + NL_TEST_ASSERT(inSuite, opCertStore.HasCertificateForFabric(kFabricIndex1, CertChainElement::kRcac)); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kIcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_NOT_FOUND); + NL_TEST_ASSERT(inSuite, !opCertStore.HasCertificateForFabric(kFabricIndex1, CertChainElement::kIcac)); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kNoc, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kNewNoc })); + NL_TEST_ASSERT(inSuite, opCertStore.HasCertificateForFabric(kFabricIndex1, CertChainElement::kNoc)); + } + + opCertStore.Finish(); +} + +void TestRevertAddNoc(nlTestSuite * inSuite, void * inContext) +{ + TestPersistentStorageDelegate storageDelegate; + PersistentStorageOpCertStore opCertStore; + + // Init succeeds + CHIP_ERROR err = opCertStore.Init(&storageDelegate); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + + // Add a new pending trusted root + uint8_t largeBuf[400]; + MutableByteSpan largeSpan{ largeBuf }; + + { + // Add new root + err = opCertStore.AddNewTrustedRootCertForFabric(kFabricIndex1, kTestRcacSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 0); //< Storage count did not yet increase + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingRootCert() == true); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingNocChain()); + + // Add NOC chain, with NO ICAC + err = opCertStore.AddNewOpCertsForFabric(kFabricIndex1, kTestNocSpan, ByteSpan{}); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingNocChain()); + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingRootCert() == true); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 0); //< Storage count did not yet increase + } + + // Make sure we get expected pending state before revert + { + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kRcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(kTestRcacSpan)); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kIcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_NOT_FOUND); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kNoc, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(kTestNocSpan)); + } + + // Revert using RemoveOpCertsForFabric + err = opCertStore.RemoveOpCertsForFabric(kFabricIndex1); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingRootCert()); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingNocChain()); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 0); //< Storage count did not yet increase + + // Add again, and commit + { + // Add new root + err = opCertStore.AddNewTrustedRootCertForFabric(kFabricIndex1, kTestRcacSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 0); //< Storage count did not yet increase + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingRootCert() == true); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingNocChain()); + + // Add NOC chain, with NO ICAC + err = opCertStore.AddNewOpCertsForFabric(kFabricIndex1, kTestNocSpan, ByteSpan{}); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingNocChain()); + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingRootCert() == true); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 0); //< Storage count did not yet increase + + err = opCertStore.CommitOpCertsForFabric(kFabricIndex1); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingRootCert()); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingNocChain()); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 2); //< We have RCAC, NOC, no ICAC + } + + // Update to add an ICAC + const uint8_t kNewIcac[] = { 'i', 'c', 'a', 'c', ' ', 'n', 'e', 'w' }; + const uint8_t kNewNoc[] = { 'n', 'o', 'c', ' ', 'n', 'e', 'w' }; + + // Updating certs should work (NO ICAC) + err = opCertStore.UpdateOpCertsForFabric(kFabricIndex1, ByteSpan{ kNewNoc }, ByteSpan{ kNewIcac }); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingRootCert()); + NL_TEST_ASSERT(inSuite, opCertStore.HasPendingNocChain()); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 2); //< No change to keys + + // Should see committed root, pending NOC, pending ICAC + { + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kRcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestRcacSpan })); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kIcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kNewIcac })); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kNoc, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kNewNoc })); + } + + // Commit, should see the new ICAC appear. + err = opCertStore.CommitOpCertsForFabric(kFabricIndex1); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingRootCert()); + NL_TEST_ASSERT(inSuite, !opCertStore.HasPendingNocChain()); + NL_TEST_ASSERT(inSuite, storageDelegate.GetNumKeys() == 3); //< We have RCAC, NOC, ICAC + + // Should see committed root, new NOC, new ICAC + { + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kRcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kTestRcacSpan })); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kIcac, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kNewIcac })); + + largeSpan = MutableByteSpan{ largeBuf }; + err = opCertStore.GetCertificate(kFabricIndex1, CertChainElement::kNoc, largeSpan); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, largeSpan.data_equal(ByteSpan{ kNewNoc })); + } + + opCertStore.Finish(); +} + +/** + * Test Suite. It lists all the test functions. + */ +static const nlTest sTests[] = { + NL_TEST_DEF("Test AddNOC-like flows PersistentStorageOpCertStore", TestAddNocFlow), + NL_TEST_DEF("Test UpdateNOC-like flows PersistentStorageOpCertStore", TestUpdateNocFlow), + NL_TEST_DEF("Test revert operations of PersistentStorageOpCertStore", TestReverts), + NL_TEST_DEF("Test revert operations with AddNOC of PersistentStorageOpCertStore", TestRevertAddNoc), NL_TEST_SENTINEL() +}; + +/** + * Set up the test suite. + */ +int Test_Setup(void * inContext) +{ + CHIP_ERROR error = chip::Platform::MemoryInit(); + VerifyOrReturnError(error == CHIP_NO_ERROR, FAILURE); + return SUCCESS; +} + +/** + * Tear down the test suite. + */ +int Test_Teardown(void * inContext) +{ + chip::Platform::MemoryShutdown(); + return SUCCESS; +} + +} // namespace + +/** + * Main + */ +int TestPersistentStorageOpCertStore() +{ + nlTestSuite theSuite = { "PersistentStorageOpCertStore tests", &sTests[0], Test_Setup, Test_Teardown }; + + // Run test suite againt one context. + nlTestRunner(&theSuite, nullptr); + return nlTestRunnerStats(&theSuite); +} + +CHIP_REGISTER_TEST_SUITE(TestPersistentStorageOpCertStore) diff --git a/src/crypto/OperationalKeystore.h b/src/crypto/OperationalKeystore.h index 29f13290d42f92..443542bf8b7bfe 100644 --- a/src/crypto/OperationalKeystore.h +++ b/src/crypto/OperationalKeystore.h @@ -1,3 +1,20 @@ +/* + * Copyright (c) 2022 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include @@ -44,7 +61,7 @@ class OperationalKeystore * * The keypair is temporary and becomes usable for `SignWithOpKeypair` only after either * `ActivateOpKeypairForFabric` is called. It is destroyed if - * `RevertPendingKeypair` or `Finish` is called before `CommitOpKeypairForFabric`. + * `RevertPendingKeypair` is called before `CommitOpKeypairForFabric`. * If a pending keypair already existed for the given `fabricIndex`, it is replaced by this call. * * Only one pending operational keypair is supported at a time.