summaryrefslogtreecommitdiffstats
path: root/src/VBox/Runtime/common/crypto/pemfile.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-06 03:01:46 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-06 03:01:46 +0000
commitf8fe689a81f906d1b91bb3220acde2a4ecb14c5b (patch)
tree26484e9d7e2c67806c2d1760196ff01aaa858e8c /src/VBox/Runtime/common/crypto/pemfile.cpp
parentInitial commit. (diff)
downloadvirtualbox-upstream.tar.xz
virtualbox-upstream.zip
Adding upstream version 6.0.4-dfsg.upstream/6.0.4-dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--src/VBox/Runtime/common/crypto/pemfile.cpp652
1 files changed, 652 insertions, 0 deletions
diff --git a/src/VBox/Runtime/common/crypto/pemfile.cpp b/src/VBox/Runtime/common/crypto/pemfile.cpp
new file mode 100644
index 00000000..b23c3b44
--- /dev/null
+++ b/src/VBox/Runtime/common/crypto/pemfile.cpp
@@ -0,0 +1,652 @@
+/* $Id: pemfile.cpp $ */
+/** @file
+ * IPRT - Crypto - PEM file reader / writer.
+ *
+ * See RFC-1341 for the original ideas for the format, but keep in mind
+ * that the format was hijacked and put to different uses. We're aiming at
+ * dealing with the different uses rather than anything email related here.
+ */
+
+/*
+ * Copyright (C) 2006-2019 Oracle Corporation
+ *
+ * This file is part of VirtualBox Open Source Edition (OSE), as
+ * available from http://www.virtualbox.org. This file is free software;
+ * you can redistribute it and/or modify it under the terms of the GNU
+ * General Public License (GPL) as published by the Free Software
+ * Foundation, in version 2 as it comes in the "COPYING" file of the
+ * VirtualBox OSE distribution. VirtualBox OSE is distributed in the
+ * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
+ *
+ * The contents of this file may alternatively be used under the terms
+ * of the Common Development and Distribution License Version 1.0
+ * (CDDL) only, as it comes in the "COPYING.CDDL" file of the
+ * VirtualBox OSE distribution, in which case the provisions of the
+ * CDDL are applicable instead of those of the GPL.
+ *
+ * You may elect to license modified versions of this file under the
+ * terms and conditions of either the GPL or the CDDL or both.
+ */
+
+
+/*********************************************************************************************************************************
+* Header Files *
+*********************************************************************************************************************************/
+#include "internal/iprt.h"
+#include <iprt/crypto/pem.h>
+
+#include <iprt/asm.h>
+#include <iprt/base64.h>
+#include <iprt/ctype.h>
+#include <iprt/err.h>
+#include <iprt/mem.h>
+#include <iprt/memsafer.h>
+#include <iprt/file.h>
+#include <iprt/string.h>
+
+
+
+/**
+ * Looks for a PEM-like marker.
+ *
+ * @returns true if found, false if not.
+ * @param pbContent Start of the content to search thru.
+ * @param cbContent The size of the content to search.
+ * @param offStart The offset into pbContent to start searching.
+ * @param pszLeadWord The lead word (BEGIN/END).
+ * @param cchLeadWord The length of the lead word.
+ * @param paMarkers Pointer to an array of markers.
+ * @param cMarkers Number of markers in the array.
+ * @param ppMatch Where to return the pointer to the matching
+ * marker. Optional.
+ * @param poffBegin Where to return the start offset of the marker.
+ * Optional.
+ * @param poffEnd Where to return the end offset of the marker
+ * (trailing whitespace and newlines will be
+ * skipped). Optional.
+ */
+static bool rtCrPemFindMarker(uint8_t const *pbContent, size_t cbContent, size_t offStart,
+ const char *pszLeadWord, size_t cchLeadWord, PCRTCRPEMMARKER paMarkers, size_t cMarkers,
+ PCRTCRPEMMARKER *ppMatch, size_t *poffBegin, size_t *poffEnd)
+{
+ /* Remember the start of the content for the purpose of calculating offsets. */
+ uint8_t const * const pbStart = pbContent;
+
+ /* Skip adhead by offStart */
+ if (offStart >= cbContent)
+ return false;
+ pbContent += offStart;
+ cbContent -= offStart;
+
+ /*
+ * Search the content.
+ */
+ while (cbContent > 6)
+ {
+ /*
+ * Look for dashes.
+ */
+ uint8_t const *pbStartSearch = pbContent;
+ pbContent = (uint8_t const *)memchr(pbContent, '-', cbContent);
+ if (!pbContent)
+ break;
+
+ cbContent -= pbContent - pbStartSearch;
+ if (cbContent < 6)
+ break;
+
+ /*
+ * There must be at least three to interest us.
+ */
+ if ( pbContent[1] == '-'
+ && pbContent[2] == '-')
+ {
+ unsigned cDashes = 3;
+ while (cDashes < cbContent && pbContent[cDashes] == '-')
+ cDashes++;
+
+ if (poffBegin)
+ *poffBegin = pbContent - pbStart;
+ cbContent -= cDashes;
+ pbContent += cDashes;
+
+ /*
+ * Match lead word.
+ */
+ if ( cbContent > cchLeadWord
+ && memcmp(pbContent, pszLeadWord, cchLeadWord) == 0
+ && RT_C_IS_BLANK(pbContent[cchLeadWord]) )
+ {
+ pbContent += cchLeadWord;
+ cbContent -= cchLeadWord;
+ while (cbContent > 0 && RT_C_IS_BLANK(*pbContent))
+ {
+ pbContent++;
+ cbContent--;
+ }
+
+ /*
+ * Match one of the specified markers.
+ */
+ uint8_t const *pbSavedContent = pbContent;
+ size_t const cbSavedContent = cbContent;
+ for (uint32_t iMarker = 0; iMarker < cMarkers; iMarker++)
+ {
+ pbContent = pbSavedContent;
+ cbContent = cbSavedContent;
+
+ uint32_t cWords = paMarkers[iMarker].cWords;
+ PCRTCRPEMMARKERWORD pWord = paMarkers[iMarker].paWords;
+ while (cWords > 0)
+ {
+ uint32_t const cchWord = pWord->cchWord;
+ if (cbContent <= cchWord)
+ break;
+ if (memcmp(pbContent, pWord->pszWord, cchWord))
+ break;
+ pbContent += cchWord;
+ cbContent -= cchWord;
+
+ if (!cbContent)
+ break;
+ if (RT_C_IS_BLANK(*pbContent))
+ do
+ {
+ pbContent++;
+ cbContent--;
+ } while (cbContent > 0 && RT_C_IS_BLANK(*pbContent));
+ else if (cWords > 1 || pbContent[0] != '-')
+ break;
+
+ cWords--;
+ if (cWords == 0)
+ {
+ /*
+ * If there are three or more dashes following now, we've got a hit.
+ */
+ if ( cbContent > 3
+ && pbContent[0] == '-'
+ && pbContent[1] == '-'
+ && pbContent[2] == '-')
+ {
+ cDashes = 3;
+ while (cDashes < cbContent && pbContent[cDashes] == '-')
+ cDashes++;
+ cbContent -= cDashes;
+ pbContent += cDashes;
+
+ /*
+ * Skip spaces and newline.
+ */
+ while (cbContent > 0 && RT_C_IS_SPACE(*pbContent))
+ pbContent++, cbContent--;
+ if (poffEnd)
+ *poffEnd = pbContent - pbStart;
+ if (ppMatch)
+ *ppMatch = &paMarkers[iMarker];
+ return true;
+ }
+ break;
+ }
+ pWord++;
+ } /* for each word in marker. */
+ } /* for each marker. */
+ }
+ }
+ else
+ {
+ pbContent++;
+ cbContent--;
+ }
+ }
+
+ return false;
+}
+
+
+static bool rtCrPemFindMarkerSection(uint8_t const *pbContent, size_t cbContent, size_t offStart,
+ PCRTCRPEMMARKER paMarkers, size_t cMarkers,
+ PCRTCRPEMMARKER *ppMatch, size_t *poffBegin, size_t *poffEnd, size_t *poffResume)
+{
+ /** @todo Detect BEGIN / END mismatch. */
+ PCRTCRPEMMARKER pMatch;
+ if (rtCrPemFindMarker(pbContent, cbContent, offStart, "BEGIN", 5, paMarkers, cMarkers,
+ &pMatch, NULL /*poffStart*/, poffBegin))
+ {
+ if (rtCrPemFindMarker(pbContent, cbContent, *poffBegin, "END", 3, pMatch, 1,
+ NULL /*ppMatch*/, poffEnd, poffResume))
+ {
+ *ppMatch = pMatch;
+ return true;
+ }
+ }
+ *ppMatch = NULL;
+ return false;
+}
+
+
+/**
+ * Parses any fields the message may contain.
+ *
+ * @retval VINF_SUCCESS
+ * @retval VERR_NO_MEMORY
+ * @retval VERR_CR_MALFORMED_PEM_HEADER
+ *
+ * @param pSection The current section, where we will attach a list of
+ * fields to the pFieldHead member.
+ * @param pbContent The content of the PEM message being parsed.
+ * @param cbContent The length of the PEM message.
+ * @param pcbFields Where to return the length of the header fields we found.
+ */
+static int rtCrPemProcessFields(PRTCRPEMSECTION pSection, uint8_t const *pbContent, size_t cbContent, size_t *pcbFields)
+{
+ uint8_t const * const pbContentStart = pbContent;
+
+ /*
+ * Work the encapulated header protion field by field.
+ *
+ * This is optional, so currently we don't throw errors here but leave that
+ * to when we work the text portion with the base64 decoder. Also, as a reader
+ * we don't go all pedanic on confirming to specification (RFC-1421), especially
+ * given that it's used for crypto certificates, keys and the like not email. :-)
+ */
+ PCRTCRPEMFIELD *ppNext = &pSection->pFieldHead;
+ while (cbContent > 0)
+ {
+ /* Just look for a colon first. */
+ const uint8_t *pbColon = (const uint8_t *)memchr(pbContent, ':', cbContent);
+ if (!pbColon)
+ break;
+ size_t offColon = pbColon - pbContent;
+
+ /* Check that the colon is within the first line. */
+ if (!memchr(pbContent, '\n', cbContent - offColon))
+ return VERR_CR_MALFORMED_PEM_HEADER;
+
+ /* Skip leading spaces (there shouldn't be any, but just in case). */
+ while (RT_C_IS_BLANK(*pbContent) && /*paranoia:*/ offColon > 0)
+ {
+ offColon--;
+ cbContent--;
+ pbContent++;
+ }
+
+ /* There shouldn't be any spaces before the colon, but just in case */
+ size_t cchName = offColon;
+ while (cchName > 0 && RT_C_IS_BLANK(pbContent[cchName - 1]))
+ cchName--;
+
+ /* Skip leading value spaces (there typically is at least one). */
+ size_t offValue = offColon + 1;
+ while (offValue < cbContent && RT_C_IS_BLANK(pbContent[offValue]))
+ offValue++;
+
+ /* Find the newline the field value ends with and where the next iteration should start later on. */
+ size_t cbLeft;
+ uint8_t const *pbNext = (uint8_t const *)memchr(&pbContent[offValue], '\n', cbContent - offValue);
+ while ( pbNext
+ && (cbLeft = pbNext - pbContent) < cbContent
+ && RT_C_IS_BLANK(pbNext[1]) /* next line must start with a space or tab */)
+ pbNext = (uint8_t const *)memchr(&pbNext[1], '\n', cbLeft - 1);
+
+ size_t cchValue;
+ if (pbNext)
+ {
+ cchValue = pbNext - &pbContent[offValue];
+ if (cchValue > 0 && pbNext[-1] == '\r')
+ cchValue--;
+ pbNext++;
+ }
+ else
+ {
+ cchValue = cbContent - offValue;
+ pbNext = &pbContent[cbContent];
+ }
+
+ /* Strip trailing spaces. */
+ while (cchValue > 0 && RT_C_IS_BLANK(pbContent[offValue + cchValue - 1]))
+ cchValue--;
+
+ /*
+ * Allocate a field instance.
+ *
+ * Note! We don't consider field data sensitive at the moment. This
+ * mainly because the fields are chiefly used to indicate the
+ * encryption parameters to the body.
+ */
+ PRTCRPEMFIELD pNewField = (PRTCRPEMFIELD)RTMemAllocZVar(sizeof(*pNewField) + cchName + 1 + cchValue + 1);
+ if (!pNewField)
+ return VERR_NO_MEMORY;
+ pNewField->cchName = cchName;
+ pNewField->cchValue = cchValue;
+ memcpy(pNewField->szName, pbContent, cchName);
+ pNewField->szName[cchName] = '\0';
+ char *pszDst = (char *)memcpy(&pNewField->szName[cchName + 1], &pbContent[offValue], cchValue);
+ pNewField->pszValue = pszDst;
+ pszDst[cchValue] = '\0';
+ pNewField->pNext = NULL;
+
+ *ppNext = pNewField;
+ ppNext = &pNewField->pNext;
+
+ /*
+ * Advance past the field.
+ */
+ cbContent -= pbNext - pbContent;
+ pbContent = pbNext;
+ }
+
+ /*
+ * Skip blank line(s) before the body.
+ */
+ while (cbContent >= 1)
+ {
+ size_t cbSkip;
+ if (pbContent[0] == '\n')
+ cbSkip = 1;
+ else if ( pbContent[0] == '\r'
+ && cbContent >= 2
+ && pbContent[1] == '\n')
+ cbSkip = 2;
+ else
+ break;
+ pbContent += cbSkip;
+ cbContent -= cbSkip;
+ }
+
+ *pcbFields = pbContent - pbContentStart;
+ return VINF_SUCCESS;
+}
+
+
+/**
+ * Does the decoding of a PEM-like data blob after it has been located.
+ *
+ * @returns IPRT status ocde
+ * @param pbContent The start of the PEM-like content (text).
+ * @param cbContent The max size of the PEM-like content.
+ * @param fSensitive Set if the safer allocator should be used.
+ * @param ppvDecoded Where to return a heap block containing the
+ * decoded content.
+ * @param pcbDecoded Where to return the size of the decoded content.
+ */
+static int rtCrPemDecodeBase64(uint8_t const *pbContent, size_t cbContent, bool fSensitive,
+ void **ppvDecoded, size_t *pcbDecoded)
+{
+ ssize_t cbDecoded = RTBase64DecodedSizeEx((const char *)pbContent, cbContent, NULL);
+ if (cbDecoded < 0)
+ return VERR_INVALID_BASE64_ENCODING;
+
+ *pcbDecoded = cbDecoded;
+ void *pvDecoded = !fSensitive ? RTMemAlloc(cbDecoded) : RTMemSaferAllocZ(cbDecoded);
+ if (!pvDecoded)
+ return VERR_NO_MEMORY;
+
+ size_t cbActual;
+ int rc = RTBase64DecodeEx((const char *)pbContent, cbContent, pvDecoded, cbDecoded, &cbActual, NULL);
+ if (RT_SUCCESS(rc))
+ {
+ if (cbActual == (size_t)cbDecoded)
+ {
+ *ppvDecoded = pvDecoded;
+ return VINF_SUCCESS;
+ }
+
+ rc = VERR_INTERNAL_ERROR_3;
+ }
+ if (!fSensitive)
+ RTMemFree(pvDecoded);
+ else
+ RTMemSaferFree(pvDecoded, cbDecoded);
+ return rc;
+}
+
+
+/**
+ * Checks if the content of a file looks to be binary or not.
+ *
+ * @returns true if likely to be binary, false if not binary.
+ * @param pbFile The file bytes to scan.
+ * @param cbFile The number of bytes.
+ * @param fFlags RTCRPEMREADFILE_F_XXX
+ */
+static bool rtCrPemIsBinaryBlob(uint8_t const *pbFile, size_t cbFile, uint32_t fFlags)
+{
+ if (fFlags & RTCRPEMREADFILE_F_ONLY_PEM)
+ return false;
+
+ /*
+ * Well formed PEM files should probably only contain 7-bit ASCII and
+ * restrict thenselfs to the following control characters:
+ * tab, newline, return, form feed
+ *
+ * However, if we want to read PEM files which contains human readable
+ * certificate details before or after each base-64 section, we can't stick
+ * to 7-bit ASCII. We could say it must be UTF-8, but that's probably to
+ * limited as well. So, we'll settle for detecting binary files by control
+ * characters alone (safe enough for DER encoded stuff, I think).
+ */
+ while (cbFile-- > 0)
+ {
+ uint8_t const b = *pbFile++;
+ if (b < 32 && b != '\t' && b != '\n' && b != '\r' && b != '\f')
+ {
+ /* Ignore EOT (4), SUB (26) and NUL (0) at the end of the file. */
+ if ( (b == 4 || b == 26)
+ && ( cbFile == 0
+ || ( cbFile == 1
+ && *pbFile == '\0')))
+ return false;
+
+ if (b == 0 && cbFile == 0)
+ return false;
+
+ return true;
+ }
+ }
+ return false;
+}
+
+
+RTDECL(int) RTCrPemFreeSections(PCRTCRPEMSECTION pSectionHead)
+{
+ while (pSectionHead != NULL)
+ {
+ PRTCRPEMSECTION pFree = (PRTCRPEMSECTION)pSectionHead;
+ pSectionHead = pSectionHead->pNext;
+ ASMCompilerBarrier(); /* paranoia */
+
+ if (pFree->pbData)
+ {
+ if (!pFree->fSensitive)
+ RTMemFree(pFree->pbData);
+ else
+ RTMemSaferFree(pFree->pbData, pFree->cbData);
+ pFree->pbData = NULL;
+ pFree->cbData = 0;
+ }
+
+ PRTCRPEMFIELD pField = (PRTCRPEMFIELD)pFree->pFieldHead;
+ if (pField)
+ {
+ pFree->pFieldHead = NULL;
+ do
+ {
+ PRTCRPEMFIELD pFreeField = pField;
+ pField = (PRTCRPEMFIELD)pField->pNext;
+ ASMCompilerBarrier(); /* paranoia */
+
+ pFreeField->pszValue = NULL;
+ RTMemFree(pFreeField);
+ } while (pField);
+ }
+
+ RTMemFree(pFree);
+ }
+ return VINF_SUCCESS;
+}
+
+
+RTDECL(int) RTCrPemParseContent(void const *pvContent, size_t cbContent, uint32_t fFlags,
+ PCRTCRPEMMARKER paMarkers, size_t cMarkers,
+ PCRTCRPEMSECTION *ppSectionHead, PRTERRINFO pErrInfo)
+{
+ RT_NOREF_PV(pErrInfo);
+
+ /*
+ * Input validation.
+ */
+ AssertPtr(ppSectionHead);
+ *ppSectionHead = NULL;
+ AssertReturn(cbContent, VINF_EOF);
+ AssertPtr(pvContent);
+ AssertPtr(paMarkers);
+ AssertReturn(!(fFlags & ~RTCRPEMREADFILE_F_VALID_MASK), VERR_INVALID_FLAGS);
+
+ /*
+ * Pre-allocate a section.
+ */
+ int rc = VINF_SUCCESS;
+ PRTCRPEMSECTION pSection = (PRTCRPEMSECTION)RTMemAllocZ(sizeof(*pSection));
+ if (pSection)
+ {
+ bool const fSensitive = RT_BOOL(fFlags & RTCRPEMREADFILE_F_SENSITIVE);
+
+ /*
+ * Try locate the first section.
+ */
+ uint8_t const *pbContent = (uint8_t const *)pvContent;
+ size_t offBegin, offEnd, offResume;
+ PCRTCRPEMMARKER pMatch;
+ if ( !rtCrPemIsBinaryBlob(pbContent, cbContent, fFlags)
+ && rtCrPemFindMarkerSection(pbContent, cbContent, 0 /*offStart*/, paMarkers, cMarkers,
+ &pMatch, &offBegin, &offEnd, &offResume) )
+ {
+ PCRTCRPEMSECTION *ppNext = ppSectionHead;
+ for (;;)
+ {
+ //pSection->pNext = NULL;
+ pSection->pMarker = pMatch;
+ //pSection->pbData = NULL;
+ //pSection->cbData = 0;
+ //pSection->pFieldHead = NULL;
+ pSection->fSensitive = fSensitive;
+
+ *ppNext = pSection;
+ ppNext = &pSection->pNext;
+
+ /*
+ * Decode the section.
+ */
+ size_t cbFields = 0;
+ int rc2 = rtCrPemProcessFields(pSection, pbContent + offBegin, offEnd - offBegin, &cbFields);
+ offBegin += cbFields;
+ if (RT_SUCCESS(rc2))
+ rc2 = rtCrPemDecodeBase64(pbContent + offBegin, offEnd - offBegin, fSensitive,
+ (void **)&pSection->pbData, &pSection->cbData);
+ if (RT_FAILURE(rc2))
+ {
+ pSection->pbData = NULL;
+ pSection->cbData = 0;
+ if ( rc2 == VERR_INVALID_BASE64_ENCODING
+ && (fFlags & RTCRPEMREADFILE_F_CONTINUE_ON_ENCODING_ERROR))
+ rc = -rc2;
+ else
+ {
+ rc = rc2;
+ break;
+ }
+ }
+
+ /*
+ * More sections?
+ */
+ if ( offResume + 12 >= cbContent
+ || offResume >= cbContent
+ || !rtCrPemFindMarkerSection(pbContent, cbContent, offResume, paMarkers, cMarkers,
+ &pMatch, &offBegin, &offEnd, &offResume) )
+ break; /* No. */
+
+ /* Ok, allocate a new record for it. */
+ pSection = (PRTCRPEMSECTION)RTMemAllocZ(sizeof(*pSection));
+ if (RT_UNLIKELY(!pSection))
+ {
+ rc = VERR_NO_MEMORY;
+ break;
+ }
+ }
+ if (RT_SUCCESS(rc))
+ return rc;
+
+ RTCrPemFreeSections(*ppSectionHead);
+ }
+ else
+ {
+ if (!(fFlags & RTCRPEMREADFILE_F_ONLY_PEM))
+ {
+ /*
+ * No PEM section found. Return the whole file as one binary section.
+ */
+ //pSection->pNext = NULL;
+ //pSection->pMarker = NULL;
+ //pSection->pFieldHead = NULL;
+ pSection->cbData = cbContent;
+ pSection->fSensitive = fSensitive;
+ if (!fSensitive)
+ pSection->pbData = (uint8_t *)RTMemDup(pbContent, cbContent);
+ else
+ {
+ pSection->pbData = (uint8_t *)RTMemSaferAllocZ(cbContent);
+ if (pSection->pbData)
+ memcpy(pSection->pbData, pbContent, cbContent);
+ }
+ if (pSection->pbData)
+ {
+ *ppSectionHead = pSection;
+ return VINF_SUCCESS;
+ }
+
+ rc = VERR_NO_MEMORY;
+ }
+ else
+ rc = VWRN_NOT_FOUND;
+ RTMemFree(pSection);
+ }
+ }
+ else
+ rc = VERR_NO_MEMORY;
+ *ppSectionHead = NULL;
+ return rc;
+}
+
+
+RTDECL(int) RTCrPemReadFile(const char *pszFilename, uint32_t fFlags, PCRTCRPEMMARKER paMarkers, size_t cMarkers,
+ PCRTCRPEMSECTION *ppSectionHead, PRTERRINFO pErrInfo)
+{
+ *ppSectionHead = NULL;
+ AssertReturn(!(fFlags & ~RTCRPEMREADFILE_F_VALID_MASK), VERR_INVALID_FLAGS);
+
+ size_t cbContent;
+ void *pvContent;
+ int rc = RTFileReadAllEx(pszFilename, 0, 64U*_1M, RTFILE_RDALL_O_DENY_WRITE, &pvContent, &cbContent);
+ if (RT_SUCCESS(rc))
+ {
+ rc = RTCrPemParseContent(pvContent, cbContent, fFlags, paMarkers, cMarkers, ppSectionHead, pErrInfo);
+ if (fFlags & RTCRPEMREADFILE_F_SENSITIVE)
+ RTMemWipeThoroughly(pvContent, cbContent, 3);
+ RTFileReadAllFree(pvContent, cbContent);
+ }
+ else
+ rc = RTErrInfoSetF(pErrInfo, rc, "RTFileReadAllEx failed with %Rrc on '%s'", rc, pszFilename);
+ return rc;
+}
+
+
+RTDECL(const char *) RTCrPemFindFirstSectionInContent(void const *pvContent, size_t cbContent,
+ PCRTCRPEMMARKER paMarkers, size_t cMarkers)
+{
+ size_t offBegin;
+ if (rtCrPemFindMarker((uint8_t *)pvContent, cbContent, 0, "BEGIN", 5, paMarkers, cMarkers, NULL, &offBegin, NULL))
+ return (const char *)pvContent + offBegin;
+ return NULL;
+}