]> git.saurik.com Git - apple/security.git/blobdiff - OSX/libsecurity_apple_x509_tp/lib/TPDatabase.cpp
Security-57336.1.9.tar.gz
[apple/security.git] / OSX / libsecurity_apple_x509_tp / lib / TPDatabase.cpp
diff --git a/OSX/libsecurity_apple_x509_tp/lib/TPDatabase.cpp b/OSX/libsecurity_apple_x509_tp/lib/TPDatabase.cpp
new file mode 100644 (file)
index 0000000..5ba632f
--- /dev/null
@@ -0,0 +1,569 @@
+/*
+ * Copyright (c) 2002-2009,2011-2012,2014 Apple Inc. All Rights Reserved.
+ *
+ * The contents of this file constitute Original Code as defined in and are
+ * subject to the Apple Public Source License Version 1.2 (the 'License').
+ * You may not use this file except in compliance with the License. Please obtain
+ * a copy of the License at http://www.apple.com/publicsource and read it before
+ * using this file.
+ *
+ * This Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS
+ * OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, INCLUDING WITHOUT
+ * LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ * PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. Please see the License for the
+ * specific language governing rights and limitations under the License.
+ */
+
+
+/*
+ * TPDatabase.cpp - TP's DL/DB access functions.
+ *
+ */
+
+#include <Security/cssmtype.h>
+#include <Security/cssmapi.h>
+#include <security_cdsa_utilities/Schema.h>            /* private API */
+#include <security_keychain/TrustKeychains.h>  /* private SecTrustKeychainsGetMutex() */
+#include <Security/SecCertificatePriv.h>               /* private SecInferLabelFromX509Name() */
+#include <Security/oidscert.h>
+#include "TPDatabase.h"
+#include "tpdebugging.h"
+#include "certGroupUtils.h"
+#include "TPCertInfo.h"
+#include "TPCrlInfo.h"
+#include "tpCrlVerify.h"
+#include "tpTime.h"
+
+
+/*
+ * Given a DL/DB, look up cert by subject name. Subsequent
+ * certs can be found using the returned result handle.
+ */
+static CSSM_DB_UNIQUE_RECORD_PTR tpCertLookup(
+       CSSM_DL_DB_HANDLE       dlDb,
+       const CSSM_DATA         *subjectName,   // DER-encoded
+       CSSM_HANDLE_PTR         resultHand,             // RETURNED
+       CSSM_DATA_PTR           cert)                   // RETURNED
+{
+       CSSM_QUERY                                              query;
+       CSSM_SELECTION_PREDICATE                predicate;
+       CSSM_DB_UNIQUE_RECORD_PTR               record = NULL;
+
+       cert->Data = NULL;
+       cert->Length = 0;
+
+       /* SWAG until cert schema nailed down */
+       predicate.DbOperator = CSSM_DB_EQUAL;
+       predicate.Attribute.Info.AttributeNameFormat =
+               CSSM_DB_ATTRIBUTE_NAME_AS_STRING;
+       predicate.Attribute.Info.Label.AttributeName = (char*) "Subject";
+       predicate.Attribute.Info.AttributeFormat = CSSM_DB_ATTRIBUTE_FORMAT_BLOB;
+       predicate.Attribute.Value = const_cast<CSSM_DATA_PTR>(subjectName);
+       predicate.Attribute.NumberOfValues = 1;
+
+       query.RecordType = CSSM_DL_DB_RECORD_X509_CERTIFICATE;
+       query.Conjunctive = CSSM_DB_NONE;
+       query.NumSelectionPredicates = 1;
+       query.SelectionPredicate = &predicate;
+       query.QueryLimits.TimeLimit = 0;        // FIXME - meaningful?
+       query.QueryLimits.SizeLimit = 1;        // FIXME - meaningful?
+       query.QueryFlags = 0;                           // FIXME - used?
+
+       CSSM_DL_DataGetFirst(dlDb,
+               &query,
+               resultHand,
+               NULL,                           // don't fetch attributes
+               cert,
+               &record);
+
+       return record;
+}
+
+/*
+ * Search a list of DBs for a cert which verifies specified subject item.
+ * Just a boolean return - we found it, or not. If we did, we return
+ * TPCertInfo associated with the raw cert.
+ * A true partialIssuerKey on return indicates that caller must deal
+ * with partial public key processing later.
+ * If verifyCurrent is true, we will not return a cert which is not
+ * temporally valid; else we may well do so.
+ */
+TPCertInfo *tpDbFindIssuerCert(
+       Allocator                               &alloc,
+       CSSM_CL_HANDLE                  clHand,
+       CSSM_CSP_HANDLE                 cspHand,
+       const TPClItemInfo              *subjectItem,
+       const CSSM_DL_DB_LIST   *dbList,
+       const char                              *verifyTime,            // may be NULL
+       bool                                    &partialIssuerKey,      // RETURNED
+    TPCertInfo              *oldRoot)
+{
+       StLock<Mutex> _(SecTrustKeychainsGetMutex());
+
+       uint32                                          dbDex;
+       CSSM_HANDLE                                     resultHand;
+       CSSM_DATA                                       cert;
+       CSSM_DL_DB_HANDLE                       dlDb;
+       CSSM_DB_UNIQUE_RECORD_PTR       record;
+       TPCertInfo                                      *issuerCert = NULL;
+       bool                                            foundIt;
+       TPCertInfo                                      *expiredIssuer = NULL;
+       TPCertInfo                                      *nonRootIssuer = NULL;
+
+       partialIssuerKey = false;
+       if(dbList == NULL) {
+               return NULL;
+       }
+       for(dbDex=0; dbDex<dbList->NumHandles; dbDex++) {
+               dlDb = dbList->DLDBHandle[dbDex];
+               cert.Data = NULL;
+               cert.Length = 0;
+               resultHand = 0;
+               record = tpCertLookup(dlDb,
+                       subjectItem->issuerName(),
+                       &resultHand,
+                       &cert);
+               /* remember we have to:
+                * -- abort this query regardless, and
+                * -- free the CSSM_DATA cert regardless, and
+                * -- free the unique record if we don't use it
+                *    (by placing it in issuerCert)...
+                */
+               if(record != NULL) {
+                       /* Found one */
+                       assert(cert.Data != NULL);
+                       tpDbDebug("tpDbFindIssuerCert: found cert record (1) %p", record);
+                       issuerCert = NULL;
+                       CSSM_RETURN crtn = CSSM_OK;
+                       try {
+                               issuerCert = new TPCertInfo(clHand, cspHand, &cert, TIC_CopyData, verifyTime);
+                       }
+                       catch(...) {
+                               crtn = CSSMERR_TP_INVALID_CERTIFICATE;
+                       }
+
+                       /* we're done with raw cert data */
+                       tpFreePluginMemory(dlDb.DLHandle, cert.Data);
+                       cert.Data = NULL;
+                       cert.Length = 0;
+
+                       /* Does it verify the subject cert? */
+                       if(crtn == CSSM_OK) {
+                               crtn = subjectItem->verifyWithIssuer(issuerCert);
+                       }
+
+                       /*
+                        * Handle temporal invalidity - if so and this is the first one
+                        * we've seen, hold on to it while we search for better one.
+                        */
+                       if((crtn == CSSM_OK) && (expiredIssuer == NULL)) {
+                               if(issuerCert->isExpired() || issuerCert->isNotValidYet()) {
+                                       /*
+                                        * Exact value not important here, this just uniquely identifies
+                                        * this situation in the switch below.
+                                        */
+                                       tpDbDebug("tpDbFindIssuerCert: holding expired cert (1)");
+                                       crtn = CSSM_CERT_STATUS_EXPIRED;
+                                       expiredIssuer = issuerCert;
+                                       expiredIssuer->dlDbHandle(dlDb);
+                                       expiredIssuer->uniqueRecord(record);
+                               }
+                       }
+                       /*
+                        * Prefer a root over an intermediate issuer if we can get one
+                        * (in case a cross-signed intermediate and root are both available)
+                        */
+                       if((crtn == CSSM_OK) && (nonRootIssuer == NULL)) {
+                               if(!issuerCert->isSelfSigned()) {
+                                       /*
+                                        * Exact value not important here, this just uniquely identifies
+                                        * this situation in the switch below.
+                                        */
+                                       tpDbDebug("tpDbFindIssuerCert: holding non-root cert (1)");
+                                       crtn = CSSM_CERT_STATUS_IS_ROOT;
+                                       nonRootIssuer = issuerCert;
+                                       nonRootIssuer->dlDbHandle(dlDb);
+                                       nonRootIssuer->uniqueRecord(record);
+                               }
+                       }
+                       switch(crtn) {
+                               case CSSMERR_CSP_APPLE_PUBLIC_KEY_INCOMPLETE:
+                                       partialIssuerKey = true;
+                                       break;
+                case CSSM_OK:
+                    if((oldRoot == NULL) ||
+                       !tp_CompareCerts(issuerCert->itemData(), oldRoot->itemData())) {
+                        /* We found a new root cert which does not match the old one */
+                        break;
+                    }
+                    /* else fall through to search for a different one */
+                               default:
+                                       if(issuerCert != NULL) {
+                                               /* either holding onto this cert, or done with it. */
+                                               if(crtn != CSSM_CERT_STATUS_EXPIRED &&
+                                                  crtn != CSSM_CERT_STATUS_IS_ROOT) {
+                                                       delete issuerCert;
+                                                       CSSM_DL_FreeUniqueRecord(dlDb, record);
+                                               }
+                                               issuerCert = NULL;
+                                       }
+
+                                       /*
+                                        * Continue searching this DB. Break on finding the holy
+                                        * grail or no more records found.
+                                        */
+                                       for(;;) {
+                                               cert.Data = NULL;
+                                               cert.Length = 0;
+                                               record = NULL;
+                                               CSSM_RETURN crtn = CSSM_DL_DataGetNext(dlDb,
+                                                       resultHand,
+                                                       NULL,           // no attrs
+                                                       &cert,
+                                                       &record);
+                                               if(crtn) {
+                                                       /* no more, done with this DB */
+                                                       assert(cert.Data == NULL);
+                                                       break;
+                                               }
+                                               assert(cert.Data != NULL);
+                                               tpDbDebug("tpDbFindIssuerCert: found cert record (2) %p", record);
+
+                                               /* found one - does it verify subject? */
+                                               try {
+                                                       issuerCert = new TPCertInfo(clHand, cspHand, &cert, TIC_CopyData,
+                                                                       verifyTime);
+                                               }
+                                               catch(...) {
+                                                       crtn = CSSMERR_TP_INVALID_CERTIFICATE;
+                                               }
+                                               /* we're done with raw cert data */
+                                               tpFreePluginMemory(dlDb.DLHandle, cert.Data);
+                                               cert.Data = NULL;
+                                               cert.Length = 0;
+
+                                               if(crtn == CSSM_OK) {
+                                                       crtn = subjectItem->verifyWithIssuer(issuerCert);
+                                               }
+
+                                               /* temporal validity check, again */
+                                               if((crtn == CSSM_OK) && (expiredIssuer == NULL)) {
+                                                       if(issuerCert->isExpired() || issuerCert->isNotValidYet()) {
+                                                               tpDbDebug("tpDbFindIssuerCert: holding expired cert (2)");
+                                                               crtn = CSSM_CERT_STATUS_EXPIRED;
+                                                               expiredIssuer = issuerCert;
+                                                               expiredIssuer->dlDbHandle(dlDb);
+                                                               expiredIssuer->uniqueRecord(record);
+                                                       }
+                                               }
+                                               /* self-signed check, again */
+                                               if((crtn == CSSM_OK) && (nonRootIssuer == NULL)) {
+                                                       if(!issuerCert->isSelfSigned()) {
+                                                               tpDbDebug("tpDbFindIssuerCert: holding non-root cert (2)");
+                                                               crtn = CSSM_CERT_STATUS_IS_ROOT;
+                                                               nonRootIssuer = issuerCert;
+                                                               nonRootIssuer->dlDbHandle(dlDb);
+                                                               nonRootIssuer->uniqueRecord(record);
+                                                       }
+                                               }
+
+                                               foundIt = false;
+                                               switch(crtn) {
+                                                       case CSSM_OK:
+                                /* duplicate check, again */
+                                if((oldRoot == NULL) ||
+                                   !tp_CompareCerts(issuerCert->itemData(), oldRoot->itemData())) {
+                                    foundIt = true;
+                                }
+                                break;
+                                                       case CSSMERR_CSP_APPLE_PUBLIC_KEY_INCOMPLETE:
+                                                               partialIssuerKey = true;
+                                                               foundIt = true;
+                                                               break;
+                                                       default:
+                                                               break;
+                                               }
+                                               if(foundIt) {
+                                                       /* yes! */
+                                                       break;
+                                               }
+                                               if(issuerCert != NULL) {
+                                                       /* either holding onto this cert, or done with it. */
+                                                       if(crtn != CSSM_CERT_STATUS_EXPIRED &&
+                                                          crtn != CSSM_CERT_STATUS_IS_ROOT) {
+                                                               delete issuerCert;
+                                                               CSSM_DL_FreeUniqueRecord(dlDb, record);
+                                                       }
+                                                       issuerCert = NULL;
+                                               }
+                                       } /* searching subsequent records */
+                       }       /* switch verify */
+
+                       if(record != NULL) {
+                               /* NULL record --> end of search --> DB auto-aborted */
+                               crtn = CSSM_DL_DataAbortQuery(dlDb, resultHand);
+                               assert(crtn == CSSM_OK);
+                       }
+                       if(issuerCert != NULL) {
+                               /* successful return */
+                               tpDbDebug("tpDbFindIssuer: returning record %p", record);
+                               issuerCert->dlDbHandle(dlDb);
+                               issuerCert->uniqueRecord(record);
+                               if(expiredIssuer != NULL) {
+                                       /* We found a replacement */
+                                       tpDbDebug("tpDbFindIssuer: discarding expired cert");
+                                       expiredIssuer->freeUniqueRecord();
+                                       delete expiredIssuer;
+                               }
+                               /* Avoid deleting the non-root cert if same as expired cert */
+                               if(nonRootIssuer != NULL && nonRootIssuer != expiredIssuer) {
+                                       /* We found a replacement */
+                                       tpDbDebug("tpDbFindIssuer: discarding non-root cert");
+                                       nonRootIssuer->freeUniqueRecord();
+                                       delete nonRootIssuer;
+                               }
+                               return issuerCert;
+                       }
+               }       /* tpCertLookup, i.e., CSSM_DL_DataGetFirst, succeeded */
+               else {
+                       assert(cert.Data == NULL);
+                       assert(resultHand == 0);
+               }
+       }       /* main loop searching dbList */
+
+       if(nonRootIssuer != NULL) {
+               /* didn't find root issuer, so use this one */
+               tpDbDebug("tpDbFindIssuer: taking non-root issuer cert, record %p",
+                       nonRootIssuer->uniqueRecord());
+               if(expiredIssuer != NULL && expiredIssuer != nonRootIssuer) {
+                       expiredIssuer->freeUniqueRecord();
+                       delete expiredIssuer;
+               }
+               return nonRootIssuer;
+       }
+
+       if(expiredIssuer != NULL) {
+               /* OK, we'll take this one */
+               tpDbDebug("tpDbFindIssuer: taking expired cert after all, record %p",
+                       expiredIssuer->uniqueRecord());
+               return expiredIssuer;
+       }
+       /* issuer not found */
+       return NULL;
+}
+
+/*
+ * Given a DL/DB, look up CRL by issuer name and validity time.
+ * Subsequent CRLs can be found using the returned result handle.
+ */
+#define SEARCH_BY_DATE         1
+
+static CSSM_DB_UNIQUE_RECORD_PTR tpCrlLookup(
+       CSSM_DL_DB_HANDLE       dlDb,
+       const CSSM_DATA         *issuerName,    // DER-encoded
+       CSSM_TIMESTRING         verifyTime,             // may be NULL, implies "now"
+       CSSM_HANDLE_PTR         resultHand,             // RETURNED
+       CSSM_DATA_PTR           crl)                    // RETURNED
+{
+       CSSM_QUERY                                              query;
+       CSSM_SELECTION_PREDICATE                pred[3];
+       CSSM_DB_UNIQUE_RECORD_PTR               record = NULL;
+       char                                                    timeStr[CSSM_TIME_STRLEN + 1];
+
+       crl->Data = NULL;
+       crl->Length = 0;
+
+       /* Three predicates...first, the issuer name */
+       pred[0].DbOperator = CSSM_DB_EQUAL;
+       pred[0].Attribute.Info.AttributeNameFormat =
+               CSSM_DB_ATTRIBUTE_NAME_AS_STRING;
+       pred[0].Attribute.Info.Label.AttributeName = (char*) "Issuer";
+       pred[0].Attribute.Info.AttributeFormat = CSSM_DB_ATTRIBUTE_FORMAT_BLOB;
+       pred[0].Attribute.Value = const_cast<CSSM_DATA_PTR>(issuerName);
+       pred[0].Attribute.NumberOfValues = 1;
+
+       /* now before/after. Cook up an appropriate time string. */
+       if(verifyTime != NULL) {
+               /* Caller spec'd tolerate any format */
+               int rtn = tpTimeToCssmTimestring(verifyTime, (unsigned)strlen(verifyTime), timeStr);
+               if(rtn) {
+                       tpErrorLog("tpCrlLookup: Invalid VerifyTime string\n");
+                       return NULL;
+               }
+       }
+       else {
+               /* right now */
+               StLock<Mutex> _(tpTimeLock());
+               timeAtNowPlus(0, TIME_CSSM, timeStr);
+       }
+       CSSM_DATA timeData;
+       timeData.Data = (uint8 *)timeStr;
+       timeData.Length = CSSM_TIME_STRLEN;
+
+       #if SEARCH_BY_DATE
+       pred[1].DbOperator = CSSM_DB_LESS_THAN;
+       pred[1].Attribute.Info.AttributeNameFormat = CSSM_DB_ATTRIBUTE_NAME_AS_STRING;
+       pred[1].Attribute.Info.Label.AttributeName = (char*) "NextUpdate";
+       pred[1].Attribute.Info.AttributeFormat = CSSM_DB_ATTRIBUTE_FORMAT_BLOB;
+       pred[1].Attribute.Value = &timeData;
+       pred[1].Attribute.NumberOfValues = 1;
+
+       pred[2].DbOperator = CSSM_DB_GREATER_THAN;
+       pred[2].Attribute.Info.AttributeNameFormat = CSSM_DB_ATTRIBUTE_NAME_AS_STRING;
+       pred[2].Attribute.Info.Label.AttributeName = (char*) "ThisUpdate";
+       pred[2].Attribute.Info.AttributeFormat = CSSM_DB_ATTRIBUTE_FORMAT_BLOB;
+       pred[2].Attribute.Value = &timeData;
+       pred[2].Attribute.NumberOfValues = 1;
+       #endif
+
+       query.RecordType = CSSM_DL_DB_RECORD_X509_CRL;
+       query.Conjunctive = CSSM_DB_AND;
+       #if SEARCH_BY_DATE
+       query.NumSelectionPredicates = 3;
+       #else
+       query.NumSelectionPredicates = 1;
+       #endif
+       query.SelectionPredicate = pred;
+       query.QueryLimits.TimeLimit = 0;        // FIXME - meaningful?
+       query.QueryLimits.SizeLimit = 1;        // FIXME - meaningful?
+       query.QueryFlags = 0;                           // FIXME - used?
+
+       CSSM_DL_DataGetFirst(dlDb,
+               &query,
+               resultHand,
+               NULL,                           // don't fetch attributes
+               crl,
+               &record);
+       return record;
+}
+
+/*
+ * Search a list of DBs for a CRL from the specified issuer and (optional)
+ * TPVerifyContext.verifyTime.
+ * Just a boolean return - we found it, or not. If we did, we return a
+ * TPCrlInfo which has been verified with the specified TPVerifyContext.
+ */
+TPCrlInfo *tpDbFindIssuerCrl(
+       TPVerifyContext         &vfyCtx,
+       const CSSM_DATA         &issuer,
+       TPCertInfo                      &forCert)
+{
+       StLock<Mutex> _(SecTrustKeychainsGetMutex());
+
+       uint32                                          dbDex;
+       CSSM_HANDLE                                     resultHand;
+       CSSM_DATA                                       crl;
+       CSSM_DL_DB_HANDLE                       dlDb;
+       CSSM_DB_UNIQUE_RECORD_PTR       record;
+       TPCrlInfo                                       *issuerCrl = NULL;
+       CSSM_DL_DB_LIST_PTR             dbList = vfyCtx.dbList;
+       CSSM_RETURN                                     crtn;
+
+       if(dbList == NULL) {
+               return NULL;
+       }
+       for(dbDex=0; dbDex<dbList->NumHandles; dbDex++) {
+               dlDb = dbList->DLDBHandle[dbDex];
+               crl.Data = NULL;
+               crl.Length = 0;
+               record = tpCrlLookup(dlDb,
+                       &issuer,
+                       vfyCtx.verifyTime,
+                       &resultHand,
+                       &crl);
+               /* remember we have to:
+                * -- abort this query regardless, and
+                * -- free the CSSM_DATA crl regardless, and
+                * -- free the unique record if we don't use it
+                *    (by placing it in issuerCert)...
+                */
+               if(record != NULL) {
+                       /* Found one */
+                       assert(crl.Data != NULL);
+                       issuerCrl = new TPCrlInfo(vfyCtx.clHand,
+                               vfyCtx.cspHand,
+                               &crl,
+                               TIC_CopyData,
+                               vfyCtx.verifyTime);
+                       /* we're done with raw CRL data */
+                       /* FIXME this assumes that vfyCtx.alloc is the same as the
+                        * allocator associated with DlDB...OK? */
+                       tpFreeCssmData(vfyCtx.alloc, &crl, CSSM_FALSE);
+                       crl.Data = NULL;
+                       crl.Length = 0;
+
+                       /* and we're done with the record */
+                       CSSM_DL_FreeUniqueRecord(dlDb, record);
+
+                       /* Does it verify with specified context? */
+                       crtn = issuerCrl->verifyWithContextNow(vfyCtx, &forCert);
+                       if(crtn) {
+
+                               delete issuerCrl;
+                               issuerCrl = NULL;
+
+                               /*
+                                * Verify fail. Continue searching this DB. Break on
+                                * finding the holy grail or no more records found.
+                                */
+                               for(;;) {
+                                       crl.Data = NULL;
+                                       crl.Length = 0;
+                                       crtn = CSSM_DL_DataGetNext(dlDb,
+                                               resultHand,
+                                               NULL,           // no attrs
+                                               &crl,
+                                               &record);
+                                       if(crtn) {
+                                               /* no more, done with this DB */
+                                               assert(crl.Data == NULL);
+                                               break;
+                                       }
+                                       assert(crl.Data != NULL);
+
+                                       /* found one - is it any good? */
+                                       issuerCrl = new TPCrlInfo(vfyCtx.clHand,
+                                               vfyCtx.cspHand,
+                                               &crl,
+                                               TIC_CopyData,
+                                               vfyCtx.verifyTime);
+                                       /* we're done with raw CRL data */
+                                       /* FIXME this assumes that vfyCtx.alloc is the same as the
+                                       * allocator associated with DlDB...OK? */
+                                       tpFreeCssmData(vfyCtx.alloc, &crl, CSSM_FALSE);
+                                       crl.Data = NULL;
+                                       crl.Length = 0;
+
+                                       CSSM_DL_FreeUniqueRecord(dlDb, record);
+
+                                       crtn = issuerCrl->verifyWithContextNow(vfyCtx, &forCert);
+                                       if(crtn == CSSM_OK) {
+                                               /* yes! */
+                                               break;
+                                       }
+                                       delete issuerCrl;
+                                       issuerCrl = NULL;
+                               } /* searching subsequent records */
+                       }       /* verify fail */
+                       /* else success! */
+
+                       if(issuerCrl != NULL) {
+                               /* successful return */
+                               CSSM_DL_DataAbortQuery(dlDb, resultHand);
+                               tpDebug("tpDbFindIssuerCrl: found CRL record %p", record);
+                               return issuerCrl;
+                       }
+               }       /* tpCrlLookup, i.e., CSSM_DL_DataGetFirst, succeeded */
+               else {
+                       assert(crl.Data == NULL);
+               }
+               /* in any case, abort the query for this db */
+               CSSM_DL_DataAbortQuery(dlDb, resultHand);
+
+       }       /* main loop searching dbList */
+
+       /* issuer not found */
+       return NULL;
+}
+