SecPolicyLeafCallbacks.c [plain text]
#include <AssertMacros.h>
#include <CoreFoundation/CFDictionary.h>
#include <Security/SecPolicyPriv.h>
#include <Security/SecPolicyInternal.h>
#include <Security/SecCertificateInternal.h>
#include <utilities/SecCFWrappers.h>
#include <utilities/SecInternalReleasePriv.h>
#include <wctype.h>
#include <dlfcn.h>
#include <libDER/oids.h>
typedef bool (*SecPolicyCheckCertFunction)(SecCertificateRef cert, CFTypeRef pvcValue);
bool SecPolicyCheckCertCriticalExtensions(SecCertificateRef cert, CFTypeRef __unused pvcValue) {
if (SecCertificateHasUnknownCriticalExtension(cert)) {
return false;
}
return true;
}
static bool keyusage_allows(SecKeyUsage keyUsage, CFTypeRef xku) {
if (!xku || CFGetTypeID(xku) != CFNumberGetTypeID())
return false;
SInt32 dku;
CFNumberGetValue((CFNumberRef)xku, kCFNumberSInt32Type, &dku);
SecKeyUsage ku = (SecKeyUsage)dku;
return (keyUsage & ku) == ku;
}
bool SecPolicyCheckCertKeyUsage(SecCertificateRef cert, CFTypeRef pvcValue) {
SecKeyUsage keyUsage = SecCertificateGetKeyUsage(cert);
bool match = false;
CFTypeRef xku = pvcValue;
if (isArray(xku)) {
CFIndex ix, count = CFArrayGetCount(xku);
for (ix = 0; ix < count; ++ix) {
CFTypeRef ku = CFArrayGetValueAtIndex(xku, ix);
if (keyusage_allows(keyUsage, ku)) {
match = true;
break;
}
}
} else {
match = keyusage_allows(keyUsage, xku);
}
return match;
}
static bool extendedkeyusage_allows(CFArrayRef extendedKeyUsage,
CFDataRef xeku) {
if (!xeku)
return false;
if (extendedKeyUsage) {
CFRange all = { 0, CFArrayGetCount(extendedKeyUsage) };
return CFArrayContainsValue(extendedKeyUsage, all, xeku);
} else {
return CFDataGetLength((CFDataRef)xeku) == 0;
}
}
static bool isExtendedKeyUsageAllowed(CFArrayRef extendedKeyUsage,
CFTypeRef xeku) {
if (!xeku) {
return false;
}
if(CFGetTypeID(xeku) == CFDataGetTypeID()) {
return extendedkeyusage_allows(extendedKeyUsage, xeku);
} else if (CFGetTypeID(xeku) == CFStringGetTypeID()) {
CFDataRef eku = SecCertificateCreateOidDataFromString(NULL, xeku);
if (eku) {
bool result = extendedkeyusage_allows(extendedKeyUsage, eku);
CFRelease(eku);
return result;
}
}
return false;
}
bool SecPolicyCheckCertExtendedKeyUsage(SecCertificateRef cert, CFTypeRef pvcValue) {
CFArrayRef certExtendedKeyUsage = SecCertificateCopyExtendedKeyUsage(cert);
bool match = false;
CFTypeRef xeku = pvcValue;
if (isArray(xeku)) {
CFIndex ix, count = CFArrayGetCount(xeku);
for (ix = 0; ix < count; ix++) {
CFTypeRef eku = CFArrayGetValueAtIndex(xeku, ix);
if (isExtendedKeyUsageAllowed(certExtendedKeyUsage, eku)) {
match = true;
break;
}
}
} else {
match = isExtendedKeyUsageAllowed(certExtendedKeyUsage, xeku);
}
CFReleaseSafe(certExtendedKeyUsage);
return match;
}
bool SecPolicyCheckCertNonEmptySubject(SecCertificateRef cert, CFTypeRef __unused pvcValue) {
if (!SecCertificateHasSubject(cert)) {
if (SecCertificateIsCA(cert)) {
return false;
} else {
if (!SecCertificateHasCriticalSubjectAltName(cert)) {
return false;
}
}
}
return true;
}
typedef bool (*CFNIsTLD_f)(CFStringRef domain);
bool SecDNSIsTLD(CFStringRef reference) {
bool result = false;
static CFNIsTLD_f CFNIsDomainTopLevelFunctionPtr = NULL;
static dispatch_once_t onceToken;
CFStringRef presentedDomain = NULL;
dispatch_once(&onceToken, ^{
void *framework = dlopen("/System/Library/Frameworks/CFNetwork.framework/CFNetwork", RTLD_LAZY);
if (framework) {
CFNIsDomainTopLevelFunctionPtr = dlsym(framework, "_CFHostIsDomainTopLevelForCertificatePolicy");
}
});
require_quiet(CFNIsDomainTopLevelFunctionPtr, out);
CFIndex referenceLen = CFStringGetLength(reference);
require_action_quiet(referenceLen > 2, out, result = true);
require_quiet(presentedDomain = CFStringCreateWithSubstring(NULL, reference,
CFRangeMake(2, referenceLen - 2)),
out);
result = CFNIsDomainTopLevelFunctionPtr(presentedDomain);
out:
CFReleaseNull(presentedDomain);
return result;
}
static bool SecDNSMatch(CFStringRef reference, CFStringRef presented) {
CFArrayRef referenceLabels = NULL, presentedLabels = NULL;
bool result = false;
CFIndex referenceLen = CFStringGetLength(reference);
require_quiet(referenceLen > 0, noMatch);
if ('.' == CFStringGetCharacterAtIndex(reference, referenceLen - 1)) {
CFStringRef truncatedReference = CFStringCreateWithSubstring(NULL, reference,
CFRangeMake(0, referenceLen - 1));
referenceLabels = CFStringCreateArrayBySeparatingStrings(NULL, truncatedReference, CFSTR("."));
CFReleaseNull(truncatedReference);
require_quiet(referenceLabels, noMatch);
} else {
require_quiet(referenceLabels = CFStringCreateArrayBySeparatingStrings(NULL, reference, CFSTR(".")),
noMatch);
}
require_quiet(presentedLabels = CFStringCreateArrayBySeparatingStrings(NULL, presented, CFSTR(".")),
noMatch);
require_quiet(CFArrayGetCount(referenceLabels) == CFArrayGetCount(presentedLabels), noMatch);
CFIndex ix, count = CFArrayGetCount(referenceLabels);
for (ix = count - 1; ix >= 0; ix--) {
CFStringRef rlabel = NULL, plabel = NULL;
require_quiet(rlabel = CFArrayGetValueAtIndex(referenceLabels, ix), noMatch);
require_quiet(plabel = CFArrayGetValueAtIndex(presentedLabels, ix), noMatch);
if (CFEqual(plabel, CFSTR("*"))) {
require_quiet(ix == 0, noMatch);
require_quiet(count > 2 && ix != count - 2, noMatch);
require_quiet(!SecDNSIsTLD(presented), noMatch);
} else {
CFRange partialRange = CFStringFind(plabel, CFSTR("*"), 0);
require_quiet(partialRange.location == kCFNotFound && partialRange.length == 0 ,
noMatch);
require_quiet(CFStringCompare(rlabel, plabel, kCFCompareCaseInsensitive) == kCFCompareEqualTo, noMatch);
}
}
result = true;
noMatch:
CFReleaseNull(referenceLabels);
CFReleaseNull(presentedLabels);
return result;
}
bool SecPolicyCheckCertSSLHostname(SecCertificateRef cert, CFTypeRef pvcValue) {
CFStringRef hostName = pvcValue;
if (!isString(hostName)) {
return false;
}
bool dnsMatch = false;
CFArrayRef dnsNames = SecCertificateCopyDNSNames(cert);
if (dnsNames) {
CFIndex ix, count = CFArrayGetCount(dnsNames);
for (ix = 0; ix < count; ++ix) {
CFStringRef dns = (CFStringRef)CFArrayGetValueAtIndex(dnsNames, ix);
if (SecDNSMatch(hostName, dns)) {
dnsMatch = true;
break;
}
}
CFRelease(dnsNames);
}
if (!dnsMatch) {
CFArrayRef ipAddresses = SecCertificateCopyIPAddresses(cert);
if (ipAddresses) {
CFIndex ix, count = CFArrayGetCount(ipAddresses);
for (ix = 0; ix < count; ++ix) {
CFStringRef ipAddress = (CFStringRef)CFArrayGetValueAtIndex(ipAddresses, ix);
if (!CFStringCompare(hostName, ipAddress, kCFCompareCaseInsensitive)) {
dnsMatch = true;
break;
}
}
CFRelease(ipAddresses);
}
}
return dnsMatch;
}
bool SecPolicyCheckCertEmail(SecCertificateRef cert, CFTypeRef pvcValue) {
CFStringRef email = pvcValue;
bool match = false;
if (!isString(email)) {
return false;
}
CFArrayRef addrs = SecCertificateCopyRFC822Names(cert);
if (addrs) {
CFIndex ix, count = CFArrayGetCount(addrs);
for (ix = 0; ix < count; ++ix) {
CFStringRef addr = (CFStringRef)CFArrayGetValueAtIndex(addrs, ix);
if (!CFStringCompare(email, addr, kCFCompareCaseInsensitive)) {
match = true;
break;
}
}
CFRelease(addrs);
}
return match;
}
bool SecPolicyCheckCertTemporalValidity(SecCertificateRef cert, CFTypeRef pvcValue) {
CFAbsoluteTime verifyTime = CFDateGetAbsoluteTime(pvcValue);
if (!SecCertificateIsValid(cert, verifyTime)) {
return false;
}
return true;
}
bool SecPolicyCheckCertSubjectCommonNamePrefix(SecCertificateRef cert, CFTypeRef pvcValue) {
CFStringRef prefix = pvcValue;
bool match = true;
if (!isString(prefix)) {
return false;
}
CFArrayRef commonNames = SecCertificateCopyCommonNames(cert);
if (!commonNames || CFArrayGetCount(commonNames) != 1 ||
!CFStringHasPrefix(CFArrayGetValueAtIndex(commonNames, 0), prefix)) {
match = false;
}
CFReleaseSafe(commonNames);
return match;
}
bool SecPolicyCheckCertSubjectCommonName(SecCertificateRef cert, CFTypeRef pvcValue) {
CFStringRef common_name = pvcValue;
bool match = true;
if (!isString(common_name)) {
return false;
}
CFArrayRef commonNames = SecCertificateCopyCommonNames(cert);
if (!commonNames || CFArrayGetCount(commonNames) != 1 ||
!CFEqual(common_name, CFArrayGetValueAtIndex(commonNames, 0))) {
match = false;
}
CFReleaseSafe(commonNames);
return match;
}
bool SecPolicyCheckCertSubjectCommonNameTEST(SecCertificateRef cert, CFTypeRef pvcValue) {
CFStringRef common_name = pvcValue;
bool match = true;
if (!isString(common_name)) {
return false;
}
CFArrayRef commonNames = SecCertificateCopyCommonNames(cert);
if (!commonNames || CFArrayGetCount(commonNames) != 1) {
CFStringRef cert_common_name = CFArrayGetValueAtIndex(commonNames, 0);
CFStringRef test_common_name = common_name ?
CFStringCreateWithFormat(kCFAllocatorDefault,
NULL, CFSTR("TEST %@ TEST"), common_name) :
NULL;
if (!CFEqual(common_name, cert_common_name) &&
(!test_common_name || !CFEqual(test_common_name, cert_common_name)))
match = false;
CFReleaseSafe(test_common_name);
}
CFReleaseSafe(commonNames);
return match;
}
bool SecPolicyCheckCertNotValidBefore(SecCertificateRef cert, CFTypeRef pvcValue) {
CFDateRef date = pvcValue;
if (!isDate(date)) {
return false;
}
CFAbsoluteTime at = CFDateGetAbsoluteTime(date);
if (SecCertificateNotValidBefore(cert) <= at) {
return false;
}
return true;
}
bool SecPolicyCheckCertSubjectOrganization(SecCertificateRef cert, CFTypeRef pvcValue) {
CFStringRef org = pvcValue;
bool match = true;
if (!isString(org)) {
return false;
}
CFArrayRef organization = SecCertificateCopyOrganization(cert);
if (!organization || CFArrayGetCount(organization) != 1 ||
!CFEqual(org, CFArrayGetValueAtIndex(organization, 0))) {
match = false;
}
CFReleaseSafe(organization);
return match;
}
bool SecPolicyCheckCertSubjectOrganizationalUnit(SecCertificateRef cert, CFTypeRef pvcValue) {
CFStringRef orgUnit = pvcValue;
bool match = true;
if (!isString(orgUnit)) {
return false;
}
CFArrayRef organizationalUnit = SecCertificateCopyOrganizationalUnit(cert);
if (!organizationalUnit || CFArrayGetCount(organizationalUnit) != 1 ||
!CFEqual(orgUnit, CFArrayGetValueAtIndex(organizationalUnit, 0))) {
match = false;
}
CFReleaseSafe(organizationalUnit);
return match;
}
bool SecPolicyCheckCertSubjectCountry(SecCertificateRef cert, CFTypeRef pvcValue) {
CFStringRef country = pvcValue;
bool match = true;
if (!isString(country)) {
return false;
}
CFArrayRef certCountry = SecCertificateCopyCountry(cert);
if (!certCountry || CFArrayGetCount(certCountry) != 1 ||
!CFEqual(country, CFArrayGetValueAtIndex(certCountry, 0))) {
match = false;
}
CFReleaseSafe(certCountry);
return match;
}
bool SecPolicyCheckCertEAPTrustedServerNames(SecCertificateRef cert, CFTypeRef pvcValue) {
CFArrayRef trustedServerNames = pvcValue;
if (!trustedServerNames)
return true;
if (!isArray(trustedServerNames)) {
return false;
}
CFIndex tsnCount = CFArrayGetCount(trustedServerNames);
bool dnsMatch = false;
CFArrayRef dnsNames = SecCertificateCopyDNSNames(cert);
if (dnsNames) {
CFIndex ix, count = CFArrayGetCount(dnsNames);
for (ix = 0; !dnsMatch && ix < count; ++ix) {
CFStringRef dns = (CFStringRef)CFArrayGetValueAtIndex(dnsNames, ix);
CFIndex tix;
for (tix = 0; tix < tsnCount; ++tix) {
CFStringRef serverName =
(CFStringRef)CFArrayGetValueAtIndex(trustedServerNames, tix);
if (!isString(serverName)) {
CFReleaseSafe(dnsNames);
return false;
}
if (SecDNSMatch(dns, serverName)) {
dnsMatch = true;
break;
}
}
}
CFRelease(dnsNames);
}
return dnsMatch;
}
bool SecPolicyCheckCertLeafMarkerOid(SecCertificateRef cert, CFTypeRef pvcValue) {
if (pvcValue && SecCertificateHasMarkerExtension(cert, pvcValue)) {
return true;
}
return false;
}
bool SecPolicyCheckCertLeafMarkerOidWithoutValueCheck(SecCertificateRef cert,
CFTypeRef pvcValue) {
if (CFGetTypeID(pvcValue) == CFArrayGetTypeID()) {
CFIndex ix, length = CFArrayGetCount(pvcValue);
for (ix = 0; ix < length; ix++)
if (SecPolicyCheckCertLeafMarkerOidWithoutValueCheck(cert,
CFArrayGetValueAtIndex((CFArrayRef)pvcValue, ix))) {
return true;
}
} else if (CFGetTypeID(pvcValue) == CFDataGetTypeID() ||
CFGetTypeID(pvcValue) == CFStringGetTypeID()) {
return (NULL != SecCertificateGetExtensionValue(cert, pvcValue));
}
return false;
}
bool SecPolicyCheckCertLeafMarkersProdAndQA(SecCertificateRef cert, CFTypeRef pvcValue)
{
CFTypeRef prodValue = CFDictionaryGetValue(pvcValue, kSecPolicyLeafMarkerProd);
if (!SecPolicyCheckCertLeafMarkerOid(cert, prodValue)) {
bool result = false;
return result;
}
return true;
}
static CFSetRef copyCertificatePolicies(SecCertificateRef cert) {
CFMutableSetRef policies = NULL;
policies = CFSetCreateMutable(NULL, 0, &kCFTypeSetCallBacks);
if (!policies) return NULL;
const SecCECertificatePolicies *cp = SecCertificateGetCertificatePolicies(cert);
size_t policy_ix, policy_count = cp ? cp->numPolicies : 0;
for (policy_ix = 0; policy_ix < policy_count; ++policy_ix) {
CFDataRef oidData = NULL;
DERItem *policyOID = &cp->policies[policy_ix].policyIdentifier;
oidData = CFDataCreate(kCFAllocatorDefault, policyOID->data, policyOID->length);
CFSetAddValue(policies, oidData);
CFReleaseSafe(oidData);
}
return policies;
}
static bool checkPolicyOidData(SecCertificateRef cert , CFDataRef oid) {
CFSetRef policies = copyCertificatePolicies(cert);
bool found = false;
if (policies && CFSetContainsValue(policies, oid)) {
found = true;
}
CFReleaseSafe(policies);
return found;
}
bool SecPolicyCheckCertCertificatePolicy(SecCertificateRef cert, CFTypeRef pvcValue) {
CFTypeRef value = pvcValue;
bool result = false;
if (CFGetTypeID(value) == CFDataGetTypeID())
{
result = checkPolicyOidData(cert, value);
} else if (CFGetTypeID(value) == CFStringGetTypeID()) {
CFDataRef dataOid = SecCertificateCreateOidDataFromString(NULL, value);
if (dataOid) {
result = checkPolicyOidData(cert, dataOid);
CFRelease(dataOid);
}
}
return result;
}
bool SecPolicyCheckCertWeakKeySize(SecCertificateRef cert, CFTypeRef __unused pvcValue) {
if (cert && SecCertificateIsWeakKey(cert)) {
return false;
}
return true;
}
bool SecPolicyCheckCertKeySize(SecCertificateRef cert, CFTypeRef pvcValue) {
CFDictionaryRef keySizes = pvcValue;
if (!SecCertificateIsAtLeastMinKeySize(cert, keySizes)) {
return false;
}
return true;
}
bool SecPolicyCheckCertWeakSignature(SecCertificateRef cert, CFTypeRef __unused pvcValue) {
bool result = true;
CFMutableSetRef disallowedHashes = CFSetCreateMutable(NULL, 3, &kCFTypeSetCallBacks);
if (!disallowedHashes) {
return result;
}
CFSetAddValue(disallowedHashes, kSecSignatureDigestAlgorithmMD2);
CFSetAddValue(disallowedHashes, kSecSignatureDigestAlgorithmMD4);
CFSetAddValue(disallowedHashes, kSecSignatureDigestAlgorithmMD5);
Boolean isSelfSigned = false;
OSStatus status = SecCertificateIsSelfSigned(cert, &isSelfSigned);
if (!SecPolicyCheckCertSignatureHashAlgorithms(cert, disallowedHashes) && (status != errSecSuccess || !isSelfSigned)) {
result = false;
}
CFReleaseSafe(disallowedHashes);
return result;
}
static CFStringRef convertSignatureHashAlgorithm(SecSignatureHashAlgorithm algorithmEnum) {
const void *digests[] = { kSecSignatureDigestAlgorithmUnknown,
kSecSignatureDigestAlgorithmMD2,
kSecSignatureDigestAlgorithmMD4,
kSecSignatureDigestAlgorithmMD5,
kSecSignatureDigestAlgorithmSHA1,
kSecSignatureDigestAlgorithmSHA224,
kSecSignatureDigestAlgorithmSHA256,
kSecSignatureDigestAlgorithmSHA384,
kSecSignatureDigestAlgorithmSHA512,
};
return digests[algorithmEnum];
}
bool SecPolicyCheckCertSignatureHashAlgorithms(SecCertificateRef cert, CFTypeRef pvcValue) {
CFSetRef disallowedHashAlgorithms = pvcValue;
CFStringRef certAlg = convertSignatureHashAlgorithm(SecCertificateGetSignatureHashAlgorithm(cert));
if (CFSetContainsValue(disallowedHashAlgorithms, certAlg)) {
return false;
}
return true;
}
static CFDictionaryRef SecLeafPVCCopyCallbacks(void) {
CFMutableDictionaryRef leafCallbacks = NULL;
leafCallbacks = CFDictionaryCreateMutable(kCFAllocatorDefault, 0,
&kCFTypeDictionaryKeyCallBacks, NULL);
#undef POLICYCHECKMACRO
#define __PC_ADD_CHECK_(NAME)
#define __PC_ADD_CHECK_O(NAME) CFDictionaryAddValue(leafCallbacks, \
kSecPolicyCheck##NAME, SecPolicyCheckCert##NAME);
#define POLICYCHECKMACRO(NAME, TRUSTRESULT, SUBTYPE, LEAFCHECK, PATHCHECK, LEAFONLY, CSSMERR, OSSTATUS) \
__PC_ADD_CHECK_##LEAFONLY(NAME)
#include "SecPolicyChecks.list"
return leafCallbacks;
}
void SecLeafPVCInit(SecLeafPVCRef pvc, SecCertificateRef leaf, CFArrayRef policies,
CFAbsoluteTime verifyTime) {
secdebug("alloc", "leafpvc %p", pvc);
pvc->leaf = CFRetainSafe(leaf);
pvc->policies = CFRetainSafe(policies);
pvc->verifyTime = verifyTime;
pvc->callbacks = SecLeafPVCCopyCallbacks();
pvc->policyIX = 0;
pvc->result = true;
CFMutableDictionaryRef certDetail = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
pvc->details = CFArrayCreate(kCFAllocatorDefault, (const void **)&certDetail, 1,
&kCFTypeArrayCallBacks);
CFRelease(certDetail);
}
void SecLeafPVCDelete(SecLeafPVCRef pvc) {
secdebug("alloc", "delete leaf pvc %p", pvc);
CFReleaseNull(pvc->policies);
CFReleaseNull(pvc->details);
CFReleaseNull(pvc->callbacks);
CFReleaseNull(pvc->leaf);
}
static bool SecLeafPVCSetResultForced(SecLeafPVCRef pvc,
CFStringRef key, CFIndex ix, CFTypeRef result, bool force) {
secdebug("policy", "cert[%d]: %@ =(%s)[%s]> %@", (int) ix, key, "leaf",
(force ? "force" : ""), result);
if (!force) {
SecPolicyRef policy = (SecPolicyRef)CFArrayGetValueAtIndex(pvc->policies, pvc->policyIX);
if (policy && !CFDictionaryContainsKey(policy->_options, key))
return true;
}
pvc->result = false;
if (!pvc->details)
return false;
CFMutableDictionaryRef detail =
(CFMutableDictionaryRef)CFArrayGetValueAtIndex(pvc->details, ix);
CFDictionarySetValue(detail, key, result);
return true;
}
static bool SecLeafPVCSetResult(SecLeafPVCRef pvc,
CFStringRef key, CFIndex ix, CFTypeRef result) {
return SecLeafPVCSetResultForced(pvc, key, ix, result, false);
}
static void SecLeafPVCValidateKey(const void *key, const void *value,
void *context) {
SecLeafPVCRef pvc = (SecLeafPVCRef)context;
if (!pvc->result && !pvc->details)
return;
SecPolicyCheckCertFunction fcn = (SecPolicyCheckCertFunction) CFDictionaryGetValue(pvc->callbacks, key);
if (!fcn) {
pvc->result = false;
return;
}
if (CFEqual(key, kSecPolicyCheckTemporalValidity)) {
CFDateRef verifyDate = CFDateCreate(NULL, pvc->verifyTime);
if(!fcn(pvc->leaf, verifyDate)) {
SecLeafPVCSetResult(pvc, key, 0, kCFBooleanFalse);
}
CFReleaseSafe(verifyDate);
} else {
SecPolicyRef policy = (SecPolicyRef)CFArrayGetValueAtIndex(pvc->policies, pvc->policyIX);
if (!policy) {
pvc->result = false;
return;
}
CFTypeRef pvcValue = (CFTypeRef)CFDictionaryGetValue(policy->_options, key);
if(!fcn(pvc->leaf, pvcValue)) {
SecLeafPVCSetResult(pvc, key, 0, kCFBooleanFalse);
}
}
}
bool SecLeafPVCLeafChecks(SecLeafPVCRef pvc) {
pvc->result = true;
CFArrayRef policies = pvc->policies;
CFIndex ix, count = CFArrayGetCount(policies);
for (ix = 0; ix < count; ++ix) {
SecPolicyRef policy = (SecPolicyRef)CFArrayGetValueAtIndex(policies, ix);
pvc->policyIX = ix;
CFDictionaryApplyFunction(policy->_options, SecLeafPVCValidateKey, pvc);
if (!pvc->result && !pvc->details)
return pvc->result;
}
return pvc->result;
}