]>
Commit | Line | Data |
---|---|---|
b1ab9ed8 | 1 | /* |
d8f41ccd | 2 | * Copyright (c) 2006-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 | #include "bundlediskrep.h" | |
24 | #include "filediskrep.h" | |
80e23899 | 25 | #include "dirscanner.h" |
79b9da22 | 26 | #include "notarization.h" |
d87e1158 | 27 | #include <CoreFoundation/CFBundlePriv.h> |
b1ab9ed8 A |
28 | #include <CoreFoundation/CFURLAccess.h> |
29 | #include <CoreFoundation/CFBundlePriv.h> | |
30 | #include <security_utilities/cfmunge.h> | |
31 | #include <copyfile.h> | |
32 | #include <fts.h> | |
d8f41ccd | 33 | #include <sstream> |
b1ab9ed8 A |
34 | |
35 | namespace Security { | |
36 | namespace CodeSigning { | |
37 | ||
38 | using namespace UnixPlusPlus; | |
39 | ||
40 | ||
41 | // | |
42 | // Local helpers | |
43 | // | |
44 | static std::string findDistFile(const std::string &directory); | |
45 | ||
46 | ||
47 | // | |
48 | // We make a CFBundleRef immediately, but everything else is lazy | |
49 | // | |
50 | BundleDiskRep::BundleDiskRep(const char *path, const Context *ctx) | |
79b9da22 | 51 | : mBundle(_CFBundleCreateUnique(NULL, CFTempURL(path))), forcePlatform(false) |
b1ab9ed8 A |
52 | { |
53 | if (!mBundle) | |
54 | MacOSError::throwMe(errSecCSBadBundleFormat); | |
55 | setup(ctx); | |
79b9da22 | 56 | forcePlatform = mExecRep->appleInternalForcePlatform(); |
b1ab9ed8 A |
57 | CODESIGN_DISKREP_CREATE_BUNDLE_PATH(this, (char*)path, (void*)ctx, mExecRep); |
58 | } | |
59 | ||
60 | BundleDiskRep::BundleDiskRep(CFBundleRef ref, const Context *ctx) | |
61 | { | |
62 | mBundle = ref; // retains | |
63 | setup(ctx); | |
79b9da22 | 64 | forcePlatform = mExecRep->appleInternalForcePlatform(); |
b1ab9ed8 A |
65 | CODESIGN_DISKREP_CREATE_BUNDLE_REF(this, ref, (void*)ctx, mExecRep); |
66 | } | |
67 | ||
427c49bc A |
68 | BundleDiskRep::~BundleDiskRep() |
69 | { | |
70 | } | |
d87e1158 A |
71 | |
72 | void BundleDiskRep::checkMoved(CFURLRef oldPath, CFURLRef newPath) | |
73 | { | |
74 | char cOld[PATH_MAX]; | |
75 | char cNew[PATH_MAX]; | |
76 | // The realpath call is important because alot of Framework bundles have a symlink | |
77 | // to their "Current" version binary in the main bundle | |
78 | if (realpath(cfString(oldPath).c_str(), cOld) == NULL || | |
79 | realpath(cfString(newPath).c_str(), cNew) == NULL) | |
fa7225c8 | 80 | MacOSError::throwMe(errSecCSAmbiguousBundleFormat); |
d87e1158 A |
81 | |
82 | if (strcmp(cOld, cNew) != 0) | |
83 | recordStrictError(errSecCSAmbiguousBundleFormat); | |
84 | } | |
427c49bc | 85 | |
b1ab9ed8 A |
86 | // common construction code |
87 | void BundleDiskRep::setup(const Context *ctx) | |
88 | { | |
fa7225c8 | 89 | mComponentsFromExecValid = false; // not yet known |
b1ab9ed8 | 90 | mInstallerPackage = false; // default |
e3d460c9 A |
91 | mAppLike = false; // pessimism first |
92 | bool appDisqualified = false; // found reason to disqualify as app | |
79b9da22 | 93 | |
80e23899 A |
94 | // capture the path of the main executable before descending into a specific version |
95 | CFRef<CFURLRef> mainExecBefore = CFBundleCopyExecutableURL(mBundle); | |
d87e1158 | 96 | CFRef<CFURLRef> infoPlistBefore = _CFBundleCopyInfoPlistURL(mBundle); |
80e23899 A |
97 | |
98 | // validate the bundle root; fish around for the desired framework version | |
99 | string root = cfStringRelease(copyCanonicalPath()); | |
fa7225c8 A |
100 | if (filehasExtendedAttribute(root, XATTR_FINDERINFO_NAME)) |
101 | recordStrictError(errSecCSInvalidAssociatedFileData); | |
80e23899 | 102 | string contents = root + "/Contents"; |
d87e1158 | 103 | string supportFiles = root + "/Support Files"; |
80e23899 | 104 | string version = root + "/Versions/" |
b1ab9ed8 A |
105 | + ((ctx && ctx->version) ? ctx->version : "Current") |
106 | + "/."; | |
80e23899 A |
107 | if (::access(contents.c_str(), F_OK) == 0) { // not shallow |
108 | DirValidator val; | |
109 | val.require("^Contents$", DirValidator::directory); // duh | |
110 | val.allow("^(\\.LSOverride|\\.DS_Store|Icon\r|\\.SoftwareDepot\\.tracking)$", DirValidator::file | DirValidator::noexec); | |
111 | try { | |
112 | val.validate(root, errSecCSUnsealedAppRoot); | |
113 | } catch (const MacOSError &err) { | |
114 | recordStrictError(err.error); | |
115 | } | |
d87e1158 A |
116 | } else if (::access(supportFiles.c_str(), F_OK) == 0) { // ancient legacy boondoggle bundle |
117 | // treat like a shallow bundle; do not allow Versions arbitration | |
e3d460c9 | 118 | appDisqualified = true; |
80e23899 | 119 | } else if (::access(version.c_str(), F_OK) == 0) { // versioned bundle |
fa7225c8 | 120 | if (CFBundleRef versionBundle = _CFBundleCreateUnique(NULL, CFTempURL(version))) |
b1ab9ed8 A |
121 | mBundle.take(versionBundle); // replace top bundle ref |
122 | else | |
123 | MacOSError::throwMe(errSecCSStaticCodeNotFound); | |
e3d460c9 | 124 | appDisqualified = true; |
80e23899 | 125 | validateFrameworkRoot(root); |
b1ab9ed8 A |
126 | } else { |
127 | if (ctx && ctx->version) // explicitly specified | |
128 | MacOSError::throwMe(errSecCSStaticCodeNotFound); | |
129 | } | |
80e23899 | 130 | |
b1ab9ed8 A |
131 | CFDictionaryRef infoDict = CFBundleGetInfoDictionary(mBundle); |
132 | assert(infoDict); // CFBundle will always make one up for us | |
133 | CFTypeRef mainHTML = CFDictionaryGetValue(infoDict, CFSTR("MainHTML")); | |
134 | CFTypeRef packageVersion = CFDictionaryGetValue(infoDict, CFSTR("IFMajorVersion")); | |
135 | ||
136 | // conventional executable bundle: CFBundle identifies an executable for us | |
137 | if (CFRef<CFURLRef> mainExec = CFBundleCopyExecutableURL(mBundle)) // if CFBundle claims an executable... | |
138 | if (mainHTML == NULL) { // ... and it's not a widget | |
80e23899 A |
139 | |
140 | // Note that this check is skipped if there is a specific framework version checked. | |
141 | // That's because you know what you are doing if you are looking at a specific version. | |
142 | // This check is designed to stop someone who did a verification on an app root, from mistakenly | |
143 | // verifying a framework | |
d87e1158 A |
144 | if (!ctx || !ctx->version) { |
145 | if (mainExecBefore) | |
146 | checkMoved(mainExecBefore, mainExec); | |
147 | if (infoPlistBefore) | |
148 | if (CFRef<CFURLRef> infoDictPath = _CFBundleCopyInfoPlistURL(mBundle)) | |
149 | checkMoved(infoPlistBefore, infoDictPath); | |
80e23899 A |
150 | } |
151 | ||
b1ab9ed8 A |
152 | mMainExecutableURL = mainExec; |
153 | mExecRep = DiskRep::bestFileGuess(this->mainExecutablePath(), ctx); | |
fa7225c8 | 154 | checkPlainFile(mExecRep->fd(), this->mainExecutablePath()); |
e3d460c9 A |
155 | CFDictionaryRef infoDict = CFBundleGetInfoDictionary(mBundle); |
156 | bool isAppBundle = false; | |
157 | if (infoDict) | |
158 | if (CFTypeRef packageType = CFDictionaryGetValue(infoDict, CFSTR("CFBundlePackageType"))) | |
159 | if (CFEqual(packageType, CFSTR("APPL"))) | |
160 | isAppBundle = true; | |
b1ab9ed8 | 161 | mFormat = "bundle with " + mExecRep->format(); |
e3d460c9 A |
162 | if (isAppBundle) |
163 | mFormat = "app " + mFormat; | |
164 | mAppLike = isAppBundle && !appDisqualified; | |
b1ab9ed8 A |
165 | return; |
166 | } | |
167 | ||
168 | // widget | |
169 | if (mainHTML) { | |
170 | if (CFGetTypeID(mainHTML) != CFStringGetTypeID()) | |
171 | MacOSError::throwMe(errSecCSBadBundleFormat); | |
172 | mMainExecutableURL.take(makeCFURL(cfString(CFStringRef(mainHTML)), false, | |
173 | CFRef<CFURLRef>(CFBundleCopySupportFilesDirectoryURL(mBundle)))); | |
174 | if (!mMainExecutableURL) | |
175 | MacOSError::throwMe(errSecCSBadBundleFormat); | |
176 | mExecRep = new FileDiskRep(this->mainExecutablePath().c_str()); | |
fa7225c8 | 177 | checkPlainFile(mExecRep->fd(), this->mainExecutablePath()); |
b1ab9ed8 | 178 | mFormat = "widget bundle"; |
e3d460c9 | 179 | mAppLike = true; |
b1ab9ed8 A |
180 | return; |
181 | } | |
182 | ||
183 | // do we have a real Info.plist here? | |
184 | if (CFRef<CFURLRef> infoURL = _CFBundleCopyInfoPlistURL(mBundle)) { | |
185 | // focus on the Info.plist (which we know exists) as the nominal "main executable" file | |
427c49bc A |
186 | mMainExecutableURL = infoURL; |
187 | mExecRep = new FileDiskRep(this->mainExecutablePath().c_str()); | |
fa7225c8 | 188 | checkPlainFile(mExecRep->fd(), this->mainExecutablePath()); |
427c49bc A |
189 | if (packageVersion) { |
190 | mInstallerPackage = true; | |
191 | mFormat = "installer package bundle"; | |
192 | } else { | |
193 | mFormat = "bundle"; | |
b1ab9ed8 | 194 | } |
427c49bc | 195 | return; |
b1ab9ed8 A |
196 | } |
197 | ||
198 | // we're getting desperate here. Perhaps an oldish-style installer package? Look for a *.dist file | |
199 | std::string distFile = findDistFile(this->resourcesRootPath()); | |
200 | if (!distFile.empty()) { | |
201 | mMainExecutableURL = makeCFURL(distFile); | |
202 | mExecRep = new FileDiskRep(this->mainExecutablePath().c_str()); | |
fa7225c8 | 203 | checkPlainFile(mExecRep->fd(), this->mainExecutablePath()); |
b1ab9ed8 A |
204 | mInstallerPackage = true; |
205 | mFormat = "installer package bundle"; | |
206 | return; | |
207 | } | |
208 | ||
209 | // this bundle cannot be signed | |
210 | MacOSError::throwMe(errSecCSBadBundleFormat); | |
211 | } | |
212 | ||
213 | ||
214 | // | |
215 | // Return the full path to the one-and-only file named something.dist in a directory. | |
216 | // Return empty string if none; throw an exception if multiple. Do not descend into subdirectories. | |
217 | // | |
218 | static std::string findDistFile(const std::string &directory) | |
219 | { | |
220 | std::string found; | |
221 | char *paths[] = {(char *)directory.c_str(), NULL}; | |
222 | FTS *fts = fts_open(paths, FTS_PHYSICAL | FTS_NOCHDIR | FTS_NOSTAT, NULL); | |
223 | bool root = true; | |
224 | while (FTSENT *ent = fts_read(fts)) { | |
225 | switch (ent->fts_info) { | |
226 | case FTS_F: | |
227 | case FTS_NSOK: | |
228 | if (!strcmp(ent->fts_path + ent->fts_pathlen - 5, ".dist")) { // found plain file foo.dist | |
229 | if (found.empty()) // first found | |
230 | found = ent->fts_path; | |
231 | else // multiple *.dist files (bad) | |
232 | MacOSError::throwMe(errSecCSBadBundleFormat); | |
233 | } | |
234 | break; | |
235 | case FTS_D: | |
236 | if (!root) | |
237 | fts_set(fts, ent, FTS_SKIP); // don't descend | |
238 | root = false; | |
239 | break; | |
240 | default: | |
241 | break; | |
242 | } | |
243 | } | |
244 | fts_close(fts); | |
245 | return found; | |
246 | } | |
247 | ||
248 | ||
b1ab9ed8 A |
249 | // |
250 | // Try to create the meta-file directory in our bundle. | |
251 | // Does nothing if the directory already exists. | |
252 | // Throws if an error occurs. | |
253 | // | |
254 | void BundleDiskRep::createMeta() | |
255 | { | |
fa7225c8 | 256 | string meta = metaPath(NULL); |
b1ab9ed8 A |
257 | if (!mMetaExists) { |
258 | if (::mkdir(meta.c_str(), 0755) == 0) { | |
80e23899 | 259 | copyfile(cfStringRelease(copyCanonicalPath()).c_str(), meta.c_str(), NULL, COPYFILE_SECURITY); |
b1ab9ed8 A |
260 | mMetaPath = meta; |
261 | mMetaExists = true; | |
262 | } else if (errno != EEXIST) | |
263 | UnixError::throwMe(); | |
264 | } | |
265 | } | |
fa7225c8 A |
266 | |
267 | ||
268 | // | |
269 | // Create a path to a bundle signing resource, by name. | |
270 | // This is in the BUNDLEDISKREP_DIRECTORY directory in the bundle's support directory. | |
271 | // | |
272 | string BundleDiskRep::metaPath(const char *name) | |
273 | { | |
274 | if (mMetaPath.empty()) { | |
275 | string support = cfStringRelease(CFBundleCopySupportFilesDirectoryURL(mBundle)); | |
276 | mMetaPath = support + "/" BUNDLEDISKREP_DIRECTORY; | |
277 | mMetaExists = ::access(mMetaPath.c_str(), F_OK) == 0; | |
278 | } | |
279 | if (name) | |
280 | return mMetaPath + "/" + name; | |
281 | else | |
282 | return mMetaPath; | |
283 | } | |
284 | ||
285 | CFDataRef BundleDiskRep::metaData(const char *name) | |
286 | { | |
6b200bc3 A |
287 | if (CFRef<CFURLRef> url = makeCFURL(metaPath(name))) { |
288 | return cfLoadFile(url); | |
289 | } else { | |
290 | secnotice("bundlediskrep", "no metapath for %s", name); | |
291 | return NULL; | |
292 | } | |
fa7225c8 | 293 | } |
b1ab9ed8 | 294 | |
fa7225c8 A |
295 | CFDataRef BundleDiskRep::metaData(CodeDirectory::SpecialSlot slot) |
296 | { | |
297 | if (const char *name = CodeDirectory::canonicalSlotName(slot)) | |
298 | return metaData(name); | |
299 | else | |
300 | return NULL; | |
301 | } | |
302 | ||
303 | ||
304 | ||
80e23899 A |
305 | // |
306 | // Load's a CFURL and makes sure that it is a regular file and not a symlink (or fifo, etc.) | |
307 | // | |
308 | CFDataRef BundleDiskRep::loadRegularFile(CFURLRef url) | |
309 | { | |
310 | assert(url); | |
311 | ||
312 | CFDataRef data = NULL; | |
313 | ||
314 | std::string path(cfString(url)); | |
315 | ||
316 | AutoFileDesc fd(path); | |
317 | ||
fa7225c8 | 318 | checkPlainFile(fd, path); |
80e23899 A |
319 | |
320 | data = cfLoadFile(fd, fd.fileSize()); | |
321 | ||
322 | if (!data) { | |
fa7225c8 A |
323 | secinfo("bundlediskrep", "failed to load %s", cfString(url).c_str()); |
324 | MacOSError::throwMe(errSecCSInvalidSymlink); | |
80e23899 A |
325 | } |
326 | ||
327 | return data; | |
328 | } | |
b1ab9ed8 A |
329 | |
330 | // | |
331 | // Load and return a component, by slot number. | |
332 | // Info.plist components come from the bundle, always (we don't look | |
333 | // for Mach-O embedded versions). | |
fa7225c8 | 334 | // ResourceDirectory always comes from bundle files. |
b1ab9ed8 | 335 | // Everything else comes from the embedded blobs of a Mach-O image, or from |
fa7225c8 A |
336 | // files located in the Contents directory of the bundle; but we must be consistent |
337 | // (no half-and-half situations). | |
b1ab9ed8 A |
338 | // |
339 | CFDataRef BundleDiskRep::component(CodeDirectory::SpecialSlot slot) | |
340 | { | |
341 | switch (slot) { | |
342 | // the Info.plist comes from the magic CFBundle-indicated place and ONLY from there | |
343 | case cdInfoSlot: | |
344 | if (CFRef<CFURLRef> info = _CFBundleCopyInfoPlistURL(mBundle)) | |
80e23899 | 345 | return loadRegularFile(info); |
b1ab9ed8 A |
346 | else |
347 | return NULL; | |
b1ab9ed8 | 348 | case cdResourceDirSlot: |
fa7225c8 A |
349 | mUsedComponents.insert(slot); |
350 | return metaData(slot); | |
351 | // by default, we take components from the executable image or files (but not both) | |
352 | default: | |
353 | if (CFRef<CFDataRef> data = mExecRep->component(slot)) { | |
354 | componentFromExec(true); | |
355 | return data.yield(); | |
356 | } | |
357 | if (CFRef<CFDataRef> data = metaData(slot)) { | |
358 | componentFromExec(false); | |
359 | mUsedComponents.insert(slot); | |
360 | return data.yield(); | |
361 | } | |
362 | return NULL; | |
363 | } | |
364 | } | |
365 | ||
366 | ||
367 | // Check that all components of this BundleDiskRep come from either the main | |
368 | // executable or the _CodeSignature directory (not mix-and-match). | |
369 | void BundleDiskRep::componentFromExec(bool fromExec) | |
370 | { | |
371 | if (!mComponentsFromExecValid) { | |
372 | // first use; set latch | |
373 | mComponentsFromExecValid = true; | |
374 | mComponentsFromExec = fromExec; | |
375 | } else if (mComponentsFromExec != fromExec) { | |
376 | // subsequent use: check latch | |
377 | MacOSError::throwMe(errSecCSSignatureFailed); | |
b1ab9ed8 A |
378 | } |
379 | } | |
380 | ||
381 | ||
382 | // | |
383 | // The binary identifier is taken directly from the main executable. | |
384 | // | |
385 | CFDataRef BundleDiskRep::identification() | |
386 | { | |
387 | return mExecRep->identification(); | |
388 | } | |
389 | ||
390 | ||
391 | // | |
392 | // Various aspects of our DiskRep personality. | |
393 | // | |
80e23899 | 394 | CFURLRef BundleDiskRep::copyCanonicalPath() |
b1ab9ed8 | 395 | { |
427c49bc A |
396 | if (CFURLRef url = CFBundleCopyBundleURL(mBundle)) |
397 | return url; | |
398 | CFError::throwMe(); | |
b1ab9ed8 A |
399 | } |
400 | ||
401 | string BundleDiskRep::mainExecutablePath() | |
402 | { | |
403 | return cfString(mMainExecutableURL); | |
404 | } | |
405 | ||
406 | string BundleDiskRep::resourcesRootPath() | |
407 | { | |
408 | return cfStringRelease(CFBundleCopySupportFilesDirectoryURL(mBundle)); | |
409 | } | |
410 | ||
411 | void BundleDiskRep::adjustResources(ResourceBuilder &builder) | |
412 | { | |
413 | // exclude entire contents of meta directory | |
427c49bc A |
414 | builder.addExclusion("^" BUNDLEDISKREP_DIRECTORY "$"); |
415 | builder.addExclusion("^" CODERESOURCES_LINK "$"); // ancient-ish symlink into it | |
b1ab9ed8 A |
416 | |
417 | // exclude the store manifest directory | |
427c49bc | 418 | builder.addExclusion("^" STORE_RECEIPT_DIRECTORY "$"); |
b1ab9ed8 A |
419 | |
420 | // exclude the main executable file | |
421 | string resources = resourcesRootPath(); | |
427c49bc A |
422 | if (resources.compare(resources.size() - 2, 2, "/.") == 0) // chop trailing /. |
423 | resources = resources.substr(0, resources.size()-2); | |
b1ab9ed8 | 424 | string executable = mainExecutablePath(); |
427c49bc A |
425 | if (!executable.compare(0, resources.length(), resources, 0, resources.length()) |
426 | && executable[resources.length()] == '/') // is proper directory prefix | |
b1ab9ed8 | 427 | builder.addExclusion(string("^") |
5c19dc3a | 428 | + ResourceBuilder::escapeRE(executable.substr(resources.length()+1)) + "$", ResourceBuilder::softTarget); |
b1ab9ed8 A |
429 | } |
430 | ||
431 | ||
432 | ||
433 | Universal *BundleDiskRep::mainExecutableImage() | |
434 | { | |
435 | return mExecRep->mainExecutableImage(); | |
436 | } | |
437 | ||
e3d460c9 A |
438 | void BundleDiskRep::prepareForSigning(SigningContext &context) |
439 | { | |
440 | return mExecRep->prepareForSigning(context); | |
441 | } | |
442 | ||
b1ab9ed8 A |
443 | size_t BundleDiskRep::signingBase() |
444 | { | |
445 | return mExecRep->signingBase(); | |
446 | } | |
447 | ||
448 | size_t BundleDiskRep::signingLimit() | |
449 | { | |
450 | return mExecRep->signingLimit(); | |
451 | } | |
452 | ||
866f8763 A |
453 | size_t BundleDiskRep::execSegBase(const Architecture *arch) |
454 | { | |
455 | return mExecRep->execSegBase(arch); | |
456 | } | |
457 | ||
458 | size_t BundleDiskRep::execSegLimit(const Architecture *arch) | |
459 | { | |
460 | return mExecRep->execSegLimit(arch); | |
461 | } | |
462 | ||
b1ab9ed8 A |
463 | string BundleDiskRep::format() |
464 | { | |
465 | return mFormat; | |
466 | } | |
467 | ||
468 | CFArrayRef BundleDiskRep::modifiedFiles() | |
469 | { | |
6b200bc3 A |
470 | CFRef<CFArrayRef> execFiles = mExecRep->modifiedFiles(); |
471 | CFRef<CFMutableArrayRef> files = CFArrayCreateMutableCopy(NULL, 0, execFiles); | |
b1ab9ed8 A |
472 | checkModifiedFile(files, cdCodeDirectorySlot); |
473 | checkModifiedFile(files, cdSignatureSlot); | |
474 | checkModifiedFile(files, cdResourceDirSlot); | |
e3d460c9 | 475 | checkModifiedFile(files, cdTopDirectorySlot); |
b1ab9ed8 | 476 | checkModifiedFile(files, cdEntitlementSlot); |
90dc47c2 | 477 | checkModifiedFile(files, cdEntitlementDERSlot); |
e3d460c9 A |
478 | checkModifiedFile(files, cdRepSpecificSlot); |
479 | for (CodeDirectory::Slot slot = cdAlternateCodeDirectorySlots; slot < cdAlternateCodeDirectoryLimit; ++slot) | |
480 | checkModifiedFile(files, slot); | |
6b200bc3 | 481 | return files.yield(); |
b1ab9ed8 A |
482 | } |
483 | ||
484 | void BundleDiskRep::checkModifiedFile(CFMutableArrayRef files, CodeDirectory::SpecialSlot slot) | |
485 | { | |
486 | if (CFDataRef data = mExecRep->component(slot)) // provided by executable file | |
487 | CFRelease(data); | |
488 | else if (const char *resourceName = CodeDirectory::canonicalSlotName(slot)) { | |
489 | string file = metaPath(resourceName); | |
490 | if (::access(file.c_str(), F_OK) == 0) | |
491 | CFArrayAppendValue(files, CFTempURL(file)); | |
492 | } | |
493 | } | |
494 | ||
495 | FileDesc &BundleDiskRep::fd() | |
496 | { | |
497 | return mExecRep->fd(); | |
498 | } | |
499 | ||
500 | void BundleDiskRep::flush() | |
501 | { | |
502 | mExecRep->flush(); | |
503 | } | |
504 | ||
fa7225c8 A |
505 | CFDictionaryRef BundleDiskRep::diskRepInformation() |
506 | { | |
507 | return mExecRep->diskRepInformation(); | |
508 | } | |
b1ab9ed8 A |
509 | |
510 | // | |
511 | // Defaults for signing operations | |
512 | // | |
513 | string BundleDiskRep::recommendedIdentifier(const SigningContext &) | |
514 | { | |
515 | if (CFStringRef identifier = CFBundleGetIdentifier(mBundle)) | |
516 | return cfString(identifier); | |
517 | if (CFDictionaryRef infoDict = CFBundleGetInfoDictionary(mBundle)) | |
518 | if (CFStringRef identifier = CFStringRef(CFDictionaryGetValue(infoDict, kCFBundleNameKey))) | |
519 | return cfString(identifier); | |
520 | ||
521 | // fall back to using the canonical path | |
80e23899 | 522 | return canonicalIdentifier(cfStringRelease(this->copyCanonicalPath())); |
b1ab9ed8 A |
523 | } |
524 | ||
80e23899 | 525 | string BundleDiskRep::resourcesRelativePath() |
b1ab9ed8 | 526 | { |
427c49bc | 527 | // figure out the resource directory base. Clean up some gunk inserted by CFBundle in frameworks |
b1ab9ed8 | 528 | string rbase = this->resourcesRootPath(); |
427c49bc A |
529 | size_t pos = rbase.find("/./"); // gratuitously inserted by CFBundle in some frameworks |
530 | while (pos != std::string::npos) { | |
531 | rbase = rbase.replace(pos, 2, "", 0); | |
532 | pos = rbase.find("/./"); | |
533 | } | |
b1ab9ed8 A |
534 | if (rbase.substr(rbase.length()-2, 2) == "/.") // produced by versioned bundle implicit "Current" case |
535 | rbase = rbase.substr(0, rbase.length()-2); // ... so take it off for this | |
427c49bc A |
536 | |
537 | // find the resources directory relative to the resource base | |
b1ab9ed8 A |
538 | string resources = cfStringRelease(CFBundleCopyResourcesDirectoryURL(mBundle)); |
539 | if (resources == rbase) | |
540 | resources = ""; | |
541 | else if (resources.compare(0, rbase.length(), rbase, 0, rbase.length()) != 0) // Resources not in resource root | |
542 | MacOSError::throwMe(errSecCSBadBundleFormat); | |
543 | else | |
544 | resources = resources.substr(rbase.length() + 1) + "/"; // differential path segment | |
545 | ||
80e23899 A |
546 | return resources; |
547 | } | |
548 | ||
549 | CFDictionaryRef BundleDiskRep::defaultResourceRules(const SigningContext &ctx) | |
550 | { | |
551 | string resources = this->resourcesRelativePath(); | |
552 | ||
b1ab9ed8 A |
553 | // installer package rules |
554 | if (mInstallerPackage) | |
555 | return cfmake<CFDictionaryRef>("{rules={" | |
556 | "'^.*' = #T" // include everything, but... | |
557 | "%s = {optional=#T, weight=1000}" // make localizations optional | |
558 | "'^.*/.*\\.pkg/' = {omit=#T, weight=10000}" // and exclude all nested packages (by name) | |
559 | "}}", | |
560 | (string("^") + resources + ".*\\.lproj/").c_str() | |
561 | ); | |
562 | ||
427c49bc A |
563 | // old (V1) executable bundle rules - compatible with before |
564 | if (ctx.signingFlags() & kSecCSSignV1) // *** must be exactly the same as before *** | |
565 | return cfmake<CFDictionaryRef>("{rules={" | |
566 | "'^version.plist$' = #T" // include version.plist | |
567 | "%s = #T" // include Resources | |
568 | "%s = {optional=#T, weight=1000}" // make localizations optional | |
569 | "%s = {omit=#T, weight=1100}" // exclude all locversion.plist files | |
570 | "}}", | |
571 | (string("^") + resources).c_str(), | |
572 | (string("^") + resources + ".*\\.lproj/").c_str(), | |
573 | (string("^") + resources + ".*\\.lproj/locversion.plist$").c_str() | |
574 | ); | |
575 | ||
576 | // FMJ (everything is a resource) rules | |
577 | if (ctx.signingFlags() & kSecCSSignOpaque) // Full Metal Jacket - everything is a resource file | |
578 | return cfmake<CFDictionaryRef>("{rules={" | |
579 | "'^.*' = #T" // everything is a resource | |
580 | "'^Info\\.plist$' = {omit=#T,weight=10}" // explicitly exclude this for backward compatibility | |
581 | "}}"); | |
79b9da22 A |
582 | |
583 | // new (V2) executable bundle rules | |
584 | if (!resources.empty()) { | |
585 | return cfmake<CFDictionaryRef>("{" // *** the new (V2) world *** | |
586 | "rules={" // old (V1; legacy) version | |
587 | "'^version.plist$' = #T" // include version.plist | |
588 | "%s = #T" // include Resources | |
589 | "%s = {optional=#T, weight=1000}" // make localizations optional | |
590 | "%s = {weight=1010}" // ... except for Base.lproj which really isn't optional at all | |
591 | "%s = {omit=#T, weight=1100}" // exclude all locversion.plist files | |
592 | "},rules2={" | |
593 | "'^.*' = #T" // include everything as a resource, with the following exceptions | |
594 | "'^[^/]+$' = {nested=#T, weight=10}" // files directly in Contents | |
595 | "'^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/' = {nested=#T, weight=10}" // dynamic repositories | |
596 | "'.*\\.dSYM($|/)' = {weight=11}" // but allow dSYM directories in code locations (parallel to their code) | |
597 | "'^(.*/)?\\.DS_Store$' = {omit=#T,weight=2000}" // ignore .DS_Store files | |
598 | "'^Info\\.plist$' = {omit=#T, weight=20}" // excluded automatically now, but old systems need to be told | |
599 | "'^version\\.plist$' = {weight=20}" // include version.plist as resource | |
600 | "'^embedded\\.provisionprofile$' = {weight=20}" // include embedded.provisionprofile as resource | |
601 | "'^PkgInfo$' = {omit=#T, weight=20}" // traditionally not included | |
602 | "%s = {weight=20}" // Resources override default nested (widgets) | |
603 | "%s = {optional=#T, weight=1000}" // make localizations optional | |
604 | "%s = {weight=1010}" // ... except for Base.lproj which really isn't optional at all | |
605 | "%s = {omit=#T, weight=1100}" // exclude all locversion.plist files | |
606 | "}}", | |
607 | ||
608 | (string("^") + resources).c_str(), | |
609 | (string("^") + resources + ".*\\.lproj/").c_str(), | |
610 | (string("^") + resources + "Base\\.lproj/").c_str(), | |
611 | (string("^") + resources + ".*\\.lproj/locversion.plist$").c_str(), | |
612 | ||
613 | (string("^") + resources).c_str(), | |
614 | (string("^") + resources + ".*\\.lproj/").c_str(), | |
615 | (string("^") + resources + "Base\\.lproj/").c_str(), | |
616 | (string("^") + resources + ".*\\.lproj/locversion.plist$").c_str() | |
617 | ); | |
618 | } else { | |
619 | /* This is a bundle format without a Resources directory, which means we need to omit | |
620 | * Resources-directory specific rules that would create conflicts. */ | |
621 | ||
622 | /* We also declare that flat bundles do not use any nested code rules. | |
623 | * Embedded, where flat bundles are used, currently does not support them, | |
624 | * and we have no plans for allowing the replacement of nested code there. | |
625 | * Should anyone actually intend to use nested code rules in a flat | |
626 | * bundle, they are free to create their own rules. */ | |
627 | ||
628 | return cfmake<CFDictionaryRef>("{" // *** the new (V2) world *** | |
629 | "rules={" // old (V1; legacy) version | |
630 | "'^version.plist$' = #T" // include version.plist | |
631 | "'^.*' = #T" // include Resources | |
632 | "'^.*\\.lproj/' = {optional=#T, weight=1000}" // make localizations optional | |
633 | "'^Base\\.lproj/' = {weight=1010}" // ... except for Base.lproj which really isn't optional at all | |
634 | "'^.*\\.lproj/locversion.plist$' = {omit=#T, weight=1100}" // exclude all locversion.plist files | |
635 | "},rules2={" | |
636 | "'^.*' = #T" // include everything as a resource, with the following exceptions | |
637 | "'.*\\.dSYM($|/)' = {weight=11}" // but allow dSYM directories in code locations (parallel to their code) | |
638 | "'^(.*/)?\\.DS_Store$' = {omit=#T,weight=2000}" // ignore .DS_Store files | |
639 | "'^Info\\.plist$' = {omit=#T, weight=20}" // excluded automatically now, but old systems need to be told | |
640 | "'^version\\.plist$' = {weight=20}" // include version.plist as resource | |
641 | "'^embedded\\.provisionprofile$' = {weight=20}" // include embedded.provisionprofile as resource | |
642 | "'^PkgInfo$' = {omit=#T, weight=20}" // traditionally not included | |
643 | "'^.*\\.lproj/' = {optional=#T, weight=1000}" // make localizations optional | |
644 | "'^Base\\.lproj/' = {weight=1010}" // ... except for Base.lproj which really isn't optional at all | |
645 | "'^.*\\.lproj/locversion.plist$' = {omit=#T, weight=1100}" // exclude all locversion.plist files | |
646 | "}}" | |
647 | ); | |
648 | } | |
b1ab9ed8 A |
649 | } |
650 | ||
80e23899 A |
651 | |
652 | CFArrayRef BundleDiskRep::allowedResourceOmissions() | |
653 | { | |
654 | return cfmake<CFArrayRef>("[" | |
655 | "'^(.*/)?\\.DS_Store$'" | |
656 | "'^Info\\.plist$'" | |
657 | "'^PkgInfo$'" | |
658 | "%s" | |
659 | "]", | |
660 | (string("^") + this->resourcesRelativePath() + ".*\\.lproj/locversion.plist$").c_str() | |
661 | ); | |
662 | } | |
663 | ||
664 | ||
b1ab9ed8 A |
665 | const Requirements *BundleDiskRep::defaultRequirements(const Architecture *arch, const SigningContext &ctx) |
666 | { | |
667 | return mExecRep->defaultRequirements(arch, ctx); | |
668 | } | |
669 | ||
670 | size_t BundleDiskRep::pageSize(const SigningContext &ctx) | |
671 | { | |
672 | return mExecRep->pageSize(ctx); | |
673 | } | |
674 | ||
675 | ||
80e23899 A |
676 | // |
677 | // Strict validation. | |
678 | // Takes an array of CFNumbers of errors to tolerate. | |
679 | // | |
e3d460c9 | 680 | void BundleDiskRep::strictValidate(const CodeDirectory* cd, const ToleratedErrors& tolerated, SecCSFlags flags) |
80e23899 | 681 | { |
fa7225c8 A |
682 | // scan our metadirectory (_CodeSignature) for unwanted guests |
683 | if (!(flags & kSecCSQuickCheck)) | |
684 | validateMetaDirectory(cd); | |
685 | ||
686 | // check accumulated strict errors and report them | |
687 | if (!(flags & kSecCSRestrictSidebandData)) // tolerate resource forks etc. | |
688 | mStrictErrors.erase(errSecCSInvalidAssociatedFileData); | |
689 | ||
80e23899 A |
690 | std::vector<OSStatus> fatalErrors; |
691 | set_difference(mStrictErrors.begin(), mStrictErrors.end(), tolerated.begin(), tolerated.end(), back_inserter(fatalErrors)); | |
692 | if (!fatalErrors.empty()) | |
693 | MacOSError::throwMe(fatalErrors[0]); | |
e3d460c9 A |
694 | |
695 | // if app focus is requested and this doesn't look like an app, fail - but allow whitelist overrides | |
696 | if (flags & kSecCSRestrictToAppLike) | |
697 | if (!mAppLike) | |
698 | if (tolerated.find(kSecCSRestrictToAppLike) == tolerated.end()) | |
699 | MacOSError::throwMe(errSecCSNotAppLike); | |
700 | ||
701 | // now strict-check the main executable (which won't be an app-like object) | |
702 | mExecRep->strictValidate(cd, tolerated, flags & ~kSecCSRestrictToAppLike); | |
80e23899 A |
703 | } |
704 | ||
705 | void BundleDiskRep::recordStrictError(OSStatus error) | |
706 | { | |
707 | mStrictErrors.insert(error); | |
708 | } | |
709 | ||
710 | ||
fa7225c8 A |
711 | void BundleDiskRep::validateMetaDirectory(const CodeDirectory* cd) |
712 | { | |
713 | // we know the resource directory will be checked after this call, so we'll give it a pass here | |
714 | if (cd->slotIsPresent(-cdResourceDirSlot)) | |
715 | mUsedComponents.insert(cdResourceDirSlot); | |
716 | ||
717 | // make a set of allowed (regular) filenames in this directory | |
718 | std::set<std::string> allowedFiles; | |
719 | for (auto it = mUsedComponents.begin(); it != mUsedComponents.end(); ++it) { | |
720 | switch (*it) { | |
721 | case cdInfoSlot: | |
722 | break; // always from Info.plist, not from here | |
723 | default: | |
724 | if (const char *name = CodeDirectory::canonicalSlotName(*it)) { | |
725 | allowedFiles.insert(name); | |
726 | } | |
727 | break; | |
728 | } | |
729 | } | |
730 | DirScanner scan(mMetaPath); | |
731 | if (scan.initialized()) { | |
732 | while (struct dirent* ent = scan.getNext()) { | |
733 | if (!scan.isRegularFile(ent)) | |
734 | MacOSError::throwMe(errSecCSUnsealedAppRoot); // only regular files allowed | |
735 | if (allowedFiles.find(ent->d_name) == allowedFiles.end()) { // not in expected set of files | |
736 | if (strcmp(ent->d_name, kSecCS_SIGNATUREFILE) == 0) { | |
737 | // special case - might be empty and unused (adhoc signature) | |
738 | AutoFileDesc fd(metaPath(kSecCS_SIGNATUREFILE)); | |
739 | if (fd.fileSize() == 0) | |
740 | continue; // that's okay, then | |
741 | } | |
742 | // not on list of needed files; it's a freeloading rogue! | |
743 | recordStrictError(errSecCSUnsealedAppRoot); // funnel through strict set so GKOpaque can override it | |
744 | } | |
745 | } | |
746 | } | |
747 | } | |
748 | ||
749 | ||
80e23899 A |
750 | // |
751 | // Check framework root for unsafe symlinks and unsealed content. | |
752 | // | |
753 | void BundleDiskRep::validateFrameworkRoot(string root) | |
754 | { | |
755 | // build regex element that matches either the "Current" symlink, or the name of the current version | |
756 | string current = "Current"; | |
757 | char currentVersion[PATH_MAX]; | |
758 | ssize_t len = ::readlink((root + "/Versions/Current").c_str(), currentVersion, sizeof(currentVersion)-1); | |
759 | if (len > 0) { | |
760 | currentVersion[len] = '\0'; | |
761 | current = string("(Current|") + ResourceBuilder::escapeRE(currentVersion) + ")"; | |
762 | } | |
763 | ||
764 | DirValidator val; | |
765 | val.require("^Versions$", DirValidator::directory | DirValidator::descend); // descend into Versions directory | |
766 | val.require("^Versions/[^/]+$", DirValidator::directory); // require at least one version | |
767 | val.require("^Versions/Current$", DirValidator::symlink, // require Current symlink... | |
768 | "^(\\./)?(\\.\\.[^/]+|\\.?[^\\./][^/]*)$"); // ...must point to a version | |
769 | val.allow("^(Versions/)?\\.DS_Store$", DirValidator::file | DirValidator::noexec); // allow .DS_Store files | |
770 | val.allow("^[^/]+$", DirValidator::symlink, ^ string (const string &name, const string &target) { | |
771 | // top-level symlinks must point to namesake in current version | |
772 | return string("^(\\./)?Versions/") + current + "/" + ResourceBuilder::escapeRE(name) + "$"; | |
773 | }); | |
774 | // module.map must be regular non-executable file, or symlink to module.map in current version | |
775 | val.allow("^module\\.map$", DirValidator::file | DirValidator::noexec | DirValidator::symlink, | |
776 | string("^(\\./)?Versions/") + current + "/module\\.map$"); | |
777 | ||
778 | try { | |
779 | val.validate(root, errSecCSUnsealedFrameworkRoot); | |
780 | } catch (const MacOSError &err) { | |
781 | recordStrictError(err.error); | |
782 | } | |
783 | } | |
784 | ||
fa7225c8 A |
785 | |
786 | // | |
787 | // Check a file descriptor for harmlessness. This is a strict check (only). | |
788 | // | |
789 | void BundleDiskRep::checkPlainFile(FileDesc fd, const std::string& path) | |
790 | { | |
791 | if (!fd.isPlainFile(path)) | |
792 | recordStrictError(errSecCSRegularFile); | |
793 | checkForks(fd); | |
794 | } | |
795 | ||
796 | void BundleDiskRep::checkForks(FileDesc fd) | |
797 | { | |
798 | if (fd.hasExtendedAttribute(XATTR_RESOURCEFORK_NAME) || fd.hasExtendedAttribute(XATTR_FINDERINFO_NAME)) | |
799 | recordStrictError(errSecCSInvalidAssociatedFileData); | |
800 | } | |
801 | ||
80e23899 | 802 | |
b1ab9ed8 A |
803 | // |
804 | // Writers | |
805 | // | |
806 | DiskRep::Writer *BundleDiskRep::writer() | |
807 | { | |
808 | return new Writer(this); | |
809 | } | |
810 | ||
811 | BundleDiskRep::Writer::Writer(BundleDiskRep *r) | |
812 | : rep(r), mMadeMetaDirectory(false) | |
813 | { | |
814 | execWriter = rep->mExecRep->writer(); | |
815 | } | |
816 | ||
817 | ||
818 | // | |
819 | // Write a component. | |
820 | // Note that this isn't concerned with Mach-O writing; this is handled at | |
821 | // a much higher level. If we're called, we write to a file in the Bundle's meta directory. | |
822 | // | |
823 | void BundleDiskRep::Writer::component(CodeDirectory::SpecialSlot slot, CFDataRef data) | |
824 | { | |
825 | switch (slot) { | |
826 | default: | |
827 | if (!execWriter->attribute(writerLastResort)) // willing to take the data... | |
828 | return execWriter->component(slot, data); // ... so hand it through | |
829 | // execWriter doesn't want the data; store it as a resource file (below) | |
830 | case cdResourceDirSlot: | |
831 | // the resource directory always goes into a bundle file | |
832 | if (const char *name = CodeDirectory::canonicalSlotName(slot)) { | |
833 | rep->createMeta(); | |
834 | string path = rep->metaPath(name); | |
79b9da22 A |
835 | |
836 | #if TARGET_OS_OSX | |
837 | // determine AFSC status if we are told to preserve compression | |
838 | bool conductCompression = false; | |
839 | cmpInfo cInfo; | |
840 | if (this->getPreserveAFSC()) { | |
841 | struct stat statBuffer; | |
842 | if (stat(path.c_str(), &statBuffer) == 0) { | |
843 | if (queryCompressionInfo(path.c_str(), &cInfo) == 0) { | |
844 | if (cInfo.compressionType != 0 && cInfo.compressedSize > 0) { | |
845 | conductCompression = true; | |
846 | } | |
847 | } | |
848 | } | |
849 | } | |
850 | #endif | |
851 | ||
b1ab9ed8 A |
852 | AutoFileDesc fd(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); |
853 | fd.writeAll(CFDataGetBytePtr(data), CFDataGetLength(data)); | |
79b9da22 A |
854 | fd.close(); |
855 | ||
856 | #if TARGET_OS_OSX | |
857 | // if the original file was compressed, compress the new file after move | |
858 | if (conductCompression) { | |
859 | CFMutableDictionaryRef options = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); | |
860 | CFStringRef val = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, CFSTR("%d"), cInfo.compressionType); | |
861 | CFDictionarySetValue(options, kAFSCCompressionTypes, val); | |
862 | CFRelease(val); | |
863 | ||
864 | CompressionQueueContext compressionQueue = CreateCompressionQueue(NULL, NULL, NULL, NULL, options); | |
865 | ||
866 | if (!CompressFile(compressionQueue, path.c_str(), NULL)) { | |
867 | secinfo("bundlediskrep", "%p Failed to queue compression of file %s", this, path.c_str()); | |
868 | MacOSError::throwMe(errSecCSInternalError); | |
869 | } | |
870 | ||
871 | FinishCompressionAndCleanUp(compressionQueue); | |
872 | compressionQueue = NULL; | |
873 | CFRelease(options); | |
874 | } | |
875 | #endif | |
876 | ||
fa7225c8 | 877 | mWrittenFiles.insert(name); |
b1ab9ed8 A |
878 | } else |
879 | MacOSError::throwMe(errSecCSBadBundleFormat); | |
880 | } | |
881 | } | |
882 | ||
883 | ||
884 | // | |
885 | // Remove all signature data | |
886 | // | |
887 | void BundleDiskRep::Writer::remove() | |
888 | { | |
889 | // remove signature from the executable | |
890 | execWriter->remove(); | |
891 | ||
892 | // remove signature files from bundle | |
893 | for (CodeDirectory::SpecialSlot slot = 0; slot < cdSlotCount; slot++) | |
894 | remove(slot); | |
895 | remove(cdSignatureSlot); | |
896 | } | |
897 | ||
898 | void BundleDiskRep::Writer::remove(CodeDirectory::SpecialSlot slot) | |
899 | { | |
900 | if (const char *name = CodeDirectory::canonicalSlotName(slot)) | |
901 | if (::unlink(rep->metaPath(name).c_str())) | |
902 | switch (errno) { | |
903 | case ENOENT: // not found - that's okay | |
904 | break; | |
905 | default: | |
906 | UnixError::throwMe(); | |
907 | } | |
908 | } | |
909 | ||
910 | ||
911 | void BundleDiskRep::Writer::flush() | |
912 | { | |
913 | execWriter->flush(); | |
fa7225c8 A |
914 | purgeMetaDirectory(); |
915 | } | |
79b9da22 | 916 | |
fa7225c8 A |
917 | // purge _CodeSignature of all left-over files from any previous signature |
918 | void BundleDiskRep::Writer::purgeMetaDirectory() | |
919 | { | |
920 | DirScanner scan(rep->mMetaPath); | |
921 | if (scan.initialized()) { | |
922 | while (struct dirent* ent = scan.getNext()) { | |
923 | if (!scan.isRegularFile(ent)) | |
924 | MacOSError::throwMe(errSecCSUnsealedAppRoot); // only regular files allowed | |
925 | if (mWrittenFiles.find(ent->d_name) == mWrittenFiles.end()) { // we didn't write this! | |
926 | scan.unlink(ent, 0); | |
927 | } | |
928 | } | |
929 | } | |
930 | ||
b1ab9ed8 A |
931 | } |
932 | ||
79b9da22 A |
933 | void BundleDiskRep::registerStapledTicket() |
934 | { | |
935 | string root = cfStringRelease(copyCanonicalPath()); | |
936 | registerStapledTicketInBundle(root); | |
937 | } | |
b1ab9ed8 A |
938 | |
939 | } // end namespace CodeSigning | |
940 | } // end namespace Security |