VirtualBox

source: vbox/trunk/src/VBox/Main/src-server/CertificateImpl.cpp@ 64975

Last change on this file since 64975 was 64885, checked in by vboxsync, 8 years ago

IPRT/ASN.1: Refactored array handling (SET OF, SEQUENCE OF) to use a pointer array instead of an object instance array. The old approach would move objects around in memory after they'd be initialized/decoded, making certain core optimziations involving pointers to object members impossible, as well as causing potentially causing trouble when modifying structures that takes down pointers after decoding. Fixed validation bug in rtCrX509Name_CheckSanityExtra where it didn't check that the RDNs had subitems but instead checked the parent twice (slight risk).

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 17.7 KB
Line 
1/* $Id: CertificateImpl.cpp 64885 2016-12-15 15:31:24Z vboxsync $ */
2/** @file
3 * ICertificate COM class implementations.
4 */
5
6/*
7 * Copyright (C) 2008-2016 Oracle Corporation
8 *
9 * This file is part of VirtualBox Open Source Edition (OSE), as
10 * available from http://www.virtualbox.org. This file is free software;
11 * you can redistribute it and/or modify it under the terms of the GNU
12 * General Public License (GPL) as published by the Free Software
13 * Foundation, in version 2 as it comes in the "COPYING" file of the
14 * VirtualBox OSE distribution. VirtualBox OSE is distributed in the
15 * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
16 */
17
18#include <iprt/path.h>
19#include <iprt/cpp/utils.h>
20#include <VBox/com/array.h>
21#include <iprt/crypto/x509.h>
22
23#include "ProgressImpl.h"
24#include "CertificateImpl.h"
25#include "AutoCaller.h"
26#include "Global.h"
27#include "Logging.h"
28
29using namespace std;
30
31
32/**
33 * Private instance data for the #Certificate class.
34 * @see Certificate::m
35 */
36struct Certificate::Data
37{
38 Data()
39 : fTrusted(false)
40 , fExpired(false)
41 , fValidX509(false)
42 {
43 RT_ZERO(X509);
44 }
45
46 ~Data()
47 {
48 if (fValidX509)
49 {
50 RTCrX509Certificate_Delete(&X509);
51 RT_ZERO(X509);
52 fValidX509 = false;
53 }
54 }
55
56 /** Whether the certificate is trusted. */
57 bool fTrusted;
58 /** Whether the certificate is trusted. */
59 bool fExpired;
60 /** Valid data in mX509. */
61 bool fValidX509;
62 /** Clone of the X.509 certificate. */
63 RTCRX509CERTIFICATE X509;
64
65private:
66 Data(const Certificate::Data &rTodo) { AssertFailed(); NOREF(rTodo); }
67 Data &operator=(const Certificate::Data &rTodo) { AssertFailed(); NOREF(rTodo); return *this; }
68};
69
70
71///////////////////////////////////////////////////////////////////////////////////
72//
73// Certificate constructor / destructor
74//
75// ////////////////////////////////////////////////////////////////////////////////
76
77DEFINE_EMPTY_CTOR_DTOR(Certificate)
78
79HRESULT Certificate::FinalConstruct()
80{
81 return BaseFinalConstruct();
82}
83
84void Certificate::FinalRelease()
85{
86 uninit();
87 BaseFinalRelease();
88}
89
90/**
91 * Initializes a certificate instance.
92 *
93 * @returns COM status code.
94 * @param a_pCert The certificate.
95 * @param a_fTrusted Whether the caller trusts the certificate or not.
96 * @param a_fExpired Whether the caller consideres the certificate to be
97 * expired.
98 */
99HRESULT Certificate::initCertificate(PCRTCRX509CERTIFICATE a_pCert, bool a_fTrusted, bool a_fExpired)
100{
101 HRESULT rc = S_OK;
102 LogFlowThisFuncEnter();
103
104 AutoInitSpan autoInitSpan(this);
105 AssertReturn(autoInitSpan.isOk(), E_FAIL);
106
107 m = new Data();
108
109 int vrc = RTCrX509Certificate_Clone(&m->X509, a_pCert, &g_RTAsn1DefaultAllocator);
110 if (RT_SUCCESS(vrc))
111 {
112 m->fValidX509 = true;
113 m->fTrusted = a_fTrusted;
114 m->fExpired = a_fExpired;
115 autoInitSpan.setSucceeded();
116 }
117 else
118 rc = Global::vboxStatusCodeToCOM(vrc);
119
120 LogFlowThisFunc(("returns rc=%Rhrc\n", rc));
121 return rc;
122}
123
124void Certificate::uninit()
125{
126 /* Enclose the state transition Ready->InUninit->NotReady */
127 AutoUninitSpan autoUninitSpan(this);
128 if (autoUninitSpan.uninitDone())
129 return;
130
131 delete m;
132 m = NULL;
133}
134
135
136/** @name wrapped ICertificate properties
137 * @{
138 */
139
140HRESULT Certificate::getVersionNumber(CertificateVersion_T *aVersionNumber)
141{
142 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
143
144 Assert(m->fValidX509);
145 switch (m->X509.TbsCertificate.T0.Version.uValue.u)
146 {
147 case RTCRX509TBSCERTIFICATE_V1: *aVersionNumber = (CertificateVersion_T)CertificateVersion_V1; break;
148 case RTCRX509TBSCERTIFICATE_V2: *aVersionNumber = (CertificateVersion_T)CertificateVersion_V2; break;
149 case RTCRX509TBSCERTIFICATE_V3: *aVersionNumber = (CertificateVersion_T)CertificateVersion_V3; break;
150 default: AssertFailed(); *aVersionNumber = (CertificateVersion_T)CertificateVersion_Unknown; break;
151 }
152 return S_OK;
153}
154
155HRESULT Certificate::getSerialNumber(com::Utf8Str &aSerialNumber)
156{
157 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
158
159 Assert(m->fValidX509);
160
161 char szTmp[_2K];
162 int vrc = RTAsn1Integer_ToString(&m->X509.TbsCertificate.SerialNumber, szTmp, sizeof(szTmp), 0, NULL);
163 if (RT_SUCCESS(vrc))
164 aSerialNumber = szTmp;
165 else
166 return Global::vboxStatusCodeToCOM(vrc);
167
168 return S_OK;
169}
170
171HRESULT Certificate::getSignatureAlgorithmOID(com::Utf8Str &aSignatureAlgorithmOID)
172{
173 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
174
175 Assert(m->fValidX509);
176 aSignatureAlgorithmOID = m->X509.TbsCertificate.Signature.Algorithm.szObjId;
177
178 return S_OK;
179}
180
181HRESULT Certificate::getSignatureAlgorithmName(com::Utf8Str &aSignatureAlgorithmName)
182{
183 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
184
185 Assert(m->fValidX509);
186 return i_getAlgorithmName(&m->X509.TbsCertificate.Signature, aSignatureAlgorithmName);
187}
188
189HRESULT Certificate::getIssuerName(std::vector<com::Utf8Str> &aIssuerName)
190{
191 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
192
193 Assert(m->fValidX509);
194 return i_getX509Name(&m->X509.TbsCertificate.Issuer, aIssuerName);
195}
196
197HRESULT Certificate::getSubjectName(std::vector<com::Utf8Str> &aSubjectName)
198{
199 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
200
201 Assert(m->fValidX509);
202 return i_getX509Name(&m->X509.TbsCertificate.Subject, aSubjectName);
203}
204
205HRESULT Certificate::getFriendlyName(com::Utf8Str &aFriendlyName)
206{
207 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
208
209 Assert(m->fValidX509);
210
211 PCRTCRX509NAME pName = &m->X509.TbsCertificate.Subject;
212
213 /*
214 * Enumerate the subject name and pick interesting attributes we can use to
215 * form a name more friendly than the RTCrX509Name_FormatAsString output.
216 */
217 const char *pszOrg = NULL;
218 const char *pszOrgUnit = NULL;
219 const char *pszGivenName = NULL;
220 const char *pszSurname = NULL;
221 const char *pszEmail = NULL;
222 for (uint32_t i = 0; i < pName->cItems; i++)
223 {
224 PCRTCRX509RELATIVEDISTINGUISHEDNAME pRdn = pName->papItems[i];
225 for (uint32_t j = 0; j < pRdn->cItems; j++)
226 {
227 PCRTCRX509ATTRIBUTETYPEANDVALUE pComponent = pRdn->papItems[j];
228 AssertContinue(pComponent->Value.enmType == RTASN1TYPE_STRING);
229
230 /* Select interesting components based on the short RDN prefix
231 string (easier to read and write than OIDs, for now). */
232 const char *pszPrefix = RTCrX509Name_GetShortRdn(&pComponent->Type);
233 if (pszPrefix)
234 {
235 const char *pszUtf8;
236 int vrc = RTAsn1String_QueryUtf8(&pComponent->Value.u.String, &pszUtf8, NULL);
237 if (RT_SUCCESS(vrc) && *pszUtf8)
238 {
239 if (!strcmp(pszPrefix, "Email"))
240 pszEmail = pszUtf8;
241 else if (!strcmp(pszPrefix, "O"))
242 pszOrg = pszUtf8;
243 else if (!strcmp(pszPrefix, "OU"))
244 pszOrgUnit = pszUtf8;
245 else if (!strcmp(pszPrefix, "S"))
246 pszSurname = pszUtf8;
247 else if (!strcmp(pszPrefix, "G"))
248 pszGivenName = pszUtf8;
249 }
250 }
251 }
252 }
253
254 if (pszGivenName && pszSurname)
255 {
256 if (pszEmail)
257 aFriendlyName = Utf8StrFmt("%s, %s <%s>", pszSurname, pszGivenName, pszEmail);
258 else if (pszOrg)
259 aFriendlyName = Utf8StrFmt("%s, %s (%s)", pszSurname, pszGivenName, pszOrg);
260 else if (pszOrgUnit)
261 aFriendlyName = Utf8StrFmt("%s, %s (%s)", pszSurname, pszGivenName, pszOrgUnit);
262 else
263 aFriendlyName = Utf8StrFmt("%s, %s", pszSurname, pszGivenName);
264 }
265 else if (pszOrg && pszOrgUnit)
266 aFriendlyName = Utf8StrFmt("%s, %s", pszOrg, pszOrgUnit);
267 else if (pszOrg)
268 aFriendlyName = Utf8StrFmt("%s", pszOrg);
269 else if (pszOrgUnit)
270 aFriendlyName = Utf8StrFmt("%s", pszOrgUnit);
271 else
272 {
273 /*
274 * Fall back on unfriendly but accurate.
275 */
276 char szTmp[_8K];
277 RT_ZERO(szTmp);
278 RTCrX509Name_FormatAsString(pName, szTmp, sizeof(szTmp) - 1, NULL);
279 aFriendlyName = szTmp;
280 }
281
282 return S_OK;
283}
284
285HRESULT Certificate::getValidityPeriodNotBefore(com::Utf8Str &aValidityPeriodNotBefore)
286{
287 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
288
289 Assert(m->fValidX509);
290 return i_getTime(&m->X509.TbsCertificate.Validity.NotBefore, aValidityPeriodNotBefore);
291}
292
293HRESULT Certificate::getValidityPeriodNotAfter(com::Utf8Str &aValidityPeriodNotAfter)
294{
295 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
296
297 Assert(m->fValidX509);
298 return i_getTime(&m->X509.TbsCertificate.Validity.NotAfter, aValidityPeriodNotAfter);
299}
300
301HRESULT Certificate::getPublicKeyAlgorithmOID(com::Utf8Str &aPublicKeyAlgorithmOID)
302{
303 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
304
305 Assert(m->fValidX509);
306 aPublicKeyAlgorithmOID = m->X509.TbsCertificate.SubjectPublicKeyInfo.Algorithm.Algorithm.szObjId;
307 return S_OK;
308}
309
310HRESULT Certificate::getPublicKeyAlgorithm(com::Utf8Str &aPublicKeyAlgorithm)
311{
312 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
313
314 Assert(m->fValidX509);
315 return i_getAlgorithmName(&m->X509.TbsCertificate.SubjectPublicKeyInfo.Algorithm, aPublicKeyAlgorithm);
316}
317
318HRESULT Certificate::getSubjectPublicKey(std::vector<BYTE> &aSubjectPublicKey)
319{
320
321 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS); /* Getting encoded ASN.1 bytes may make changes to X509. */
322 return i_getEncodedBytes(&m->X509.TbsCertificate.SubjectPublicKeyInfo.SubjectPublicKey.Asn1Core, aSubjectPublicKey);
323}
324
325HRESULT Certificate::getIssuerUniqueIdentifier(com::Utf8Str &aIssuerUniqueIdentifier)
326{
327 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
328
329 return i_getUniqueIdentifier(&m->X509.TbsCertificate.T1.IssuerUniqueId, aIssuerUniqueIdentifier);
330}
331
332HRESULT Certificate::getSubjectUniqueIdentifier(com::Utf8Str &aSubjectUniqueIdentifier)
333{
334 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
335
336 return i_getUniqueIdentifier(&m->X509.TbsCertificate.T2.SubjectUniqueId, aSubjectUniqueIdentifier);
337}
338
339HRESULT Certificate::getCertificateAuthority(BOOL *aCertificateAuthority)
340{
341 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
342
343 *aCertificateAuthority = m->X509.TbsCertificate.T3.pBasicConstraints
344 && m->X509.TbsCertificate.T3.pBasicConstraints->CA.fValue;
345
346 return S_OK;
347}
348
349HRESULT Certificate::getKeyUsage(ULONG *aKeyUsage)
350{
351 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
352
353 *aKeyUsage = m->X509.TbsCertificate.T3.fKeyUsage;
354 return S_OK;
355}
356
357HRESULT Certificate::getExtendedKeyUsage(std::vector<com::Utf8Str> &aExtendedKeyUsage)
358{
359 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
360 NOREF(aExtendedKeyUsage);
361 return E_NOTIMPL;
362}
363
364HRESULT Certificate::getRawCertData(std::vector<BYTE> &aRawCertData)
365{
366 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS); /* Getting encoded ASN.1 bytes may make changes to X509. */
367 return i_getEncodedBytes(&m->X509.SeqCore.Asn1Core, aRawCertData);
368}
369
370HRESULT Certificate::getSelfSigned(BOOL *aSelfSigned)
371{
372 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
373
374 Assert(m->fValidX509);
375 *aSelfSigned = RTCrX509Certificate_IsSelfSigned(&m->X509);
376
377 return S_OK;
378}
379
380HRESULT Certificate::getTrusted(BOOL *aTrusted)
381{
382 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
383
384 Assert(m->fValidX509);
385 *aTrusted = m->fTrusted;
386
387 return S_OK;
388}
389
390HRESULT Certificate::getExpired(BOOL *aExpired)
391{
392 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
393 Assert(m->fValidX509);
394 *aExpired = m->fExpired;
395 return S_OK;
396}
397
398/** @} */
399
400/** @name Wrapped ICertificate methods
401 * @{
402 */
403
404HRESULT Certificate::isCurrentlyExpired(BOOL *aResult)
405{
406 AssertReturnStmt(m->fValidX509, *aResult = TRUE, E_UNEXPECTED);
407 RTTIMESPEC Now;
408 *aResult = RTCrX509Validity_IsValidAtTimeSpec(&m->X509.TbsCertificate.Validity, RTTimeNow(&Now)) ? FALSE : TRUE;
409 return S_OK;
410}
411
412HRESULT Certificate::queryInfo(LONG aWhat, com::Utf8Str &aResult)
413{
414 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
415 /* Insurance. */
416 NOREF(aResult);
417 return setError(E_FAIL, "Unknown item %u", aWhat);
418}
419
420/** @} */
421
422
423/** @name Methods extracting COM data from the certificate object
424 * @{
425 */
426
427/**
428 * Translates an algorithm OID into a human readable string, if possible.
429 *
430 * @returns S_OK.
431 * @param a_pAlgId The algorithm.
432 * @param a_rReturn The return string value.
433 * @throws std::bad_alloc
434 */
435HRESULT Certificate::i_getAlgorithmName(PCRTCRX509ALGORITHMIDENTIFIER a_pAlgId, com::Utf8Str &a_rReturn)
436{
437 const char *pszOid = a_pAlgId->Algorithm.szObjId;
438 const char *pszName;
439 if (!pszOid) pszName = "";
440 else if (strcmp(pszOid, RTCRX509ALGORITHMIDENTIFIERID_RSA)) pszName = "rsaEncryption";
441 else if (strcmp(pszOid, RTCRX509ALGORITHMIDENTIFIERID_MD2_WITH_RSA)) pszName = "md2WithRSAEncryption";
442 else if (strcmp(pszOid, RTCRX509ALGORITHMIDENTIFIERID_MD4_WITH_RSA)) pszName = "md4WithRSAEncryption";
443 else if (strcmp(pszOid, RTCRX509ALGORITHMIDENTIFIERID_MD5_WITH_RSA)) pszName = "md5WithRSAEncryption";
444 else if (strcmp(pszOid, RTCRX509ALGORITHMIDENTIFIERID_SHA1_WITH_RSA)) pszName = "sha1WithRSAEncryption";
445 else if (strcmp(pszOid, RTCRX509ALGORITHMIDENTIFIERID_SHA224_WITH_RSA)) pszName = "sha224WithRSAEncryption";
446 else if (strcmp(pszOid, RTCRX509ALGORITHMIDENTIFIERID_SHA256_WITH_RSA)) pszName = "sha256WithRSAEncryption";
447 else if (strcmp(pszOid, RTCRX509ALGORITHMIDENTIFIERID_SHA384_WITH_RSA)) pszName = "sha384WithRSAEncryption";
448 else if (strcmp(pszOid, RTCRX509ALGORITHMIDENTIFIERID_SHA512_WITH_RSA)) pszName = "sha512WithRSAEncryption";
449 else
450 pszName = pszOid;
451 a_rReturn = pszName;
452 return S_OK;
453}
454
455/**
456 * Formats a X.509 name into a string array.
457 *
458 * The name is prefix with a short hand of the relative distinguished name
459 * type followed by an equal sign.
460 *
461 * @returns S_OK.
462 * @param a_pName The X.509 name.
463 * @param a_rReturn The return string array.
464 * @throws std::bad_alloc
465 */
466HRESULT Certificate::i_getX509Name(PCRTCRX509NAME a_pName, std::vector<com::Utf8Str> &a_rReturn)
467{
468 if (RTCrX509Name_IsPresent(a_pName))
469 {
470 for (uint32_t i = 0; i < a_pName->cItems; i++)
471 {
472 PCRTCRX509RELATIVEDISTINGUISHEDNAME pRdn = a_pName->papItems[i];
473 for (uint32_t j = 0; j < pRdn->cItems; j++)
474 {
475 PCRTCRX509ATTRIBUTETYPEANDVALUE pComponent = pRdn->papItems[j];
476
477 AssertReturn(pComponent->Value.enmType == RTASN1TYPE_STRING,
478 setErrorVrc(VERR_CR_X509_NAME_NOT_STRING, "VERR_CR_X509_NAME_NOT_STRING"));
479
480 /* Get the prefix for this name component. */
481 const char *pszPrefix = RTCrX509Name_GetShortRdn(&pComponent->Type);
482 AssertStmt(pszPrefix, pszPrefix = pComponent->Type.szObjId);
483
484 /* Get the string. */
485 const char *pszUtf8;
486 int vrc = RTAsn1String_QueryUtf8(&pComponent->Value.u.String, &pszUtf8, NULL /*pcch*/);
487 AssertRCReturn(vrc, setErrorVrc(vrc, "RTAsn1String_QueryUtf8(%u/%u,,) -> %Rrc", i, j, vrc));
488
489 a_rReturn.push_back(Utf8StrFmt("%s=%s", pszPrefix, pszUtf8));
490 }
491 }
492 }
493 return S_OK;
494}
495
496/**
497 * Translates an ASN.1 timestamp into an ISO timestamp string.
498 *
499 * @returns S_OK.
500 * @param a_pTime The timestamp
501 * @param a_rReturn The return string value.
502 * @throws std::bad_alloc
503 */
504HRESULT Certificate::i_getTime(PCRTASN1TIME a_pTime, com::Utf8Str &a_rReturn)
505{
506 char szTmp[128];
507 if (RTTimeToString(&a_pTime->Time, szTmp, sizeof(szTmp)))
508 {
509 a_rReturn = szTmp;
510 return S_OK;
511 }
512 AssertFailed();
513 return E_FAIL;
514}
515
516/**
517 * Translates a X.509 unique identifier to a string.
518 *
519 * @returns S_OK.
520 * @param a_pUniqueId The unique identifier.
521 * @param a_rReturn The return string value.
522 * @throws std::bad_alloc
523 */
524HRESULT Certificate::i_getUniqueIdentifier(PCRTCRX509UNIQUEIDENTIFIER a_pUniqueId, com::Utf8Str &a_rReturn)
525{
526 /* The a_pUniqueId may not be present! */
527 if (RTCrX509UniqueIdentifier_IsPresent(a_pUniqueId))
528 {
529 void const *pvData = RTASN1BITSTRING_GET_BIT0_PTR(a_pUniqueId);
530 size_t const cbData = RTASN1BITSTRING_GET_BYTE_SIZE(a_pUniqueId);
531 size_t const cbFormatted = cbData * 3 - 1 + 1;
532 a_rReturn.reserve(cbFormatted); /* throws */
533 int vrc = RTStrPrintHexBytes(a_rReturn.mutableRaw(), cbFormatted, pvData, cbData, RTSTRPRINTHEXBYTES_F_SEP_COLON);
534 a_rReturn.jolt();
535 AssertRCReturn(vrc, Global::vboxStatusCodeToCOM(vrc));
536 }
537 else
538 Assert(a_rReturn.isEmpty());
539 return S_OK;
540}
541
542/**
543 * Translates any ASN.1 object into a (DER encoded) byte array.
544 *
545 * @returns S_OK.
546 * @param a_pAsn1Obj The ASN.1 object to get the DER encoded bytes for.
547 * @param a_rReturn The return byte vector.
548 * @throws std::bad_alloc
549 */
550HRESULT Certificate::i_getEncodedBytes(PRTASN1CORE a_pAsn1Obj, std::vector<BYTE> &a_rReturn)
551{
552 HRESULT hrc = S_OK;
553 Assert(a_rReturn.size() == 0);
554 if (RTAsn1Core_IsPresent(a_pAsn1Obj))
555 {
556 uint32_t cbEncoded;
557 int vrc = RTAsn1EncodePrepare(a_pAsn1Obj, 0, &cbEncoded, NULL);
558 if (RT_SUCCESS(vrc))
559 {
560 a_rReturn.resize(cbEncoded);
561 Assert(a_rReturn.size() == cbEncoded);
562 if (cbEncoded)
563 {
564 vrc = RTAsn1EncodeToBuffer(a_pAsn1Obj, 0, &a_rReturn.front(), a_rReturn.size(), NULL);
565 if (RT_FAILURE(vrc))
566 hrc = setErrorVrc(vrc, "RTAsn1EncodeToBuffer failed with %Rrc", vrc);
567 }
568 }
569 else
570 hrc = setErrorVrc(vrc, "RTAsn1EncodePrepare failed with %Rrc", vrc);
571 }
572 return hrc;
573}
574
575/** @} */
576
Note: See TracBrowser for help on using the repository browser.

© 2024 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette