]> git.saurik.com Git - apple/security.git/blame - OSX/libsecurity_codesigning/lib/reqinterp.cpp
Security-57337.60.2.tar.gz
[apple/security.git] / OSX / libsecurity_codesigning / lib / reqinterp.cpp
CommitLineData
b1ab9ed8 1/*
d8f41ccd 2 * Copyright (c) 2006,2011-2014 Apple Inc. All Rights Reserved.
b1ab9ed8
A
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
11 * file.
12 *
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24//
25// reqinterp - Requirement language (exprOp) interpreter
26//
27#include "reqinterp.h"
28#include "codesigning_dtrace.h"
29#include <Security/SecTrustSettingsPriv.h>
30#include <Security/SecCertificatePriv.h>
31#include <security_utilities/memutils.h>
32#include <security_utilities/logging.h>
e3d460c9
A
33#include <sys/csr.h>
34#include <IOKit/IOKitLib.h>
35#include <IOKit/IOCFUnserialize.h>
b1ab9ed8
A
36#include "csutilities.h"
37
38namespace Security {
39namespace CodeSigning {
40
41
42//
43// Fragment fetching, caching, and evaluation.
44//
45// Several language elements allow "calling" of separate requirement programs
46// stored on disk as (binary) requirement blobs. The Fragments class takes care
47// of finding, loading, caching, and evaluating them.
48//
49// This is a singleton for (process global) caching. It works fine as multiple instances,
50// at a loss of caching effectiveness.
51//
52class Fragments {
53public:
54 Fragments();
55
56 bool named(const std::string &name, const Requirement::Context &ctx)
57 { return evalNamed("subreq", name, ctx); }
58 bool namedAnchor(const std::string &name, const Requirement::Context &ctx)
59 { return evalNamed("anchorreq", name, ctx); }
60
61private:
62 bool evalNamed(const char *type, const std::string &name, const Requirement::Context &ctx);
63 CFDataRef fragment(const char *type, const std::string &name);
64
65 typedef std::map<std::string, CFRef<CFDataRef> > FragMap;
66
67private:
68 CFBundleRef mMyBundle; // Security.framework bundle
69 Mutex mLock; // lock for all of the below...
70 FragMap mFragments; // cached fragments
71};
72
73static ModuleNexus<Fragments> fragments;
74
75
76//
77// Magic certificate features
78//
79static CFStringRef appleIntermediateCN = CFSTR("Apple Code Signing Certification Authority");
80static CFStringRef appleIntermediateO = CFSTR("Apple Inc.");
81
82
83//
84// Main interpreter function.
85//
86// ExprOp code is in Polish Notation (operator followed by operands),
87// and this engine uses opportunistic evaluation.
88//
89bool Requirement::Interpreter::evaluate()
5c19dc3a
A
90{ return eval(stackLimit); }
91
92bool Requirement::Interpreter::eval(int depth)
b1ab9ed8 93{
5c19dc3a
A
94 if (--depth <= 0) // nested too deeply - protect the stack
95 MacOSError::throwMe(errSecCSReqInvalid);
96
b1ab9ed8
A
97 ExprOp op = ExprOp(get<uint32_t>());
98 CODESIGN_EVAL_REQINT_OP(op, this->pc() - sizeof(uint32_t));
99 switch (op & ~opFlagMask) {
100 case opFalse:
101 return false;
102 case opTrue:
103 return true;
104 case opIdent:
105 return mContext->directory && getString() == mContext->directory->identifier();
106 case opAppleAnchor:
107 return appleSigned();
108 case opAppleGenericAnchor:
109 return appleAnchored();
110 case opAnchorHash:
111 {
112 SecCertificateRef cert = mContext->cert(get<int32_t>());
113 return verifyAnchor(cert, getSHA1());
114 }
115 case opInfoKeyValue: // [legacy; use opInfoKeyField]
116 {
117 string key = getString();
118 return infoKeyValue(key, Match(CFTempString(getString()), matchEqual));
119 }
120 case opAnd:
5c19dc3a 121 return eval(depth) & eval(depth);
b1ab9ed8 122 case opOr:
5c19dc3a 123 return eval(depth) | eval(depth);
b1ab9ed8
A
124 case opCDHash:
125 if (mContext->directory) {
5c19dc3a
A
126 CFRef<CFDataRef> cdhash = mContext->directory->cdhash();
127 CFRef<CFDataRef> required = getHash();
128 return CFEqual(cdhash, required);
b1ab9ed8
A
129 } else
130 return false;
131 case opNot:
5c19dc3a 132 return !eval(depth);
b1ab9ed8
A
133 case opInfoKeyField:
134 {
135 string key = getString();
136 Match match(*this);
137 return infoKeyValue(key, match);
138 }
139 case opEntitlementField:
140 {
141 string key = getString();
142 Match match(*this);
143 return entitlementValue(key, match);
144 }
145 case opCertField:
146 {
147 SecCertificateRef cert = mContext->cert(get<int32_t>());
148 string key = getString();
149 Match match(*this);
150 return certFieldValue(key, match, cert);
151 }
152 case opCertGeneric:
153 {
154 SecCertificateRef cert = mContext->cert(get<int32_t>());
155 string key = getString();
156 Match match(*this);
157 return certFieldGeneric(key, match, cert);
158 }
159 case opCertPolicy:
160 {
161 SecCertificateRef cert = mContext->cert(get<int32_t>());
162 string key = getString();
163 Match match(*this);
164 return certFieldPolicy(key, match, cert);
165 }
166 case opTrustedCert:
167 return trustedCert(get<int32_t>());
168 case opTrustedCerts:
169 return trustedCerts();
170 case opNamedAnchor:
171 return fragments().namedAnchor(getString(), *mContext);
172 case opNamedCode:
173 return fragments().named(getString(), *mContext);
5c19dc3a
A
174 case opPlatform:
175 {
176 int32_t targetPlatform = get<int32_t>();
177 return mContext->directory && mContext->directory->platform == targetPlatform;
178 }
b1ab9ed8
A
179 default:
180 // opcode not recognized - handle generically if possible, fail otherwise
181 if (op & (opGenericFalse | opGenericSkip)) {
182 // unknown opcode, but it has a size field and can be safely bypassed
183 skip(get<uint32_t>());
184 if (op & opGenericFalse) {
185 CODESIGN_EVAL_REQINT_UNKNOWN_FALSE(op);
186 return false;
187 } else {
188 CODESIGN_EVAL_REQINT_UNKNOWN_SKIPPED(op);
5c19dc3a 189 return eval(depth);
b1ab9ed8
A
190 }
191 }
192 // unrecognized opcode and no way to interpret it
193 secdebug("csinterp", "opcode 0x%x cannot be handled; aborting", op);
194 MacOSError::throwMe(errSecCSUnimplemented);
195 }
196}
197
198
199//
200// Evaluate an Info.plist key condition
201//
202bool Requirement::Interpreter::infoKeyValue(const string &key, const Match &match)
203{
204 if (mContext->info) // we have an Info.plist
205 if (CFTypeRef value = CFDictionaryGetValue(mContext->info, CFTempString(key)))
206 return match(value);
207 return false;
208}
209
210
211//
212// Evaluate an entitlement condition
213//
214bool Requirement::Interpreter::entitlementValue(const string &key, const Match &match)
215{
216 if (mContext->entitlements) // we have an Info.plist
217 if (CFTypeRef value = CFDictionaryGetValue(mContext->entitlements, CFTempString(key)))
218 return match(value);
219 return false;
220}
221
222
223bool Requirement::Interpreter::certFieldValue(const string &key, const Match &match, SecCertificateRef cert)
224{
225 // no cert, no chance
226 if (cert == NULL)
227 return false;
228
229 // a table of recognized keys for the "certificate[foo]" syntax
230 static const struct CertField {
231 const char *name;
232 const CSSM_OID *oid;
233 } certFields[] = {
234 { "subject.C", &CSSMOID_CountryName },
235 { "subject.CN", &CSSMOID_CommonName },
236 { "subject.D", &CSSMOID_Description },
237 { "subject.L", &CSSMOID_LocalityName },
238// { "subject.C-L", &CSSMOID_CollectiveLocalityName }, // missing from Security.framework headers
239 { "subject.O", &CSSMOID_OrganizationName },
240 { "subject.C-O", &CSSMOID_CollectiveOrganizationName },
241 { "subject.OU", &CSSMOID_OrganizationalUnitName },
242 { "subject.C-OU", &CSSMOID_CollectiveOrganizationalUnitName },
243 { "subject.ST", &CSSMOID_StateProvinceName },
244 { "subject.C-ST", &CSSMOID_CollectiveStateProvinceName },
245 { "subject.STREET", &CSSMOID_StreetAddress },
246 { "subject.C-STREET", &CSSMOID_CollectiveStreetAddress },
247 { "subject.UID", &CSSMOID_UserID },
248 { NULL, NULL }
249 };
250
251 // DN-component single-value match
252 for (const CertField *cf = certFields; cf->name; cf++)
253 if (cf->name == key) {
254 CFRef<CFStringRef> value;
255 if (OSStatus rc = SecCertificateCopySubjectComponent(cert, cf->oid, &value.aref())) {
256 secdebug("csinterp", "cert %p lookup for DN.%s failed rc=%d", cert, key.c_str(), (int)rc);
257 return false;
258 }
259 return match(value);
260 }
261
262 // email multi-valued match (any of...)
263 if (key == "email") {
264 CFRef<CFArrayRef> value;
265 if (OSStatus rc = SecCertificateCopyEmailAddresses(cert, &value.aref())) {
266 secdebug("csinterp", "cert %p lookup for email failed rc=%d", cert, (int)rc);
267 return false;
268 }
269 return match(value);
270 }
271
272 // unrecognized key. Fail but do not abort to promote backward compatibility down the road
273 secdebug("csinterp", "cert field notation \"%s\" not understood", key.c_str());
274 return false;
275}
276
277
278bool Requirement::Interpreter::certFieldGeneric(const string &key, const Match &match, SecCertificateRef cert)
279{
280 // the key is actually a (binary) OID value
281 CssmOid oid((char *)key.data(), key.length());
282 return certFieldGeneric(oid, match, cert);
283}
284
285bool Requirement::Interpreter::certFieldGeneric(const CssmOid &oid, const Match &match, SecCertificateRef cert)
286{
287 return cert && certificateHasField(cert, oid) && match(kCFBooleanTrue);
288}
289
290bool Requirement::Interpreter::certFieldPolicy(const string &key, const Match &match, SecCertificateRef cert)
291{
292 // the key is actually a (binary) OID value
293 CssmOid oid((char *)key.data(), key.length());
294 return certFieldPolicy(oid, match, cert);
295}
296
297bool Requirement::Interpreter::certFieldPolicy(const CssmOid &oid, const Match &match, SecCertificateRef cert)
298{
299 return cert && certificateHasPolicy(cert, oid) && match(kCFBooleanTrue);
300}
301
302
303//
304// Check the Apple-signed condition
305//
306bool Requirement::Interpreter::appleAnchored()
307{
308 if (SecCertificateRef cert = mContext->cert(anchorCert))
e3d460c9 309 if (isAppleCA(cert))
b1ab9ed8
A
310 return true;
311 return false;
312}
313
e3d460c9
A
314static CFStringRef kAMFINVRAMTrustedKeys = CFSTR("AMFITrustedKeys");
315
316CFArrayRef Requirement::Interpreter::getAdditionalTrustedAnchors()
317{
318 __block CFRef<CFMutableArrayRef> keys = makeCFMutableArray(0);
319
320 try {
321 io_registry_entry_t entry = IORegistryEntryFromPath(kIOMasterPortDefault, "IODeviceTree:/options");
322 if (entry == IO_OBJECT_NULL)
323 return NULL;
324
325 CFRef<CFDataRef> configData = (CFDataRef)IORegistryEntryCreateCFProperty(entry, kAMFINVRAMTrustedKeys, kCFAllocatorDefault, 0);
326 IOObjectRelease(entry);
327 if (!configData)
328 return NULL;
329
330 CFRef<CFDictionaryRef> configDict = CFDictionaryRef(IOCFUnserialize((const char *)CFDataGetBytePtr(configData), kCFAllocatorDefault, 0, NULL));
331 if (!configDict)
332 return NULL;
333
334 CFArrayRef trustedKeys = CFArrayRef(CFDictionaryGetValue(configDict, CFSTR("trustedKeys")));
335 if (!trustedKeys && CFGetTypeID(trustedKeys) != CFArrayGetTypeID())
336 return NULL;
337
338 cfArrayApplyBlock(trustedKeys, ^(const void *value) {
339 CFDictionaryRef key = CFDictionaryRef(value);
340 if (!key && CFGetTypeID(key) != CFDictionaryGetTypeID())
341 return;
342
343 CFDataRef hash = CFDataRef(CFDictionaryGetValue(key, CFSTR("certDigest")));
344 if (!hash && CFGetTypeID(hash) != CFDataGetTypeID())
345 return;
346 CFArrayAppendValue(keys, hash);
347 });
348
349 } catch (...) {
350 }
351
352 if (CFArrayGetCount(keys) == 0)
353 return NULL;
354
355 return keys.yield();
356}
357
358bool Requirement::Interpreter::appleLocalAnchored()
359{
360 static CFArrayRef additionalTrustedCertificates = NULL;
361
362 if (csr_check(CSR_ALLOW_APPLE_INTERNAL))
363 return false;
364
365 static dispatch_once_t onceToken;
366 dispatch_once(&onceToken, ^{
367 additionalTrustedCertificates = getAdditionalTrustedAnchors();
368 });
369
370 if (additionalTrustedCertificates == NULL)
371 return false;
372
373 CFRef<CFDataRef> hash = SecCertificateCopySHA256Digest(mContext->cert(leafCert));
374 if (!hash)
375 return false;
376
377 if (CFArrayContainsValue(additionalTrustedCertificates, CFRangeMake(0, CFArrayGetCount(additionalTrustedCertificates)), hash))
378 return true;
379
380 return false;
381}
382
b1ab9ed8
A
383bool Requirement::Interpreter::appleSigned()
384{
e3d460c9 385 if (appleAnchored()) {
b1ab9ed8
A
386 if (SecCertificateRef intermed = mContext->cert(-2)) // first intermediate
387 // first intermediate common name match (exact)
388 if (certFieldValue("subject.CN", Match(appleIntermediateCN, matchEqual), intermed)
389 && certFieldValue("subject.O", Match(appleIntermediateO, matchEqual), intermed))
390 return true;
e3d460c9
A
391 } else if (appleLocalAnchored()) {
392 return true;
393 }
b1ab9ed8
A
394 return false;
395}
396
397
398//
399// Verify an anchor requirement against the context
400//
401bool Requirement::Interpreter::verifyAnchor(SecCertificateRef cert, const unsigned char *digest)
402{
403 // get certificate bytes
404 if (cert) {
405 CSSM_DATA certData;
406 MacOSError::check(SecCertificateGetData(cert, &certData));
407
408 // verify hash
b1ab9ed8
A
409 SHA1 hasher;
410 hasher(certData.Data, certData.Length);
411 return hasher.verify(digest);
412 }
413 return false;
414}
415
416
417//
418// Check one or all certificate(s) in the cert chain against the Trust Settings database.
419//
420bool Requirement::Interpreter::trustedCerts()
421{
422 int anchor = mContext->certCount() - 1;
423 for (int slot = 0; slot <= anchor; slot++)
424 if (SecCertificateRef cert = mContext->cert(slot))
425 switch (trustSetting(cert, slot == anchor)) {
426 case kSecTrustSettingsResultTrustRoot:
427 case kSecTrustSettingsResultTrustAsRoot:
428 return true;
429 case kSecTrustSettingsResultDeny:
430 return false;
431 case kSecTrustSettingsResultUnspecified:
432 break;
433 default:
434 assert(false);
435 return false;
436 }
437 else
438 return false;
439 return false;
440}
441
442bool Requirement::Interpreter::trustedCert(int slot)
443{
444 if (SecCertificateRef cert = mContext->cert(slot)) {
445 int anchorSlot = mContext->certCount() - 1;
446 switch (trustSetting(cert, slot == anchorCert || slot == anchorSlot)) {
447 case kSecTrustSettingsResultTrustRoot:
448 case kSecTrustSettingsResultTrustAsRoot:
449 return true;
450 case kSecTrustSettingsResultDeny:
451 case kSecTrustSettingsResultUnspecified:
452 return false;
453 default:
454 assert(false);
455 return false;
456 }
457 } else
458 return false;
459}
460
461
462//
463// Explicitly check one certificate against the Trust Settings database and report
464// the findings. This is a helper for the various Trust Settings evaluators.
465//
466SecTrustSettingsResult Requirement::Interpreter::trustSetting(SecCertificateRef cert, bool isAnchor)
467{
468 // the SPI input is the uppercase hex form of the SHA-1 of the certificate...
469 assert(cert);
470 SHA1::Digest digest;
471 hashOfCertificate(cert, digest);
472 string Certhex = CssmData(digest, sizeof(digest)).toHex();
473 for (string::iterator it = Certhex.begin(); it != Certhex.end(); ++it)
474 if (islower(*it))
475 *it = toupper(*it);
476
477 // call Trust Settings and see what it finds
478 SecTrustSettingsDomain domain;
479 SecTrustSettingsResult result;
480 CSSM_RETURN *errors = NULL;
481 uint32 errorCount = 0;
482 bool foundMatch, foundAny;
483 switch (OSStatus rc = SecTrustSettingsEvaluateCert(
484 CFTempString(Certhex), // settings index
485 &CSSMOID_APPLE_TP_CODE_SIGNING, // standard code signing policy
486 NULL, 0, // policy string (unused)
487 kSecTrustSettingsKeyUseAny, // no restriction on key usage @@@
488 isAnchor, // consult system default anchor set
489
490 &domain, // domain of found setting
491 &errors, &errorCount, // error set and maximum count
492 &result, // the actual setting
493 &foundMatch, &foundAny // optimization hints (not used)
494 )) {
427c49bc 495 case errSecSuccess:
b1ab9ed8
A
496 ::free(errors);
497 if (foundMatch)
498 return result;
499 else
500 return kSecTrustSettingsResultUnspecified;
501 default:
502 ::free(errors);
503 MacOSError::throwMe(rc);
504 }
505}
506
507
508//
509// Create a Match object from the interpreter stream
510//
511Requirement::Interpreter::Match::Match(Interpreter &interp)
512{
513 switch (mOp = interp.get<MatchOperation>()) {
514 case matchExists:
515 break;
516 case matchEqual:
517 case matchContains:
518 case matchBeginsWith:
519 case matchEndsWith:
520 case matchLessThan:
521 case matchGreaterThan:
522 case matchLessEqual:
523 case matchGreaterEqual:
524 mValue.take(makeCFString(interp.getString()));
525 break;
526 default:
527 // Assume this (unknown) match type has a single data argument.
528 // This gives us a chance to keep the instruction stream aligned.
529 interp.getString(); // discard
530 break;
531 }
532}
533
534
535//
536// Execute a match against a candidate value
537//
538bool Requirement::Interpreter::Match::operator () (CFTypeRef candidate) const
539{
540 // null candidates always fail
541 if (!candidate)
542 return false;
543
544 // interpret an array as matching alternatives (any one succeeds)
545 if (CFGetTypeID(candidate) == CFArrayGetTypeID()) {
546 CFArrayRef array = CFArrayRef(candidate);
547 CFIndex count = CFArrayGetCount(array);
548 for (CFIndex n = 0; n < count; n++)
549 if ((*this)(CFArrayGetValueAtIndex(array, n))) // yes, it's recursive
550 return true;
551 }
552
553 switch (mOp) {
554 case matchExists: // anything but NULL and boolean false "exists"
555 return !CFEqual(candidate, kCFBooleanFalse);
556 case matchEqual: // equality works for all CF types
557 return CFEqual(candidate, mValue);
558 case matchContains:
559 if (CFGetTypeID(candidate) == CFStringGetTypeID()) {
560 CFStringRef value = CFStringRef(candidate);
561 if (CFStringFindWithOptions(value, mValue, CFRangeMake(0, CFStringGetLength(value)), 0, NULL))
562 return true;
563 }
564 return false;
565 case matchBeginsWith:
566 if (CFGetTypeID(candidate) == CFStringGetTypeID()) {
567 CFStringRef value = CFStringRef(candidate);
568 if (CFStringFindWithOptions(value, mValue, CFRangeMake(0, CFStringGetLength(mValue)), 0, NULL))
569 return true;
570 }
571 return false;
572 case matchEndsWith:
573 if (CFGetTypeID(candidate) == CFStringGetTypeID()) {
574 CFStringRef value = CFStringRef(candidate);
575 CFIndex matchLength = CFStringGetLength(mValue);
576 CFIndex start = CFStringGetLength(value) - matchLength;
577 if (start >= 0)
578 if (CFStringFindWithOptions(value, mValue, CFRangeMake(start, matchLength), 0, NULL))
579 return true;
580 }
581 return false;
582 case matchLessThan:
583 return inequality(candidate, kCFCompareNumerically, kCFCompareLessThan, true);
584 case matchGreaterThan:
585 return inequality(candidate, kCFCompareNumerically, kCFCompareGreaterThan, true);
586 case matchLessEqual:
587 return inequality(candidate, kCFCompareNumerically, kCFCompareGreaterThan, false);
588 case matchGreaterEqual:
589 return inequality(candidate, kCFCompareNumerically, kCFCompareLessThan, false);
590 default:
591 // unrecognized match types can never match
592 return false;
593 }
594}
595
596
597bool Requirement::Interpreter::Match::inequality(CFTypeRef candidate, CFStringCompareFlags flags,
598 CFComparisonResult outcome, bool negate) const
599{
600 if (CFGetTypeID(candidate) == CFStringGetTypeID()) {
601 CFStringRef value = CFStringRef(candidate);
602 if ((CFStringCompare(value, mValue, flags) == outcome) == negate)
603 return true;
604 }
605 return false;
606}
607
608
609//
610// External fragments
611//
612Fragments::Fragments()
613{
614 mMyBundle = CFBundleGetBundleWithIdentifier(CFSTR("com.apple.security"));
615}
616
617
618bool Fragments::evalNamed(const char *type, const std::string &name, const Requirement::Context &ctx)
619{
620 if (CFDataRef fragData = fragment(type, name)) {
621 const Requirement *req = (const Requirement *)CFDataGetBytePtr(fragData); // was prevalidated as Requirement
622 return req->validates(ctx);
623 }
624 return false;
625}
626
627
628CFDataRef Fragments::fragment(const char *type, const std::string &name)
629{
630 string key = name + "!!" + type; // compound key
631 StLock<Mutex> _(mLock); // lock for cache access
632 FragMap::const_iterator it = mFragments.find(key);
633 if (it == mFragments.end()) {
634 CFRef<CFDataRef> fragData; // will always be set (NULL on any errors)
635 if (CFRef<CFURLRef> fragURL = CFBundleCopyResourceURL(mMyBundle, CFTempString(name), CFSTR("csreq"), CFTempString(type)))
636 if (CFRef<CFDataRef> data = cfLoadFile(fragURL)) { // got data
637 const Requirement *req = (const Requirement *)CFDataGetBytePtr(data);
638 if (req->validateBlob(CFDataGetLength(data))) // looks like a Requirement...
639 fragData = data; // ... so accept it
640 else
641 Syslog::warning("Invalid sub-requirement at %s", cfString(fragURL).c_str());
642 }
643 if (CODESIGN_EVAL_REQINT_FRAGMENT_LOAD_ENABLED())
644 CODESIGN_EVAL_REQINT_FRAGMENT_LOAD(type, name.c_str(), fragData ? CFDataGetBytePtr(fragData) : NULL);
645 mFragments[key] = fragData; // cache it, success or failure
646 return fragData;
647 }
648 CODESIGN_EVAL_REQINT_FRAGMENT_HIT(type, name.c_str());
649 return it->second;
650}
651
652
653} // CodeSigning
654} // Security