123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590 |
- /* This work is licensed under a Creative Commons CCZero 1.0 Universal License.
- * See http://creativecommons.org/publicdomain/zero/1.0/ for more information.
- *
- * Copyright 2018 (c) Mark Giraud, Fraunhofer IOSB
- * Copyright 2019 (c) Kalycito Infotech Private Limited
- * Copyright 2019 (c) Julius Pfrommer, Fraunhofer IOSB
- */
- #include <open62541/server_config.h>
- #include <open62541/plugin/pki_default.h>
- #include <open62541/plugin/log_stdout.h>
- #ifdef UA_ENABLE_ENCRYPTION
- #include <mbedtls/x509.h>
- #include <mbedtls/x509_crt.h>
- #include <mbedtls/error.h>
- #endif
- #define REMOTECERTIFICATETRUSTED 1
- #define ISSUERKNOWN 2
- #define DUALPARENT 3
- #define PARENTFOUND 4
- /************/
- /* AllowAll */
- /************/
- static UA_StatusCode
- verifyCertificateAllowAll(void *verificationContext,
- const UA_ByteString *certificate) {
- return UA_STATUSCODE_GOOD;
- }
- static UA_StatusCode
- verifyApplicationURIAllowAll(void *verificationContext,
- const UA_ByteString *certificate,
- const UA_String *applicationURI) {
- return UA_STATUSCODE_GOOD;
- }
- static void
- deleteVerifyAllowAll(UA_CertificateVerification *cv) {
- }
- void UA_CertificateVerification_AcceptAll(UA_CertificateVerification *cv) {
- cv->verifyCertificate = verifyCertificateAllowAll;
- cv->verifyApplicationURI = verifyApplicationURIAllowAll;
- cv->deleteMembers = deleteVerifyAllowAll;
- }
- #ifdef UA_ENABLE_ENCRYPTION
- typedef struct {
- /* If the folders are defined, we use them to reload the certificates during
- * runtime */
- UA_String trustListFolder;
- UA_String issuerListFolder;
- UA_String revocationListFolder;
- mbedtls_x509_crt certificateTrustList;
- mbedtls_x509_crt certificateIssuerList;
- mbedtls_x509_crl certificateRevocationList;
- } CertInfo;
- #ifdef __linux__ /* Linux only so far */
- #include <dirent.h>
- #include <limits.h>
- static UA_StatusCode
- fileNamesFromFolder(const UA_String *folder, size_t *pathsSize, UA_String **paths) {
- char buf[PATH_MAX + 1];
- if(folder->length > PATH_MAX)
- return UA_STATUSCODE_BADINTERNALERROR;
- memcpy(buf, folder->data, folder->length);
- buf[folder->length] = 0;
-
- DIR *dir = opendir(buf);
- if(!dir)
- return UA_STATUSCODE_BADINTERNALERROR;
- *paths = (UA_String*)UA_Array_new(256, &UA_TYPES[UA_TYPES_STRING]);
- if(*paths == NULL) {
- closedir(dir);
- return UA_STATUSCODE_BADOUTOFMEMORY;
- }
- struct dirent *ent;
- char buf2[PATH_MAX + 1];
- realpath(buf, buf2);
- size_t pathlen = strlen(buf2);
- *pathsSize = 0;
- while((ent = readdir (dir)) != NULL && *pathsSize < 256) {
- if(ent->d_type != DT_REG)
- continue;
- buf2[pathlen] = '/';
- buf2[pathlen+1] = 0;
- strcat(buf2, ent->d_name);
- (*paths)[*pathsSize] = UA_STRING_ALLOC(buf2);
- *pathsSize += 1;
- }
- closedir(dir);
- if(*pathsSize == 0) {
- UA_free(*paths);
- *paths = NULL;
- }
- return UA_STATUSCODE_GOOD;
- }
- static UA_StatusCode
- reloadCertificates(CertInfo *ci) {
- UA_StatusCode retval = UA_STATUSCODE_GOOD;
- int err = 0;
- /* Load the trustlists */
- if(ci->trustListFolder.length > 0) {
- UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "Reloading the trust-list");
- mbedtls_x509_crt_free(&ci->certificateTrustList);
- mbedtls_x509_crt_init(&ci->certificateTrustList);
- char f[PATH_MAX];
- memcpy(f, ci->trustListFolder.data, ci->trustListFolder.length);
- f[ci->trustListFolder.length] = 0;
- err = mbedtls_x509_crt_parse_path(&ci->certificateTrustList, f);
- if(err == 0) {
- UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER,
- "Loaded certificate from %s", f);
- } else {
- char errBuff[300];
- mbedtls_strerror(err, errBuff, 300);
- UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER,
- "Failed to load certificate from %s", f);
- }
- }
- /* Load the revocationlists */
- if(ci->revocationListFolder.length > 0) {
- UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "Reloading the revocation-list");
- size_t pathsSize = 0;
- UA_String *paths = NULL;
- retval = fileNamesFromFolder(&ci->revocationListFolder, &pathsSize, &paths);
- if(retval != UA_STATUSCODE_GOOD)
- return retval;
- mbedtls_x509_crl_free(&ci->certificateRevocationList);
- mbedtls_x509_crl_init(&ci->certificateRevocationList);
- for(size_t i = 0; i < pathsSize; i++) {
- char f[PATH_MAX];
- memcpy(f, paths[i].data, paths[i].length);
- f[paths[i].length] = 0;
- err = mbedtls_x509_crl_parse_file(&ci->certificateRevocationList, f);
- if(err == 0) {
- UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER,
- "Loaded certificate from %.*s",
- (int)paths[i].length, paths[i].data);
- } else {
- UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER,
- "Failed to load certificate from %.*s",
- (int)paths[i].length, paths[i].data);
- }
- }
- UA_Array_delete(paths, pathsSize, &UA_TYPES[UA_TYPES_STRING]);
- paths = NULL;
- pathsSize = 0;
- }
- /* Load the issuerlists */
- if(ci->issuerListFolder.length > 0) {
- UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "Reloading the issuer-list");
- mbedtls_x509_crt_free(&ci->certificateIssuerList);
- mbedtls_x509_crt_init(&ci->certificateIssuerList);
- char f[PATH_MAX];
- memcpy(f, ci->issuerListFolder.data, ci->issuerListFolder.length);
- f[ci->issuerListFolder.length] = 0;
- err = mbedtls_x509_crt_parse_path(&ci->certificateIssuerList, f);
- if(err == 0) {
- UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER,
- "Loaded certificate from %s", f);
- } else {
- UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER,
- "Failed to load certificate from %s", f);
- }
- }
- return retval;
- }
- #endif
- static UA_StatusCode
- certificateVerification_verify(void *verificationContext,
- const UA_ByteString *certificate) {
- CertInfo *ci = (CertInfo*)verificationContext;
- if(!ci)
- return UA_STATUSCODE_BADINTERNALERROR;
- #ifdef __linux__ /* Reload certificates if folder paths are specified */
- reloadCertificates(ci);
- #endif
- /* if(ci->certificateTrustList.raw.len == 0) { */
- /* UA_LOG_WARNING(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, */
- /* "No Trustlist loaded. Accepting the certificate."); */
- /* return UA_STATUSCODE_GOOD; */
- /* } */
- /* Parse the certificate */
- mbedtls_x509_crt remoteCertificate;
- /* Temporary Object to parse the trustList */
- mbedtls_x509_crt *tempCert;
- /* Temporary Object to parse the revocationList */
- mbedtls_x509_crl *tempCrl;
- /* Temporary Object to identify the parent CA when there is no intermediate CA */
- mbedtls_x509_crt *parentCert;
- /* Temporary Object to identify the parent CA when there is intermediate CA */
- mbedtls_x509_crt *parentCert_2;
- /* Flag value to identify if the issuer certificate is found */
- int issuerKnown = 0;
- /* Flag value to identify if the parent certificate found */
- int parentFound = 0;
- mbedtls_x509_crt_init(&remoteCertificate);
- int mbedErr = mbedtls_x509_crt_parse(&remoteCertificate, certificate->data,
- certificate->length);
- if(mbedErr) {
- /* char errBuff[300]; */
- /* mbedtls_strerror(mbedErr, errBuff, 300); */
- /* UA_LOG_WARNING(data->policyContext->securityPolicy->logger, UA_LOGCATEGORY_SECURITYPOLICY, */
- /* "Could not parse the remote certificate with error: %s", errBuff); */
- return UA_STATUSCODE_BADSECURITYCHECKSFAILED;
- }
- /* Verify */
- mbedtls_x509_crt_profile crtProfile = {
- MBEDTLS_X509_ID_FLAG(MBEDTLS_MD_SHA1) | MBEDTLS_X509_ID_FLAG(MBEDTLS_MD_SHA256),
- 0xFFFFFF, 0x000000, 128 * 8 // in bits
- }; // TODO: remove magic numbers
- uint32_t flags = 0;
- mbedErr = mbedtls_x509_crt_verify_with_profile(&remoteCertificate,
- &ci->certificateTrustList,
- &ci->certificateRevocationList,
- &crtProfile, NULL, &flags, NULL, NULL);
- /* Flag to check if the remote certificate is trusted or not */
- int TRUSTED = 0;
- /* Check if the remoteCertificate is present in the trustList while mbedErr value is not zero */
- if(mbedErr && !(flags & MBEDTLS_X509_BADCERT_EXPIRED) && !(flags & MBEDTLS_X509_BADCERT_FUTURE)) {
- for(tempCert = &ci->certificateTrustList; tempCert != NULL; tempCert = tempCert->next) {
- if(remoteCertificate.raw.len == tempCert->raw.len &&
- memcmp(remoteCertificate.raw.p, tempCert->raw.p, remoteCertificate.raw.len) == 0) {
- TRUSTED = REMOTECERTIFICATETRUSTED;
- break;
- }
- }
- }
- /* If the remote certificate is present in the trustList then check if the issuer certificate
- * of remoteCertificate is present in issuerList */
- if(TRUSTED && mbedErr) {
- mbedErr = mbedtls_x509_crt_verify_with_profile(&remoteCertificate,
- &ci->certificateIssuerList,
- &ci->certificateRevocationList,
- &crtProfile, NULL, &flags, NULL, NULL);
- /* Check if the parent certificate has a CRL file available */
- if(!mbedErr) {
- /* Flag value to identify if that there is an intermediate CA present */
- int dualParent = 0;
- /* Identify the topmost parent certificate for the remoteCertificate */
- for( parentCert = &ci->certificateIssuerList; parentCert != NULL; parentCert = parentCert->next ) {
- if(memcmp(remoteCertificate.issuer_raw.p, parentCert->subject_raw.p, parentCert->subject_raw.len) == 0) {
- for(parentCert_2 = &ci->certificateTrustList; parentCert_2 != NULL; parentCert_2 = parentCert_2->next) {
- if(memcmp(parentCert->issuer_raw.p, parentCert_2->subject_raw.p, parentCert_2->subject_raw.len) == 0) {
- dualParent = DUALPARENT;
- parentFound = PARENTFOUND;
- break;
- }
- }
- parentFound = PARENTFOUND;
- }
- if(parentFound == PARENTFOUND) {
- break;
- }
- }
- /* Check if there is an intermediate certificate between the topmost parent
- * certificate and child certificate
- * If yes the topmost parent certificate is to be checked whether it has a
- * CRL file avaiable */
- if(dualParent == DUALPARENT && parentFound == PARENTFOUND) {
- parentCert = parentCert_2;
- }
- /* If a parent certificate is found traverse the revocationList and identify
- * if there is any CRL file that corresponds to the parentCertificate */
- if(parentFound == PARENTFOUND) {
- tempCrl = &ci->certificateRevocationList;
- while(tempCrl != NULL) {
- if(tempCrl->version != 0 &&
- tempCrl->issuer_raw.len == parentCert->subject_raw.len &&
- memcmp(tempCrl->issuer_raw.p,
- parentCert->subject_raw.p,
- tempCrl->issuer_raw.len) == 0) {
- issuerKnown = ISSUERKNOWN;
- break;
- }
- tempCrl = tempCrl->next;
- }
- /* If the CRL file corresponding to the parent certificate is not present
- * then return UA_STATUSCODE_BADCERTIFICATEISSUERREVOCATIONUNKNOWN */
- if(!issuerKnown) {
- return UA_STATUSCODE_BADCERTIFICATEISSUERREVOCATIONUNKNOWN;
- }
- }
- }
- }
- else if(!mbedErr && !TRUSTED) {
- /* This else if section is to identify if the parent certificate which is present in trustList
- * has CRL file corresponding to it */
- /* Identify the parent certificate of the remoteCertificate */
- for(parentCert = &ci->certificateTrustList; parentCert != NULL; parentCert = parentCert->next) {
- if(memcmp(remoteCertificate.issuer_raw.p, parentCert->subject_raw.p, parentCert->subject_raw.len) == 0) {
- parentFound = PARENTFOUND;
- break;
- }
- }
- /* If the parent certificate is found traverse the revocationList and identify
- * if there is any CRL file that corresponds to the parentCertificate */
- if(parentFound == PARENTFOUND &&
- memcmp(remoteCertificate.issuer_raw.p, remoteCertificate.subject_raw.p, remoteCertificate.subject_raw.len) != 0) {
- tempCrl = &ci->certificateRevocationList;
- while(tempCrl != NULL) {
- if(tempCrl->version != 0 &&
- tempCrl->issuer_raw.len == parentCert->subject_raw.len &&
- memcmp(tempCrl->issuer_raw.p,
- parentCert->subject_raw.p,
- tempCrl->issuer_raw.len) == 0) {
- issuerKnown = ISSUERKNOWN;
- break;
- }
- tempCrl = tempCrl->next;
- }
- /* If the CRL file corresponding to the parent certificate is not present
- * then return UA_STATUSCODE_BADCERTIFICATEREVOCATIONUNKNOWN */
- if(!issuerKnown) {
- return UA_STATUSCODE_BADCERTIFICATEREVOCATIONUNKNOWN;
- }
- }
- }
- // TODO: Extend verification
- /* This condition will check whether the certificate is a User certificate
- * or a CA certificate. If the MBEDTLS_X509_KU_KEY_CERT_SIGN and
- * MBEDTLS_X509_KU_CRL_SIGN of key_usage are set, then the certificate
- * shall be condidered as CA Certificate and cannot be used to establish a
- * connection. Refer the test case CTT/Security/Security Certificate Validation/029.js
- * for more details */
- if((remoteCertificate.key_usage & MBEDTLS_X509_KU_KEY_CERT_SIGN) &&
- (remoteCertificate.key_usage & MBEDTLS_X509_KU_CRL_SIGN)) {
- return UA_STATUSCODE_BADCERTIFICATEUSENOTALLOWED;
- }
- UA_StatusCode retval = UA_STATUSCODE_GOOD;
- if(mbedErr) {
- /* char buff[100]; */
- /* mbedtls_x509_crt_verify_info(buff, 100, "", flags); */
- /* UA_LOG_ERROR(channelContextData->policyContext->securityPolicy->logger, */
- /* UA_LOGCATEGORY_SECURITYPOLICY, */
- /* "Verifying the certificate failed with error: %s", buff); */
- if(flags & (uint32_t)MBEDTLS_X509_BADCERT_NOT_TRUSTED) {
- retval = UA_STATUSCODE_BADCERTIFICATEUNTRUSTED;
- } else if(flags & (uint32_t)MBEDTLS_X509_BADCERT_FUTURE ||
- flags & (uint32_t)MBEDTLS_X509_BADCERT_EXPIRED) {
- retval = UA_STATUSCODE_BADCERTIFICATETIMEINVALID;
- } else if(flags & (uint32_t)MBEDTLS_X509_BADCERT_REVOKED ||
- flags & (uint32_t)MBEDTLS_X509_BADCRL_EXPIRED) {
- retval = UA_STATUSCODE_BADCERTIFICATEREVOKED;
- } else {
- retval = UA_STATUSCODE_BADSECURITYCHECKSFAILED;
- }
- }
- mbedtls_x509_crt_free(&remoteCertificate);
- return retval;
- }
- /* Find binary substring. Taken and adjusted from
- * http://tungchingkai.blogspot.com/2011/07/binary-strstr.html */
- static const unsigned char *
- bstrchr(const unsigned char *s, const unsigned char ch, size_t l) {
- /* find first occurrence of c in char s[] for length l*/
- /* handle special case */
- if(l == 0)
- return (NULL);
- for(; *s != ch; ++s, --l)
- if(l == 0)
- return (NULL);
- return s;
- }
- static const unsigned char *
- bstrstr(const unsigned char *s1, size_t l1, const unsigned char *s2, size_t l2) {
- /* find first occurrence of s2[] in s1[] for length l1*/
- const unsigned char *ss1 = s1;
- const unsigned char *ss2 = s2;
- /* handle special case */
- if(l1 == 0)
- return (NULL);
- if(l2 == 0)
- return s1;
- /* match prefix */
- for (; (s1 = bstrchr(s1, *s2, (uintptr_t)ss1-(uintptr_t)s1+(uintptr_t)l1)) != NULL &&
- (uintptr_t)ss1-(uintptr_t)s1+(uintptr_t)l1 != 0; ++s1) {
- /* match rest of prefix */
- const unsigned char *sc1, *sc2;
- for (sc1 = s1, sc2 = s2; ;)
- if (++sc2 >= ss2+l2)
- return s1;
- else if (*++sc1 != *sc2)
- break;
- }
- return NULL;
- }
- static UA_StatusCode
- certificateVerification_verifyApplicationURI(void *verificationContext,
- const UA_ByteString *certificate,
- const UA_String *applicationURI) {
- CertInfo *ci = (CertInfo*)verificationContext;
- if(!ci)
- return UA_STATUSCODE_BADINTERNALERROR;
- /* Parse the certificate */
- mbedtls_x509_crt remoteCertificate;
- mbedtls_x509_crt_init(&remoteCertificate);
- int mbedErr = mbedtls_x509_crt_parse(&remoteCertificate, certificate->data,
- certificate->length);
- if(mbedErr)
- return UA_STATUSCODE_BADSECURITYCHECKSFAILED;
- /* Poor man's ApplicationUri verification. mbedTLS does not parse all fields
- * of the Alternative Subject Name. Instead test whether the URI-string is
- * present in the v3_ext field in general.
- *
- * TODO: Improve parsing of the Alternative Subject Name */
- UA_StatusCode retval = UA_STATUSCODE_GOOD;
- if(bstrstr(remoteCertificate.v3_ext.p, remoteCertificate.v3_ext.len,
- applicationURI->data, applicationURI->length) == NULL)
- retval = UA_STATUSCODE_BADCERTIFICATEURIINVALID;
- mbedtls_x509_crt_free(&remoteCertificate);
- return retval;
- }
- static void
- certificateVerification_deleteMembers(UA_CertificateVerification *cv) {
- CertInfo *ci = (CertInfo*)cv->context;
- if(!ci)
- return;
- mbedtls_x509_crt_free(&ci->certificateTrustList);
- mbedtls_x509_crl_free(&ci->certificateRevocationList);
- mbedtls_x509_crt_free(&ci->certificateIssuerList);
- UA_String_clear(&ci->trustListFolder);
- UA_String_clear(&ci->issuerListFolder);
- UA_String_clear(&ci->revocationListFolder);
- UA_free(ci);
- cv->context = NULL;
- }
- UA_StatusCode
- UA_CertificateVerification_Trustlist(UA_CertificateVerification *cv,
- const UA_ByteString *certificateTrustList,
- size_t certificateTrustListSize,
- const UA_ByteString *certificateIssuerList,
- size_t certificateIssuerListSize,
- const UA_ByteString *certificateRevocationList,
- size_t certificateRevocationListSize) {
- CertInfo *ci = (CertInfo*)UA_malloc(sizeof(CertInfo));
- if(!ci)
- return UA_STATUSCODE_BADOUTOFMEMORY;
- memset(ci, 0, sizeof(CertInfo));
- mbedtls_x509_crt_init(&ci->certificateTrustList);
- mbedtls_x509_crl_init(&ci->certificateRevocationList);
- mbedtls_x509_crt_init(&ci->certificateIssuerList);
- cv->context = (void*)ci;
- if(certificateTrustListSize > 0)
- cv->verifyCertificate = certificateVerification_verify;
- else
- cv->verifyCertificate = verifyCertificateAllowAll;
- cv->deleteMembers = certificateVerification_deleteMembers;
- cv->verifyApplicationURI = certificateVerification_verifyApplicationURI;
- int err = 0;
- for(size_t i = 0; i < certificateTrustListSize; i++) {
- err = mbedtls_x509_crt_parse(&ci->certificateTrustList,
- certificateTrustList[i].data,
- certificateTrustList[i].length);
- if(err)
- goto error;
- }
- for(size_t i = 0; i < certificateIssuerListSize; i++) {
- err = mbedtls_x509_crt_parse(&ci->certificateIssuerList,
- certificateIssuerList[i].data,
- certificateIssuerList[i].length);
- if(err)
- goto error;
- }
- for(size_t i = 0; i < certificateRevocationListSize; i++) {
- err = mbedtls_x509_crl_parse(&ci->certificateRevocationList,
- certificateRevocationList[i].data,
- certificateRevocationList[i].length);
- if(err)
- goto error;
- }
- return UA_STATUSCODE_GOOD;
- error:
- certificateVerification_deleteMembers(cv);
- return UA_STATUSCODE_BADINTERNALERROR;
- }
- #ifdef __linux__ /* Linux only so far */
- UA_StatusCode
- UA_CertificateVerification_CertFolders(UA_CertificateVerification *cv,
- const char *trustListFolder,
- const char *issuerListFolder,
- const char *revocationListFolder) {
- CertInfo *ci = (CertInfo*)UA_malloc(sizeof(CertInfo));
- if(!ci)
- return UA_STATUSCODE_BADOUTOFMEMORY;
- memset(ci, 0, sizeof(CertInfo));
- mbedtls_x509_crt_init(&ci->certificateTrustList);
- mbedtls_x509_crl_init(&ci->certificateRevocationList);
- mbedtls_x509_crt_init(&ci->certificateIssuerList);
- /* Only set the folder paths. They will be reloaded during runtime.
- * TODO: Add a more efficient reloading of only the changes */
- ci->trustListFolder = UA_STRING_ALLOC(trustListFolder);
- ci->issuerListFolder = UA_STRING_ALLOC(issuerListFolder);
- ci->revocationListFolder = UA_STRING_ALLOC(revocationListFolder);
- reloadCertificates(ci);
- cv->context = (void*)ci;
- cv->verifyCertificate = certificateVerification_verify;
- cv->deleteMembers = certificateVerification_deleteMembers;
- cv->verifyApplicationURI = certificateVerification_verifyApplicationURI;
- return UA_STATUSCODE_GOOD;
- }
- #endif
- #endif
|