| /* |
| * 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 <credentials/CHIPCert.h> |
| #include <lib/core/CHIPError.h> |
| #include <lib/core/CHIPPersistentStorageDelegate.h> |
| #include <lib/core/CHIPTLV.h> |
| #include <lib/core/DataModelTypes.h> |
| #include <lib/support/CHIPMem.h> |
| #include <lib/support/CodeUtils.h> |
| #include <lib/support/DefaultStorageKeyAllocator.h> |
| #include <lib/support/SafeInt.h> |
| #include <lib/support/ScopedBuffer.h> |
| |
| #include "PersistentStorageOpCertStore.h" |
| |
| namespace chip { |
| namespace Credentials { |
| |
| namespace { |
| |
| using CertChainElement = OperationalCertificateStore::CertChainElement; |
| |
| StorageKeyName GetStorageKeyForCert(FabricIndex fabricIndex, CertChainElement element) |
| { |
| switch (element) |
| { |
| case CertChainElement::kNoc: |
| return DefaultStorageKeyAllocator::FabricNOC(fabricIndex); |
| break; |
| case CertChainElement::kIcac: |
| return DefaultStorageKeyAllocator::FabricICAC(fabricIndex); |
| break; |
| case CertChainElement::kRcac: |
| return DefaultStorageKeyAllocator::FabricRCAC(fabricIndex); |
| break; |
| default: |
| break; |
| } |
| |
| return StorageKeyName::Uninitialized(); |
| } |
| |
| bool StorageHasCertificate(PersistentStorageDelegate * storage, FabricIndex fabricIndex, CertChainElement element) |
| { |
| StorageKeyName storageKey = GetStorageKeyForCert(fabricIndex, element); |
| |
| if (!storageKey) |
| { |
| return false; |
| } |
| |
| // TODO(#16958): need to actually read the cert to know if it's there due to platforms not |
| // properly enforcing CHIP_ERROR_BUFFER_TOO_SMALL behavior needed by |
| // PersistentStorageDelegate. |
| uint8_t placeHolderCertBuffer[kMaxCHIPCertLength]; |
| |
| uint16_t keySize = sizeof(placeHolderCertBuffer); |
| CHIP_ERROR err = storage->SyncGetKeyValue(storageKey.KeyName(), &placeHolderCertBuffer[0], keySize); |
| |
| return (err == CHIP_NO_ERROR); |
| } |
| |
| CHIP_ERROR LoadCertFromStorage(PersistentStorageDelegate * storage, FabricIndex fabricIndex, CertChainElement element, |
| MutableByteSpan & outCert) |
| { |
| StorageKeyName storageKey = GetStorageKeyForCert(fabricIndex, element); |
| if (!storageKey) |
| { |
| return CHIP_ERROR_INTERNAL; |
| } |
| |
| uint16_t keySize = static_cast<uint16_t>(outCert.size()); |
| CHIP_ERROR err = storage->SyncGetKeyValue(storageKey.KeyName(), 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) |
| { |
| StorageKeyName storageKey = GetStorageKeyForCert(fabricIndex, element); |
| if (!storageKey) |
| { |
| return CHIP_ERROR_INTERNAL; |
| } |
| |
| // 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.KeyName()); |
| if ((err == CHIP_NO_ERROR) || (err == CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND)) |
| { |
| return CHIP_NO_ERROR; |
| } |
| return err; |
| } |
| |
| return storage->SyncSetKeyValue(storageKey.KeyName(), cert.data(), static_cast<uint16_t>(cert.size())); |
| } |
| |
| CHIP_ERROR DeleteCertFromStorage(PersistentStorageDelegate * storage, FabricIndex fabricIndex, CertChainElement element) |
| { |
| StorageKeyName storageKey = GetStorageKeyForCert(fabricIndex, element); |
| if (!storageKey) |
| { |
| return CHIP_ERROR_INTERNAL; |
| } |
| return storage->SyncDeleteKeyValue(storageKey.KeyName()); |
| } |
| |
| } // 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::ScopedMemoryBufferWithSize<uint8_t> rcacBuf; |
| ReturnErrorCodeIf(!rcacBuf.Alloc(rcac.size()), CHIP_ERROR_NO_MEMORY); |
| memcpy(rcacBuf.Get(), rcac.data(), rcac.size()); |
| |
| mPendingRcac = std::move(rcacBuf); |
| |
| 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::ScopedMemoryBufferWithSize<uint8_t> nocBuf; |
| ReturnErrorCodeIf(!nocBuf.Alloc(noc.size()), CHIP_ERROR_NO_MEMORY); |
| memcpy(nocBuf.Get(), noc.data(), noc.size()); |
| |
| Platform::ScopedMemoryBufferWithSize<uint8_t> 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); |
| mPendingIcac = std::move(icacBuf); |
| |
| 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.Has(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::ScopedMemoryBufferWithSize<uint8_t> nocBuf; |
| ReturnErrorCodeIf(!nocBuf.Alloc(noc.size()), CHIP_ERROR_NO_MEMORY); |
| memcpy(nocBuf.Get(), noc.data(), noc.size()); |
| |
| Platform::ScopedMemoryBufferWithSize<uint8_t> 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); |
| mPendingIcac = std::move(icacBuf); |
| |
| // 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(), mPendingNoc.AllocatedSize() }; |
| CHIP_ERROR nocErr = SaveCertToStorage(mStorage, mPendingFabricIndex, CertChainElement::kNoc, pendingNocSpan); |
| |
| // ICAC storage handles deleting on empty/missing |
| ByteSpan pendingIcacSpan{ mPendingIcac.Get(), mPendingIcac.AllocatedSize() }; |
| CHIP_ERROR icacErr = SaveCertToStorage(mStorage, mPendingFabricIndex, CertChainElement::kIcac, pendingIcacSpan); |
| |
| CHIP_ERROR rcacErr = CHIP_NO_ERROR; |
| if (HasPendingRootCert()) |
| { |
| ByteSpan pendingRcacSpan{ mPendingRcac.Get(), mPendingRcac.AllocatedSize() }; |
| 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 (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(), mPendingRcac.AllocatedSize() }; |
| return CopySpanToMutableSpan(rcacSpan, outCertificate); |
| } |
| break; |
| case CertChainElement::kIcac: |
| if (mPendingIcac.Get() != nullptr) |
| { |
| ByteSpan icacSpan{ mPendingIcac.Get(), mPendingIcac.AllocatedSize() }; |
| return CopySpanToMutableSpan(icacSpan, outCertificate); |
| } |
| break; |
| case CertChainElement::kNoc: |
| if (mPendingNoc.Get() != nullptr) |
| { |
| ByteSpan nocSpan{ mPendingNoc.Get(), mPendingNoc.AllocatedSize() }; |
| 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 |