2 * Copyright (c) 2006-2014 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
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
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.
21 * @APPLE_LICENSE_HEADER_END@
25 // resource directory construction and verification
27 #include "resources.h"
28 #include "csutilities.h"
29 #include <security_utilities/unix++.h>
30 #include <security_utilities/debugging.h>
31 #include <Security/CSCommon.h>
32 #include <security_utilities/unix++.h>
33 #include <security_utilities/cfmunge.h>
35 // These are pretty nasty, but are a quick safe fix
36 // to pass information down to the gatekeeper collection tool
38 int GKBIS_DS_Store_Present
;
39 int GKBIS_Dot_underbar_Present
;
40 int GKBIS_Num_localizations
;
43 int GKBIS_Num_symlinks
;
47 namespace CodeSigning
{
50 static string
removeTrailingSlash(string path
)
52 if (path
.substr(path
.length()-2, 2) == "/.")
53 return path
.substr(0, path
.length()-2);
54 else if (path
.substr(path
.length()-1, 1) == "/")
55 return path
.substr(0, path
.length()-1);
61 // Construction and maintainance
63 ResourceBuilder::ResourceBuilder(const std::string
&root
, const std::string
&relBase
,
64 CFDictionaryRef rulesDict
, bool strict
, const MacOSErrorSet
& toleratedErrors
)
65 : mCheckUnreadable(strict
&& toleratedErrors
.find(errSecCSSignatureNotVerifiable
) == toleratedErrors
.end()),
66 mCheckUnknownType(strict
&& toleratedErrors
.find(errSecCSResourceNotSupported
) == toleratedErrors
.end())
68 assert(!root
.empty());
69 char realroot
[PATH_MAX
];
70 if (realpath(root
.c_str(), realroot
) == NULL
)
73 if (realpath(removeTrailingSlash(relBase
).c_str(), realroot
) == NULL
)
76 if (mRoot
!= mRelBase
&& mRelBase
!= mRoot
+ "/Contents")
77 MacOSError::throwMe(errSecCSBadBundleFormat
);
78 const char * paths
[2] = { mRoot
.c_str(), NULL
};
79 mFTS
= fts_open((char * const *)paths
, FTS_PHYSICAL
| FTS_COMFOLLOW
| FTS_NOCHDIR
, NULL
);
82 mRawRules
= rulesDict
;
83 CFDictionary
rules(rulesDict
, errSecCSResourceRulesInvalid
);
84 rules
.apply(this, &ResourceBuilder::addRule
);
87 ResourceBuilder::~ResourceBuilder()
89 for (Rules::iterator it
= mRules
.begin(); it
!= mRules
.end(); ++it
)
91 fts_close(mFTS
); // do not check error - it's not worth aborting over (double fault etc.)
96 // Parse and add one matching rule
98 void ResourceBuilder::addRule(CFTypeRef key
, CFTypeRef value
)
100 string pattern
= cfString(key
, errSecCSResourceRulesInvalid
);
103 if (CFGetTypeID(value
) == CFBooleanGetTypeID()) {
104 if (value
== kCFBooleanFalse
)
107 CFDictionary
rule(value
, errSecCSResourceRulesInvalid
);
108 if (CFNumberRef weightRef
= rule
.get
<CFNumberRef
>("weight"))
109 weight
= cfNumber
<unsigned int>(weightRef
);
110 if (CFBooleanRef omitRef
= rule
.get
<CFBooleanRef
>("omit"))
111 if (omitRef
== kCFBooleanTrue
)
113 if (CFBooleanRef optRef
= rule
.get
<CFBooleanRef
>("optional"))
114 if (optRef
== kCFBooleanTrue
)
116 if (CFBooleanRef nestRef
= rule
.get
<CFBooleanRef
>("nested"))
117 if (nestRef
== kCFBooleanTrue
)
120 addRule(new Rule(pattern
, weight
, flags
));
123 static bool findStringEndingNoCase(const char *path
, const char * end
)
125 size_t len_path
= strlen(path
);
126 size_t len_end
= strlen(end
);
128 if (len_path
>= len_end
) {
129 return strcasecmp(path
+ (len_path
- len_end
), end
) == 0;
135 // Locate the next non-ignored file, look up its rule, and return it.
136 // Returns NULL when we're out of files.
138 void ResourceBuilder::scan(Scanner next
)
142 while (FTSENT
*ent
= fts_read(mFTS
)) {
143 static const char ds_store
[] = ".DS_Store";
144 const char *relpath
= ent
->fts_path
+ mRoot
.size(); // skip prefix
146 if (strlen(relpath
) > 0) {
147 relpath
+= 1; // skip "/"
151 if (mRelBase
!= mRoot
) {
152 assert(mRelBase
== mRoot
+ "/Contents");
153 rp
= "../" + string(relpath
);
154 if (rp
.substr(0, 12) == "../Contents/")
156 relpath
= rp
.c_str();
158 switch (ent
->fts_info
) {
160 secinfo("rdirenum", "file %s", ent
->fts_path
);
163 // These are checks for the gatekeeper collection
164 static const char underbar
[] = "._";
165 if (strncasecmp(ent
->fts_name
, underbar
, strlen(underbar
)) == 0)
166 GKBIS_Dot_underbar_Present
++;
168 if (strcasecmp(ent
->fts_name
, ds_store
) == 0)
169 GKBIS_DS_Store_Present
++;
171 if (Rule
*rule
= findRule(relpath
))
172 if (!(rule
->flags
& (omitted
| exclusion
)))
173 next(ent
, rule
->flags
, string(relpath
), rule
);
176 // symlinks cannot ever be nested code, so quietly convert to resource file
177 secinfo("rdirenum", "symlink %s", ent
->fts_path
);
178 GKBIS_Num_symlinks
++;
180 if (strcasecmp(ent
->fts_name
, ds_store
) == 0)
181 MacOSError::throwMe(errSecCSDSStoreSymlink
);
183 if (Rule
*rule
= findRule(relpath
))
184 if (!(rule
->flags
& (omitted
| exclusion
)))
185 next(ent
, rule
->flags
& ~nested
, string(relpath
), rule
);
188 secinfo("rdirenum", "entering %s", ent
->fts_path
);
191 if (!first
) { // skip root directory
192 if (Rule
*rule
= findRule(relpath
)) {
193 if (rule
->flags
& nested
) {
194 if (strchr(ent
->fts_name
, '.')) { // nested, has extension -> treat as nested bundle
195 next(ent
, rule
->flags
, string(relpath
), rule
);
196 fts_set(mFTS
, ent
, FTS_SKIP
);
198 } else if (rule
->flags
& exclusion
) { // exclude the whole directory
199 fts_set(mFTS
, ent
, FTS_SKIP
);
201 // else treat as normal directory and descend into it
204 // Report the number of localizations
205 if (findStringEndingNoCase(ent
->fts_name
, ".lproj"))
206 GKBIS_Num_localizations
++;
211 secinfo("rdirenum", "leaving %s", ent
->fts_path
);
214 secinfo("rdirenum", "cannot read directory %s", ent
->fts_path
);
215 if (mCheckUnreadable
)
216 MacOSError::throwMe(errSecCSSignatureNotVerifiable
);
219 secinfo("rdirenum", "type %d (errno %d): %s",
220 ent
->fts_info
, ent
->fts_errno
, ent
->fts_path
);
221 if (mCheckUnknownType
)
222 MacOSError::throwMe(errSecCSResourceNotSupported
);
230 // Check a single for for inclusion in the resource envelope
232 bool ResourceBuilder::includes(string path
) const
234 // process first-directory exclusions
235 size_t firstslash
= path
.find('/');
236 if (firstslash
!= string::npos
)
237 if (Rule
*rule
= findRule(path
.substr(0, firstslash
)))
238 if (rule
->flags
& exclusion
)
239 return rule
->flags
& softTarget
;
241 // process full match
242 if (Rule
*rule
= findRule(path
))
243 return !(rule
->flags
& (omitted
| exclusion
)) || (rule
->flags
& softTarget
);
250 // Find the best-matching resource rule for an alleged resource file.
251 // Returns NULL if no rule matches, or an exclusion rule applies.
253 ResourceBuilder::Rule
*ResourceBuilder::findRule(string path
) const
255 Rule
*bestRule
= NULL
;
256 secinfo("rscan", "test %s", path
.c_str());
257 for (Rules::const_iterator it
= mRules
.begin(); it
!= mRules
.end(); ++it
) {
259 secinfo("rscan", "try %s", rule
->source
.c_str());
260 if (rule
->match(path
.c_str())) {
261 secinfo("rscan", "match");
262 if (rule
->flags
& exclusion
) {
263 secinfo("rscan", "excluded");
266 if (!bestRule
|| rule
->weight
> bestRule
->weight
)
271 /* rdar://problem/30517969 */
272 if (bestRule
&& bestRule
->weight
== rule
->weight
&& !(bestRule
->flags
& omitted
) && (rule
->flags
& omitted
))
277 secinfo("rscan", "choosing %s (%d,0x%x)",
278 bestRule
? bestRule
->source
.c_str() : "NOTHING",
279 bestRule
? bestRule
->weight
: 0,
280 bestRule
? bestRule
->flags
: 0);
286 // Hash a file and return a CFDataRef with the hash
288 CFDataRef
ResourceBuilder::hashFile(const char *path
, CodeDirectory::HashAlgorithm type
)
290 UnixPlusPlus::AutoFileDesc
fd(path
);
291 fd
.fcntl(F_NOCACHE
, true); // turn off page caching (one-pass)
292 RefPointer
<DynamicHash
> hasher(CodeDirectory::hashFor(type
));
293 hashFileData(fd
, hasher
.get());
294 vector
<Hashing::Byte
> digest_vector(hasher
->digestLength());
295 hasher
->finish(digest_vector
.data());
296 return CFDataCreate(NULL
, digest_vector
.data(),
297 digest_vector
.size() * sizeof(Hashing::Byte
));
302 // Hash a file to multiple hash types and return a dictionary suitable to form a resource seal
304 CFMutableDictionaryRef
ResourceBuilder::hashFile(const char *path
, CodeDirectory::HashAlgorithms types
, bool strictCheck
)
306 UnixPlusPlus::AutoFileDesc
fd(path
);
307 fd
.fcntl(F_NOCACHE
, true); // turn off page caching (one-pass)
309 if (fd
.hasExtendedAttribute(XATTR_RESOURCEFORK_NAME
) || fd
.hasExtendedAttribute(XATTR_FINDERINFO_NAME
))
310 MacOSError::throwMe(errSecCSInvalidAssociatedFileData
);
311 CFRef
<CFMutableDictionaryRef
> result
= makeCFMutableDictionary();
312 CFMutableDictionaryRef resultRef
= result
;
313 CodeDirectory::multipleHashFileData(fd
, 0, types
, ^(CodeDirectory::HashAlgorithm type
, Security::DynamicHash
*hasher
) {
314 size_t length
= hasher
->digestLength();
315 vector
<Hashing::Byte
> digest_vector(length
);
316 hasher
->finish(digest_vector
.data());
317 CFDictionaryAddValue(resultRef
, CFTempString(hashName(type
)), CFTempData(digest_vector
.data(), length
));
319 return result
.yield();
323 std::string
ResourceBuilder::hashName(CodeDirectory::HashAlgorithm type
)
326 case kSecCodeSignatureHashSHA1
:
330 snprintf(name
, sizeof(name
), "hash%d", int(type
));
337 // Regex matching objects
339 ResourceBuilder::Rule::Rule(const std::string
&pattern
, unsigned w
, uint32_t f
)
340 : weight(w
), flags(f
), source(pattern
)
342 if (::regcomp(this, pattern
.c_str(), REG_EXTENDED
| REG_NOSUB
)) //@@@ REG_ICASE?
343 MacOSError::throwMe(errSecCSResourceRulesInvalid
);
344 secinfo("csresource", "%p rule %s added (weight %d, flags 0x%x)",
345 this, pattern
.c_str(), w
, f
);
348 ResourceBuilder::Rule::~Rule()
353 bool ResourceBuilder::Rule::match(const char *s
) const
355 switch (::regexec(this, s
, 0, NULL
, 0)) {
361 MacOSError::throwMe(errSecCSResourceRulesInvalid
);
366 std::string
ResourceBuilder::escapeRE(const std::string
&s
)
369 for (string::const_iterator it
= s
.begin(); it
!= s
.end(); ++it
) {
371 if (strchr("\\[]{}().+*?^$|", c
))
382 ResourceSeal::ResourceSeal(CFTypeRef it
)
383 : mDict(NULL
), mRequirement(NULL
), mLink(NULL
), mFlags(0)
386 MacOSError::throwMe(errSecCSResourcesInvalid
);
387 if (CFGetTypeID(it
) == CFDataGetTypeID()) // old-style form with just a hash
388 mDict
.take(cfmake
<CFDictionaryRef
>("{hash=%O}", it
));
389 else if (CFGetTypeID(it
) == CFDictionaryGetTypeID())
390 mDict
= CFDictionaryRef(it
);
392 MacOSError::throwMe(errSecCSResourcesInvalid
);
396 if (CFDictionaryGetValue(mDict
, CFSTR("requirement")))
397 err
= !cfscan(mDict
, "{requirement=%SO,?optional=%B}", &mRequirement
, &optional
);
398 else if (CFDictionaryGetValue(mDict
, CFSTR("symlink")))
399 err
= !cfscan(mDict
, "{symlink=%SO,?optional=%B}", &mLink
, &optional
);
401 err
= !cfscan(mDict
, "{?optional=%B}", &optional
);
403 MacOSError::throwMe(errSecCSResourcesInvalid
);
405 mFlags
|= ResourceBuilder::optional
;
407 mFlags
|= ResourceBuilder::nested
;
411 const Hashing::Byte
*ResourceSeal::hash(CodeDirectory::HashAlgorithm type
) const
413 std::string name
= ResourceBuilder::hashName(type
);
414 CFTypeRef hash
= CFDictionaryGetValue(mDict
, CFTempString(name
));
415 if (hash
== NULL
) // pre-agility fallback
416 hash
= CFDictionaryGetValue(mDict
, CFSTR("hash"));
417 if (hash
== NULL
|| CFGetTypeID(hash
) != CFDataGetTypeID())
418 MacOSError::throwMe(errSecCSResourcesInvalid
);
419 return CFDataGetBytePtr(CFDataRef(hash
));
423 } // end namespace CodeSigning
424 } // end namespace Security