errSecCSInfoPlistFailed = -67030, /* invalid Info.plist (plist or signature have been modified) */
errSecCSNoMainExecutable = -67029, /* the code has no main executable file */
errSecCSBadBundleFormat = -67028, /* bundle format unrecognized, invalid, or unsuitable */
+ errSecCSNoMatches = -67027, /* no matches for search or update operation */
};
kSecCSDefaultFlags = 0, /* no particular flags (default behavior) */
kSecCSConsiderExpiration = 1 << 31, /* consider expired certificates invalid */
+ kSecCSEnforceRevocationChecks = 1 << 30, /* force revocation checks regardless of preference settings */
};
#include <security_utilities/globalizer.h>
#include <security_utilities/unix++.h>
#include <security_utilities/cfmunge.h>
+#include <notify.h>
using namespace CodeSigning;
ModuleNexus<PolicyEngine> gEngine;
-//
-// Help mapping API-ish CFString keys to more convenient internal enumerations
-//
-typedef struct {
- CFStringRef cstring;
- uint enumeration;
-} StringMap;
-
-static uint mapEnum(CFDictionaryRef context, CFStringRef attr, const StringMap *map, uint value = 0)
-{
- if (context)
- if (CFTypeRef value = CFDictionaryGetValue(context, attr))
- for (const StringMap *mp = map; mp->cstring; ++mp)
- if (CFEqual(mp->cstring, value))
- return mp->enumeration;
- if (value)
- return value;
- MacOSError::throwMe(errSecCSInvalidAttributeValues);
-}
-
-static const StringMap mapType[] = {
- { kSecAssessmentOperationTypeExecute, kAuthorityExecute },
- { kSecAssessmentOperationTypeInstall, kAuthorityInstall },
- { kSecAssessmentOperationTypeOpenDocument, kAuthorityOpenDoc },
- { NULL }
-};
-
-static AuthorityType typeFor(CFDictionaryRef context, AuthorityType type = kAuthorityInvalid)
-{ return mapEnum(context, kSecAssessmentContextKeyOperation, mapType, type); }
-
-
//
// Policy evaluation ("assessment") operations
//
-const CFStringRef kSecAssessmentContextKeyCertificates = CFSTR("context:certificates");
-
const CFStringRef kSecAssessmentAssessmentVerdict = CFSTR("assessment:verdict");
const CFStringRef kSecAssessmentAssessmentOriginator = CFSTR("assessment:originator");
const CFStringRef kSecAssessmentAssessmentAuthority = CFSTR("assessment:authority");
const CFStringRef kSecAssessmentAssessmentAuthorityOverride = CFSTR("assessment:authority:override");
const CFStringRef kSecAssessmentAssessmentFromCache = CFSTR("assessment:authority:cached");
+const CFStringRef kSecAssessmentContextKeyCertificates = CFSTR("context:certificates"); // obsolete
+
SecAssessmentRef SecAssessmentCreate(CFURLRef path,
SecAssessmentFlags flags,
CFDictionaryRef context,
xpcEngineAssess(path, flags, context, result);
}
} catch (CommonError &error) {
- if (!overrideAssessment())
- throw; // let it go as an error
+ switch (error.osStatus()) {
+ case CSSMERR_TP_CERT_REVOKED:
+ throw;
+ default:
+ if (!overrideAssessment())
+ throw; // let it go as an error
+ break;
+ }
+ // record the error we would have returned
cfadd(result, "{%O=#F,'assessment:error'=%d}}", kSecAssessmentAssessmentVerdict, error.osStatus());
} catch (...) {
+ // catch stray errors not conforming to the CommonError scheme
if (!overrideAssessment())
throw; // let it go as an error
cfadd(result, "{%O=#F}", kSecAssessmentAssessmentVerdict);
//
-// Policy editing operations
+// Policy editing operations.
+// These all make permanent changes to the system-wide authority records.
//
const CFStringRef kSecAssessmentContextKeyUpdate = CFSTR("update");
-const CFStringRef kSecAssessmentUpdateOperationAddFile = CFSTR("update:addfile");
-const CFStringRef kSecAssessmentUpdateOperationRemoveFile = CFSTR("update:removefile");
+const CFStringRef kSecAssessmentUpdateOperationAdd = CFSTR("update:add");
+const CFStringRef kSecAssessmentUpdateOperationRemove = CFSTR("update:remove");
+const CFStringRef kSecAssessmentUpdateOperationEnable = CFSTR("update:enable");
+const CFStringRef kSecAssessmentUpdateOperationDisable = CFSTR("update:disable");
+const CFStringRef kSecAssessmentUpdateKeyAuthorization = CFSTR("update:authorization");
const CFStringRef kSecAssessmentUpdateKeyPriority = CFSTR("update:priority");
const CFStringRef kSecAssessmentUpdateKeyLabel = CFSTR("update:label");
+const CFStringRef kSecAssessmentUpdateKeyExpires = CFSTR("update:expires");
+const CFStringRef kSecAssessmentUpdateKeyAllow = CFSTR("update:allow");
+const CFStringRef kSecAssessmentUpdateKeyRemarks = CFSTR("update:remarks");
-Boolean SecAssessmentUpdate(CFURLRef path,
+Boolean SecAssessmentUpdate(CFTypeRef target,
SecAssessmentFlags flags,
CFDictionaryRef context,
CFErrorRef *errors)
BEGIN_CSAPI
CFDictionary ctx(context, errSecCSInvalidAttributeValues);
- CFStringRef edit = ctx.get<CFStringRef>(kSecAssessmentContextKeyUpdate);
- AuthorityType type = typeFor(context);
- if (edit == kSecAssessmentUpdateOperationAddFile)
- return gEngine().add(path, type, flags, context);
- else if (edit == kSecAssessmentUpdateOperationRemoveFile)
- MacOSError::throwMe(errSecCSUnimplemented);
- else
- MacOSError::throwMe(errSecCSInvalidAttributeValues);
+ if (flags & kSecAssessmentFlagDirect) {
+ // ask the engine right here to do its thing
+ return gEngine().update(target, flags, ctx);
+ } else {
+ // relay the question to our daemon for consideration
+ return xpcEngineUpdate(target, flags, ctx);
+ }
END_CSAPI_ERRORS1(false)
}
BEGIN_CSAPI
if (CFEqual(control, CFSTR("ui-enable"))) {
- UnixPlusPlus::AutoFileDesc flagFile(visibleSecurityFlagFile, O_CREAT | O_WRONLY, 0644);
+ { UnixPlusPlus::AutoFileDesc flagFile(visibleSecurityFlagFile, O_CREAT | O_WRONLY, 0644); }
+ notify_post(kNotifySecAssessmentMasterSwitch);
MessageTrace trace("com.apple.security.assessment.state", "enable");
trace.send("enable assessment outcomes");
return true;
} else if (CFEqual(control, CFSTR("ui-disable"))) {
if (::remove(visibleSecurityFlagFile) == 0 || errno == ENOENT) {
+ notify_post(kNotifySecAssessmentMasterSwitch);
MessageTrace trace("com.apple.security.assessment.state", "disable");
trace.send("disable assessment outcomes");
return true;
else
result = kCFBooleanTrue;
return true;
+ } else if (CFEqual(control, CFSTR("ui-enable-devid"))) {
+ CFTemp<CFDictionaryRef> ctx("{%O=%s}", kSecAssessmentUpdateKeyLabel, "Developer ID");
+ return gEngine().enable(NULL, kAuthorityInvalid, kSecCSDefaultFlags, ctx);
+ } else if (CFEqual(control, CFSTR("ui-disable-devid"))) {
+ CFTemp<CFDictionaryRef> ctx("{%O=%s}", kSecAssessmentUpdateKeyLabel, "Developer ID");
+ return gEngine().disable(NULL, kAuthorityInvalid, kSecCSDefaultFlags, ctx);
+ } else if (CFEqual(control, CFSTR("ui-get-devid"))) {
+ CFBooleanRef &result = *(CFBooleanRef*)(arguments);
+ if (gEngine().value<int>("SELECT disabled FROM authority WHERE label = 'Developer ID';", true))
+ result = kCFBooleanFalse;
+ else
+ result = kCFBooleanTrue;
+ return true;
} else
MacOSError::throwMe(errSecCSInvalidAttributeValues);
CFTypeID SecAssessmentGetTypeID();
+/*
+ * Notifications sent when the policy authority database changes.
+ * (Should move to /usr/include/notify_keys.h eventually.)
+ */
+#define kNotifySecAssessmentMasterSwitch "com.apple.security.assessment.masterswitch"
+#define kNotifySecAssessmentUpdate "com.apple.security.assessment.update"
+
+
/*!
- * Primary operation codes. These are operations the system policy can express
+ * Primary operation types. These are operations the system policy can express
* opinions on. They are not operations *on* the system configuration itself.
* (For those, see SecAssessmentUpdate below.)
*
* @constant kSecAssessmentContextKeyOperation Context key describing the type of operation
- * being contemplated.
- * @constant kSecAssessmentOperationTypeInstall Value denoting the operation of installing
- * software into the system.
+ * being contemplated. The default varies depending on the API call used.
* @constant kSecAssessmentOperationTypeExecute Value denoting the operation of running or executing
* code on the system.
+ * @constant kSecAssessmentOperationTypeInstall Value denoting the operation of installing
+ * software into the system.
+ * @constant kSecAssessmentOperationTypeOpenDocument Value denoting the operation of opening
+ * (in the LaunchServices sense) of documents.
*/
extern const CFStringRef kSecAssessmentContextKeyOperation; // proposed operation
extern const CFStringRef kSecAssessmentOperationTypeExecute; // .. execute code
evaluation of system policy. This may be substantially slower.
@constant kSecAssessmentFlagNoCache Do not save any evaluation outcome in the system caches.
Any content already there is left undisturbed. Independent of kSecAssessmentFlagIgnoreCache.
+ @constant kSecAssessmentFlagEnforce Perform normal operations even if assessments have been
+ globally bypassed (which would usually approve anything).
Flags common to multiple calls are assigned from high-bit down. Flags for particular calls
are assigned low-bit up, and are documented with that call.
@param context Optional CFDictionaryRef containing additional information bearing
on the requested assessment.
@param errors Standard CFError argument for reporting errors. Note that declining to permit
- the proposed operation is not an error. Inability to form a judgment is.
+ the proposed operation is not an error. Inability to arrive at a judgment is.
@result On success, a SecAssessment object that can be queried for its outcome.
On error, NULL (with *errors set).
@constant kSecAssessmentContextKeyOperation Type of operation (see overview above). This defaults
to the kSecAssessmentOperationTypeExecute.
- @constant kSecAssessmentContextKeyEdit A CFArray of SecCertificateRefs describing the
- certificate chain of a CMS-type signature as pulled from 'path' by the caller.
- The first certificate is the signing certificate. The certificates provided must be
- sufficient to construct a valid certificate chain.
*/
extern const CFStringRef kSecAssessmentContextKeyCertificates; // certificate chain as provided by caller
Extract results from a completed assessment and return them as a CFDictionary.
- Assessment result keys (dictionary keys returned on success):
-
@param assessment A SecAssessmentRef created with SecAssessmentCreate.
@param flags Operation flags and options. Pass kSecAssessmentDefaultFlags for default
behavior.
data as requested by flags. The caller owns this dictionary and should release it
when done with it. On error, NULL (with *errors set).
+ Assessment result keys (dictionary keys returned on success):
+
@constant kSecAssessmentAssessmentVerdict A CFBoolean value indicating whether the system policy
allows (kCFBooleanTrue) or denies (kCFBooleanFalse) the proposed operation.
@constant kSecAssessmentAssessmentAuthority A CFDictionary describing what sources of authority
@function SecAssessmentUpdate
Make changes to the system policy configuration.
- @param path CFURL describing the file central to an operation - the program
- to be executed, archive to be installed, plugin to be loaded, etc.
- Pass NULL if the requested operation has no file subject.
+ @param path CFTypeRef describing the subject of the operation. Depending on the operation,
+ this may be a CFURL denoting a (single) file or bundle; a SecRequirement describing
+ a group of files; a CFNumber denoting an existing rule by rule number, or NULL to perform
+ global changes.
@param flags Operation flags and options. Pass kSecAssessmentDefaultFlags for default
behavior.
@param context Required CFDictionaryRef containing information bearing
@constant kSecAssessmentContextKeyEdit Required context key describing the kind of change
requested to the system policy configuration. Currently understood values:
- @constant kSecAssessmentUpdateOperationAddFile Request to add rules governing operations on the 'path'
- argument.
- @constant kSecAssessmentUpdateOperationRemoveFile Request to remove rules governing operations on the
- 'path' argument.
+ @constant kSecAssessmentUpdateOperationAdd Add a new rule to the assessment rule database.
+ @constant kSecAssessmentUpdateOperationRemove Remove rules from the rule database.
+ @constant kSecAssessmentUpdateOperationEnable (Re)enable rules in the rule database.
+ @constant kSecAssessmentUpdateOperationDisable Disable rules in the rule database.
+ @constant kSecAssessmentUpdateKeyAuthorization A CFData containing the external form of a
+ system AuthorizationRef used to authorize the change. The call will automatically generate
+ a suitable authorization if this is missing; however, if the request is on behalf of
+ another client, an AuthorizationRef should be created there and passed along here.
+ @constant kSecAssessmentUpdateKeyPriority CFNumber denoting a (floating point) priority
+ for the rule(s) being processed.
+ @constant kSecAssessmentUpdateKeyLabel CFString denoting a label string applied to the rule(s)
+ being processed.
+ @constant kSecAssessmentUpdateKeyExpires CFDate denoting an (absolute, future) expiration date
+ for rule(s) being processed.
+ @constant kSecAssessmentUpdateKeyAllow CFBoolean denoting whether a new rule allows or denies
+ assessment. The default is to allow; set to kCFBooleanFalse to create a negative (denial) rule.
+ @constant kSecAssessmentUpdateKeyRemarks CFString containing a colloquial description or comment
+ about a newly created rule. This is mean to be human readable and is not used when evaluating rules.
*/
extern const CFStringRef kSecAssessmentContextKeyUpdate; // proposed operation
-extern const CFStringRef kSecAssessmentUpdateOperationAddFile; // add to policy database
-extern const CFStringRef kSecAssessmentUpdateOperationRemoveFile; // remove from policy database
+extern const CFStringRef kSecAssessmentUpdateOperationAdd; // add rule to policy database
+extern const CFStringRef kSecAssessmentUpdateOperationRemove; // remove rule from policy database
+extern const CFStringRef kSecAssessmentUpdateOperationEnable; // enable rule(s) in policy database
+extern const CFStringRef kSecAssessmentUpdateOperationDisable; // disable rule(s) in policy database
+
+extern const CFStringRef kSecAssessmentUpdateKeyAuthorization; // [CFData] external form of governing authorization
extern const CFStringRef kSecAssessmentUpdateKeyPriority; // rule priority
extern const CFStringRef kSecAssessmentUpdateKeyLabel; // rule label
+extern const CFStringRef kSecAssessmentUpdateKeyExpires; // rule expiration
+extern const CFStringRef kSecAssessmentUpdateKeyAllow; // rule outcome (allow/deny)
+extern const CFStringRef kSecAssessmentUpdateKeyRemarks; // rule remarks (human readable)
-Boolean SecAssessmentUpdate(CFURLRef path,
+extern const CFStringRef kSecAssessmentContextKeyCertificates; // obsolete
+
+Boolean SecAssessmentUpdate(CFTypeRef target,
SecAssessmentFlags flags,
CFDictionaryRef context,
CFErrorRef *errors);
/*!
@function SecAssessmentControl
- Miscellaneous system policy operations
+ Miscellaneous system policy operations.
+
+ @param control A CFString indicating which operation is requested.
+ @param arguments Arguments to the operation as documented for control.
+ @param errors Standard CFErrorRef * argument to report errors.
+ @result Returns True on success. Returns False on failure (and sets *errors).
*/
Boolean SecAssessmentControl(CFStringRef control, void *arguments, CFErrorRef *errors);
BEGIN_CSAPI
checkFlags(flags,
- kSecCSConsiderExpiration);
+ kSecCSConsiderExpiration
+ | kSecCSEnforceRevocationChecks);
SecPointer<SecCode> code = SecCode::required(codeRef);
code->checkValidity(flags);
if (const SecRequirement *req = SecRequirement::optional(requirementRef))
static void validate(SecStaticCode *code, const SecRequirement *req, SecCSFlags flags)
{
try {
- code->validateDirectory();
+ code->validateNonResourceComponents(); // also validates the CodeDirectory
if (!(flags & kSecCSDoNotValidateExecutable))
code->validateExecutable();
if (!(flags & kSecCSDoNotValidateResources))
| kSecCSDoNotValidateExecutable
| kSecCSDoNotValidateResources
| kSecCSConsiderExpiration
+ | kSecCSEnforceRevocationChecks
| kSecCSCheckNestedCode);
SecPointer<SecStaticCode> code = SecStaticCode::requiredStatic(staticCodeRef);
#include <Security/SecCmsContentInfo.h>
#include <Security/SecCmsSignerInfo.h>
#include <Security/SecCmsSignedData.h>
+#include <Security/cssmapplePriv.h>
#include <security_utilities/unix++.h>
#include <security_utilities/cfmunge.h>
#include <Security/CMSDecoder.h>
//
SecStaticCode::SecStaticCode(DiskRep *rep)
: mRep(rep),
- mValidated(false), mExecutableValidated(false),
+ mValidated(false), mExecutableValidated(false), mResourcesValidated(false), mResourcesValidContext(NULL),
mDesignatedReq(NULL), mGotResourceBase(false), mEvalDetails(NULL)
{
CODESIGN_STATIC_CREATE(this, rep);
SecStaticCode::~SecStaticCode() throw()
try {
::free(const_cast<Requirement *>(mDesignatedReq));
+ if (mResourcesValidContext)
+ delete mResourcesValidContext;
} catch (...) {
return;
}
CODESIGN_EVAL_STATIC_RESET(this);
mValidated = false;
mExecutableValidated = false;
+ mResourcesValidated = false;
+ if (mResourcesValidContext) {
+ delete mResourcesValidContext;
+ mResourcesValidContext = NULL;
+ }
mDir = NULL;
mSignature = NULL;
for (unsigned n = 0; n < cdSlotCount; n++)
CODESIGN_EVAL_STATIC_DIRECTORY(this);
mValidationExpired = verifySignature();
component(cdInfoSlot, errSecCSInfoPlistFailed); // force load of Info Dictionary (if any)
- CodeDirectory::SpecialSlot slot = codeDirectory()->nSpecialSlots;
- if (slot > cdSlotMax) // might have more special slots than we know about...
- slot = cdSlotMax; // ... but only process the ones we understand
- for ( ; slot >= 1; --slot)
+ for (CodeDirectory::SpecialSlot slot = codeDirectory()->maxSpecialSlot(); slot >= 1; --slot)
if (mCache[slot]) // if we already loaded that resource...
validateComponent(slot); // ... then check it now
mValidated = true; // we've done the deed...
}
+//
+// Load and validate the CodeDirectory and all components *except* those related to the resource envelope.
+// Those latter components are checked by validateResources().
+//
+void SecStaticCode::validateNonResourceComponents()
+{
+ this->validateDirectory();
+ for (CodeDirectory::SpecialSlot slot = codeDirectory()->maxSpecialSlot(); slot >= 1; --slot)
+ switch (slot) {
+ case cdResourceDirSlot: // validated by validateResources
+ break;
+ default:
+ this->component(slot); // loads and validates
+ break;
+ }
+}
+
+
//
// Get the (signed) signing date from the code signature.
// Sadly, we need to validate the signature to get the date (as a side benefit).
MacOSError::check(CMSDecoderSetDetachedContent(cms, mDir));
MacOSError::check(CMSDecoderFinalizeMessage(cms));
MacOSError::check(CMSDecoderSetSearchKeychain(cms, cfEmptyArray()));
+ CFRef<CFTypeRef> policy = verificationPolicy(apiFlags());
CMSSignerStatus status;
- MacOSError::check(CMSDecoderCopySignerStatus(cms, 0, verificationPolicy(),
+ MacOSError::check(CMSDecoderCopySignerStatus(cms, 0, policy,
false, &status, &mTrust.aref(), NULL));
if (status != kCMSSignerValid)
MacOSError::throwMe(errSecCSSignatureFailed);
//
// Return the TP policy used for signature verification.
-// This policy object is cached and reused.
+// This may be a simple SecPolicyRef or a CFArray of policies.
+// The caller owns the return value.
//
-SecPolicyRef SecStaticCode::verificationPolicy()
+static SecPolicyRef makeCRLPolicy()
+{
+ CFRef<SecPolicyRef> policy;
+ MacOSError::check(SecPolicyCopy(CSSM_CERT_X_509v3, &CSSMOID_APPLE_TP_REVOCATION_CRL, &policy.aref()));
+ CSSM_APPLE_TP_CRL_OPTIONS options;
+ memset(&options, 0, sizeof(options));
+ options.Version = CSSM_APPLE_TP_CRL_OPTS_VERSION;
+ options.CrlFlags = CSSM_TP_ACTION_FETCH_CRL_FROM_NET | CSSM_TP_ACTION_CRL_SUFFICIENT;
+ CSSM_DATA optData = { sizeof(options), (uint8 *)&options };
+ MacOSError::check(SecPolicySetValue(policy, &optData));
+ return policy.yield();
+}
+
+static SecPolicyRef makeOCSPPolicy()
{
- if (!mPolicy)
- MacOSError::check(SecPolicyCopy(CSSM_CERT_X_509v3,
- &CSSMOID_APPLE_TP_CODE_SIGNING, &mPolicy.aref()));
- return mPolicy;
+ CFRef<SecPolicyRef> policy;
+ MacOSError::check(SecPolicyCopy(CSSM_CERT_X_509v3, &CSSMOID_APPLE_TP_REVOCATION_OCSP, &policy.aref()));
+ CSSM_APPLE_TP_OCSP_OPTIONS options;
+ memset(&options, 0, sizeof(options));
+ options.Version = CSSM_APPLE_TP_OCSP_OPTS_VERSION;
+ options.Flags = CSSM_TP_ACTION_OCSP_SUFFICIENT;
+ CSSM_DATA optData = { sizeof(options), (uint8 *)&options };
+ MacOSError::check(SecPolicySetValue(policy, &optData));
+ return policy.yield();
+}
+
+CFTypeRef SecStaticCode::verificationPolicy(SecCSFlags flags)
+{
+ CFRef<SecPolicyRef> core;
+ MacOSError::check(SecPolicyCopy(CSSM_CERT_X_509v3,
+ &CSSMOID_APPLE_TP_CODE_SIGNING, &core.aref()));
+ if (flags & kSecCSEnforceRevocationChecks) {
+ CFRef<SecPolicyRef> crl = makeCRLPolicy();
+ CFRef<SecPolicyRef> ocsp = makeOCSPPolicy();
+ return makeCFArray(3, core.get(), crl.get(), ocsp.get());
+ } else {
+ return core.yield();
+ }
}
//
void SecStaticCode::validateExecutable()
{
- DTRACK(CODESIGN_EVAL_STATIC_EXECUTABLE, this,
- (char*)this->mainExecutablePath().c_str(), codeDirectory()->nCodeSlots);
- const CodeDirectory *cd = this->codeDirectory();
- if (!cd)
- MacOSError::throwMe(errSecCSUnsigned);
- AutoFileDesc fd(mainExecutablePath(), O_RDONLY);
- fd.fcntl(F_NOCACHE, true); // turn off page caching (one-pass)
- if (Universal *fat = mRep->mainExecutableImage())
- fd.seek(fat->archOffset());
- size_t pageSize = cd->pageSize ? (1 << cd->pageSize) : 0;
- size_t remaining = cd->codeLimit;
- for (size_t slot = 0; slot < cd->nCodeSlots; ++slot) {
- size_t size = min(remaining, pageSize);
- if (!cd->validateSlot(fd, size, slot)) {
- CODESIGN_EVAL_STATIC_EXECUTABLE_FAIL(this, slot);
- mExecutableValidated = true; // we tried
- mExecutableValid = false; // it failed
- MacOSError::throwMe(errSecCSSignatureFailed);
+ if (!validatedExecutable()) {
+ try {
+ DTRACK(CODESIGN_EVAL_STATIC_EXECUTABLE, this,
+ (char*)this->mainExecutablePath().c_str(), codeDirectory()->nCodeSlots);
+ const CodeDirectory *cd = this->codeDirectory();
+ if (!cd)
+ MacOSError::throwMe(errSecCSUnsigned);
+ AutoFileDesc fd(mainExecutablePath(), O_RDONLY);
+ fd.fcntl(F_NOCACHE, true); // turn off page caching (one-pass)
+ if (Universal *fat = mRep->mainExecutableImage())
+ fd.seek(fat->archOffset());
+ size_t pageSize = cd->pageSize ? (1 << cd->pageSize) : 0;
+ size_t remaining = cd->codeLimit;
+ for (size_t slot = 0; slot < cd->nCodeSlots; ++slot) {
+ size_t size = min(remaining, pageSize);
+ if (!cd->validateSlot(fd, size, slot)) {
+ CODESIGN_EVAL_STATIC_EXECUTABLE_FAIL(this, slot);
+ MacOSError::throwMe(errSecCSSignatureFailed);
+ }
+ remaining -= size;
+ }
+ mExecutableValidated = true;
+ mExecutableValidResult = noErr;
+ } catch (const CommonError &err) {
+ mExecutableValidated = true;
+ mExecutableValidResult = err.osStatus();
+ throw;
+ } catch (...) {
+ secdebug("staticCode", "%p executable validation threw non-common exception", this);
+ mExecutableValidated = true;
+ mExecutableValidResult = errSecCSInternalError;
+ throw;
}
- remaining -= size;
}
- mExecutableValidated = true; // we tried
- mExecutableValid = true; // it worked
+ assert(validatedExecutable());
+ if (mExecutableValidResult != noErr)
+ MacOSError::throwMe(mExecutableValidResult);
}
//
void SecStaticCode::validateResources()
{
- // sanity first
- CFDictionaryRef sealedResources = resourceDictionary();
- if (this->resourceBase()) // disk has resources
- if (sealedResources)
- /* go to work below */;
- else
- MacOSError::throwMe(errSecCSResourcesNotFound);
- else // disk has no resources
- if (sealedResources)
- MacOSError::throwMe(errSecCSResourcesNotFound);
- else
- return; // no resources, not sealed - fine (no work)
-
- // found resources, and they are sealed
- CFDictionaryRef rules = cfget<CFDictionaryRef>(sealedResources, "rules");
- CFDictionaryRef files = cfget<CFDictionaryRef>(sealedResources, "files");
- DTRACK(CODESIGN_EVAL_STATIC_RESOURCES, this,
- (char*)this->mainExecutablePath().c_str(), int(CFDictionaryGetCount(files)));
-
- // make a shallow copy of the ResourceDirectory so we can "check off" what we find
- CFRef<CFMutableDictionaryRef> resourceMap = makeCFMutableDictionary(files);
-
- // scan through the resources on disk, checking each against the resourceDirectory
- CollectingContext ctx(*this); // collect all failures in here
- ResourceBuilder resources(cfString(this->resourceBase()), rules, codeDirectory()->hashType);
- mRep->adjustResources(resources);
- string path;
- ResourceBuilder::Rule *rule;
-
- while (resources.next(path, rule)) {
- validateResource(path, ctx);
- CFDictionaryRemoveValue(resourceMap, CFTempString(path));
- }
-
- if (CFDictionaryGetCount(resourceMap) > 0) {
- secdebug("staticCode", "%p sealed resource(s) not found in code", this);
- CFDictionaryApplyFunction(resourceMap, SecStaticCode::checkOptionalResource, &ctx);
+ if (!validatedResources()) {
+ try {
+ // sanity first
+ CFDictionaryRef sealedResources = resourceDictionary();
+ if (this->resourceBase()) // disk has resources
+ if (sealedResources)
+ /* go to work below */;
+ else
+ MacOSError::throwMe(errSecCSResourcesNotFound);
+ else // disk has no resources
+ if (sealedResources)
+ MacOSError::throwMe(errSecCSResourcesNotFound);
+ else
+ return; // no resources, not sealed - fine (no work)
+
+ // found resources, and they are sealed
+ CFDictionaryRef rules = cfget<CFDictionaryRef>(sealedResources, "rules");
+ CFDictionaryRef files = cfget<CFDictionaryRef>(sealedResources, "files");
+ DTRACK(CODESIGN_EVAL_STATIC_RESOURCES, this,
+ (char*)this->mainExecutablePath().c_str(), int(CFDictionaryGetCount(files)));
+
+ // make a shallow copy of the ResourceDirectory so we can "check off" what we find
+ CFRef<CFMutableDictionaryRef> resourceMap = makeCFMutableDictionary(files);
+
+ // scan through the resources on disk, checking each against the resourceDirectory
+ mResourcesValidContext = new CollectingContext(*this); // collect all failures in here
+ ResourceBuilder resources(cfString(this->resourceBase()), rules, codeDirectory()->hashType);
+ mRep->adjustResources(resources);
+ string path;
+ ResourceBuilder::Rule *rule;
+
+ while (resources.next(path, rule)) {
+ validateResource(path, *mResourcesValidContext);
+ CFDictionaryRemoveValue(resourceMap, CFTempString(path));
+ }
+
+ if (CFDictionaryGetCount(resourceMap) > 0) {
+ secdebug("staticCode", "%p sealed resource(s) not found in code", this);
+ CFDictionaryApplyFunction(resourceMap, SecStaticCode::checkOptionalResource, mResourcesValidContext);
+ }
+
+ // now check for any errors found in the reporting context
+ mResourcesValidated = true;
+ if (mResourcesValidContext->osStatus() != noErr)
+ mResourcesValidContext->throwMe();
+
+ } catch (const CommonError &err) {
+ mResourcesValidated = true;
+ mResourcesValidResult = err.osStatus();
+ throw;
+ } catch (...) {
+ secdebug("staticCode", "%p executable validation threw non-common exception", this);
+ mResourcesValidated = true;
+ mResourcesValidResult = errSecCSInternalError;
+ throw;
+ }
}
-
- // now check for any errors found in the reporting context
- if (ctx)
- ctx.throwMe();
+ assert(!validatedResources());
+ if (mResourcesValidResult)
+ MacOSError::throwMe(mResourcesValidResult);
+ if (mResourcesValidContext->osStatus() != noErr)
+ mResourcesValidContext->throwMe();
}
void SecStaticCode::CollectingContext::throwMe()
{
assert(mStatus != noErr);
- throw CSError(mStatus, mCollection.yield());
+ throw CSError(mStatus, mCollection.retain());
}
CollectingContext(SecStaticCode &c) : code(c), mStatus(noErr) { }
void reportProblem(OSStatus rc, CFStringRef type, CFTypeRef value);
+ OSStatus osStatus() { return mStatus; }
operator OSStatus () const { return mStatus; }
void throwMe() __attribute__((noreturn));
bool validated() const { return mValidated; }
bool valid() const
{ assert(validated()); return mValidated && (mValidationResult == noErr); }
+ bool validatedExecutable() const { return mExecutableValidated; }
+ bool validatedResources() const { return mResourcesValidated; }
+
void validateDirectory();
void validateComponent(CodeDirectory::SpecialSlot slot, OSStatus fail = errSecCSSignatureFailed);
+ void validateNonResourceComponents();
void validateResources();
void validateExecutable();
protected:
CFDictionaryRef getDictionary(CodeDirectory::SpecialSlot slot, OSStatus fail); // component value as a dictionary
bool verifySignature();
- SecPolicyRef verificationPolicy();
+ CFTypeRef verificationPolicy(SecCSFlags flags);
void defaultDesignatedAppleAnchor(Requirement::Maker &maker);
void defaultDesignatedNonAppleAnchor(Requirement::Maker &maker);
// static executable validation state (nested within mValidated/mValid)
bool mExecutableValidated; // tried to validate executable file
- bool mExecutableValid; // outcome if mExecutableValidated
+ OSStatus mExecutableValidResult; // outcome if mExecutableValidated
+
+ // static resource validation state (nested within mValidated/mValid)
+ bool mResourcesValidated; // tried to validate resources
+ OSStatus mResourcesValidResult; // outcome if mResourceValidated or..
+ CollectingContext *mResourcesValidContext; // other outcome
// cached contents
CFRef<CFDataRef> mDir; // code directory data
CFRef<SecTrustRef> mTrust; // outcome of crypto validation (valid or not)
CFRef<CFArrayRef> mCertChain;
CSSM_TP_APPLE_EVIDENCE_INFO *mEvalDetails;
-
- // cached verification policy
- CFRef<SecPolicyRef> mPolicy;
};
if (ctx && ctx->version) // explicitly specified
MacOSError::throwMe(errSecCSStaticCodeNotFound);
}
-
+
// conventional executable bundle: CFBundle identifies an executable for us
if (mMainExecutableURL.take(CFBundleCopyExecutableURL(mBundle))) {
// conventional executable bundle
CFRef<CFURLRef> infoURL = _CFBundleCopyInfoPlistURL(mBundle);
if (!infoURL)
MacOSError::throwMe(errSecCSBadBundleFormat);
+
// focus on the Info.plist (which we know exists) as the nominal "main executable" file
if ((mMainExecutableURL = _CFBundleCopyInfoPlistURL(mBundle))) {
namespace CodeSigning {
+//
+// Highest understood special slot in this CodeDirectory.
+//
+CodeDirectory::SpecialSlot CodeDirectory::maxSpecialSlot() const
+{
+ SpecialSlot slot = this->nSpecialSlots;
+ if (slot > cdSlotMax)
+ slot = cdSlotMax;
+ return slot;
+}
+
+
//
// Canonical filesystem names for select slot numbers.
// These are variously used for filenames, extended attribute names, etc.
char *identifier() { return at<char>(identOffset); }
// main hash array access
+ SpecialSlot maxSpecialSlot() const;
+
unsigned char *operator [] (Slot slot)
{
assert(slot >= int(-nSpecialSlots) && slot < int(nCodeSlots));
}
+//
+// Help mapping API-ish CFString keys to more convenient internal enumerations
+//
+typedef struct {
+ const CFStringRef &cstring;
+ uint enumeration;
+} StringMap;
+
+static uint mapEnum(CFDictionaryRef context, CFStringRef attr, const StringMap *map, uint value = 0)
+{
+ if (context)
+ if (CFTypeRef value = CFDictionaryGetValue(context, attr))
+ for (const StringMap *mp = map; mp->cstring; ++mp)
+ if (CFEqual(mp->cstring, value))
+ return mp->enumeration;
+ return value;
+}
+
+static const StringMap mapType[] = {
+ { kSecAssessmentOperationTypeExecute, kAuthorityExecute },
+ { kSecAssessmentOperationTypeInstall, kAuthorityInstall },
+ { kSecAssessmentOperationTypeOpenDocument, kAuthorityOpenDoc },
+ { NULL }
+};
+
+AuthorityType typeFor(CFDictionaryRef context, AuthorityType type /* = kAuthorityInvalid */)
+{
+ return mapEnum(context, kSecAssessmentContextKeyOperation, mapType, type);
+}
+
+
//
// Open the database (creating it if necessary and possible).
// Note that this isn't creating the schema; we do that on first write.
if (type != kAuthorityExecute)
return false;
- SecCSFlags validationFlags = kSecCSDefaultFlags;
- if (overrideAssessment()) // we'll force the verdict to 'pass' at the end, so don't sweat validating code
- validationFlags = kSecCSBasicValidateOnly;
-
CFRef<SecStaticCodeRef> code;
MacOSError::check(SecStaticCodeCreateWithPath(path, kSecCSDefaultFlags, &code.aref()));
- if (SecStaticCodeCheckValidity(code, validationFlags, NULL) != noErr)
+ if (SecStaticCodeCheckValidity(code, kSecCSBasicValidateOnly, NULL) != noErr)
return false; // quick pass - any error is a cache miss
CFRef<CFDictionaryRef> info;
MacOSError::check(SecCodeCopySigningInformation(code, kSecCSDefaultFlags, &info.aref()));
CFDataRef cdHash = CFDataRef(CFDictionaryGetValue(info, kSecCodeInfoUnique));
// check the cache table for a fast match
- SQLite::Statement cached(*this, "SELECT allow, expires, label, authority FROM object WHERE type = ?1 and hash = ?2;");
- cached.bind(1).integer(type);
- cached.bind(2) = cdHash;
+ SQLite::Statement cached(*this, "SELECT object.allow, authority.label, authority FROM object, authority"
+ " WHERE object.authority = authority.id AND object.type = :type AND object.hash = :hash AND authority.disabled = 0"
+ " AND JULIANDAY('now') < object.expires;");
+ cached.bind(":type").integer(type);
+ cached.bind(":hash") = cdHash;
if (cached.nextRow()) {
bool allow = int(cached[0]);
- const char *label = cached[2];
- SQLite::int64 auth = cached[3];
- bool valid = true;
- if (SQLite3::int64 expires = cached[1])
- valid = time(NULL) <= expires;
- if (valid) {
- SYSPOLICY_ASSESS_CACHE_HIT();
- cfadd(result, "{%O=%B}", kSecAssessmentAssessmentVerdict, allow);
- PolicyEngine::addAuthority(result, label, auth, kCFBooleanTrue);
- return true;
- }
+ const char *label = cached[1];
+ SQLite::int64 auth = cached[2];
+ SYSPOLICY_ASSESS_CACHE_HIT();
+
+ // If its allowed, lets do a full validation unless if
+ // we are overriding the assessement, since that force
+ // the verdict to 'pass' at the end
+
+ if (allow && !overrideAssessment())
+ MacOSError::check(SecStaticCodeCheckValidity(code, kSecCSDefaultFlags, NULL));
+
+ cfadd(result, "{%O=%B}", kSecAssessmentAssessmentVerdict, allow);
+ PolicyEngine::addAuthority(result, label, auth, kCFBooleanTrue);
+ return true;
}
return false;
}
//
-// Purge the object cache of all expired entries
+// Purge the object cache of all expired entries.
+// These are meant to run within the caller's transaction.
//
-void PolicyDatabase::purge(const char *table)
+void PolicyDatabase::purgeAuthority()
+{
+ SQLite::Statement cleaner(*this,
+ "DELETE FROM authority WHERE expires <= JULIANDAY('now');");
+ cleaner.execute();
+}
+
+void PolicyDatabase::purgeObjects()
+{
+ SQLite::Statement cleaner(*this,
+ "DELETE FROM object WHERE expires <= JULIANDAY('now');");
+ cleaner.execute();
+}
+
+void PolicyDatabase::purgeObjects(double priority)
{
SQLite::Statement cleaner(*this,
- "DELETE FROM ?1 WHERE expires < DATE_TIME('now');");
- cleaner.bind(1) = table;
+ "DELETE FROM object WHERE expires <= JULIANDAY('now') OR (SELECT priority FROM authority WHERE id = object.authority) <= :priority;");
+ cleaner.bind(":priority") = priority;
cleaner.execute();
}
static const char defaultDatabase[] = "/var/db/SystemPolicy";
static const char visibleSecurityFlagFile[] = "/var/db/.sp_visible";
+static const double never = 5000000; // expires never (i.e. in the year 8977)
+
typedef SHA1::SDigest ObjectHash;
};
+//
+// Defined flags for authority flags column
+//
+enum {
+ kAuthorityFlagVirtual = 0x0001, // virtual rule (anchoring object records)
+ kAuthorityFlagDefault = 0x0002, // rule is part of the original default set
+ kAuthorityFlagInhibitCache = 0x0004, // never cache outcome of this rule
+};
+
+
+//
+// Mapping/translation to/from API space
+//
+AuthorityType typeFor(CFDictionaryRef context, AuthorityType type = kAuthorityInvalid);
+
+
//
// An open policy database.
// Usually read-only, but can be opened for write by privileged callers.
bool checkCache(CFURLRef path, AuthorityType type, CFMutableDictionaryRef result);
public:
- void purge(const char *table);
+ void purgeAuthority();
+ void purgeObjects();
+ void purgeObjects(double priority);
};
#include <Security/Security.h>
#include <Security/SecRequirementPriv.h>
#include <Security/SecPolicyPriv.h>
+#include <Security/SecTrustPriv.h>
+#include <Security/SecCodeSigner.h>
+#include <Security/cssmapplePriv.h>
+#include <notify.h>
+#include <security_utilities/unix++.h>
+#include "quarantine++.h"
#include <CoreServices/CoreServicesPriv.h>
#undef check // Macro! Yech.
namespace Security {
namespace CodeSigning {
-static const time_t NEGATIVE_HOLD = 60; // seconds for negative cache entries
+static const double NEGATIVE_HOLD = 60.0/86400; // 60 seconds to cache negative outcomes
+
+
+static void authorizeUpdate(SecCSFlags flags, CFDictionaryRef context);
+static void normalizeTarget(CFRef<CFTypeRef> &target, CFDictionary &context, bool signUnsigned = false);
//
CFRef<SecStaticCodeRef> code;
MacOSError::check(SecStaticCodeCreateWithPath(path, kSecCSDefaultFlags, &code.aref()));
- if (flags & kSecAssessmentFlagRequestOrigin) {
- CFRef<CFDictionaryRef> info;
- MacOSError::check(SecCodeCopySigningInformation(code, kSecCSSigningInformation, &info.aref()));
- if (CFArrayRef chain = CFArrayRef(CFDictionaryGetValue(info, kSecCodeInfoCertificates)))
- setOrigin(chain, result);
- }
-
- SecCSFlags validationFlags = kSecCSDefaultFlags;
- if (overrideAssessment()) // we'll force the verdict to 'pass' at the end, so don't sweat validating code
- validationFlags = kSecCSBasicValidateOnly;
+ const SecCSFlags validationFlags = kSecCSEnforceRevocationChecks;
SQLite::Statement query(*this,
- "SELECT allow, requirement, inhibit_cache, expires, id, label FROM authority WHERE type = ?1 ORDER BY priority DESC;");
- query.bind(1).integer(type);
+ "SELECT allow, requirement, id, label, expires, flags, disabled FROM scan_authority"
+ " WHERE type = :type"
+ " ORDER BY priority DESC;");
+ query.bind(":type").integer(type);
+ SQLite3::int64 latentID = 0; // first (highest priority) disabled matching ID
+ std::string latentLabel; // ... and associated label, if any
while (query.nextRow()) {
bool allow = int(query[0]);
const char *reqString = query[1];
- bool inhibit_cache = query[2];
- time_t expires = SQLite::int64(query[3]);
- SQLite3::int64 id = query[4];
- const char *label = query[5];
-
- if (expires && expires < time(NULL)) // no longer active
- continue;
+ SQLite3::int64 id = query[2];
+ const char *label = query[3];
+ double expires = query[4];
+ sqlite3_int64 ruleFlags = query[5];
+ SQLite3::int64 disabled = query[6];
CFRef<SecRequirementRef> requirement;
MacOSError::check(SecRequirementCreateWithString(CFTempString(reqString), kSecCSDefaultFlags, &requirement.aref()));
- switch (OSStatus rc = SecStaticCodeCheckValidity(code, validationFlags, requirement)) {
+
+ OSStatus rc = SecStaticCodeCheckValidity(code, validationFlags | kSecCSBasicValidateOnly, requirement);
+ // ok, so this rule matches lets do a full validation if not overriding assessments
+ if (rc == noErr && !overrideAssessment())
+ rc = SecStaticCodeCheckValidity(code, validationFlags, requirement);
+
+ switch (rc) {
case noErr: // success
break;
case errSecCSUnsigned:
cfadd(result, "{%O=#F}", kSecAssessmentAssessmentVerdict);
addAuthority(result, "no usable signature");
return;
- case errSecCSSignatureFailed:
- case errSecCSSignatureInvalid:
- case errSecCSSignatureUnsupported:
- case errSecCSSignatureNotVerifiable:
- cfadd(result, "{%O=#F}", kSecAssessmentAssessmentVerdict);
- addAuthority(result, "invalid signature");
- return;
case errSecCSReqFailed: // requirement missed, but otherwise okay
continue;
default: // broken in some way; all tests will fail like this so bail out
MacOSError::throwMe(rc);
}
- if (!inhibit_cache && !(flags & kSecAssessmentFlagNoCache)) // cache inhibit
- this->recordOutcome(code, allow, type, expires, id, label);
+ if (disabled) {
+ if (latentID == 0) {
+ latentID = id;
+ if (label)
+ latentLabel = label;
+ }
+ continue; // the loop
+ }
+
+ CFRef<CFDictionaryRef> info; // as needed
+ if (flags & kSecAssessmentFlagRequestOrigin) {
+ if (!info)
+ MacOSError::check(SecCodeCopySigningInformation(code, kSecCSSigningInformation, &info.aref()));
+ if (CFArrayRef chain = CFArrayRef(CFDictionaryGetValue(info, kSecCodeInfoCertificates)))
+ setOrigin(chain, result);
+ }
+ if (!(ruleFlags & kAuthorityFlagInhibitCache) && !(flags & kSecAssessmentFlagNoCache)) { // cache inhibit
+ if (!info)
+ MacOSError::check(SecCodeCopySigningInformation(code, kSecCSSigningInformation, &info.aref()));
+ if (SecTrustRef trust = SecTrustRef(CFDictionaryGetValue(info, kSecCodeInfoTrust))) {
+ CFRef<CFDictionaryRef> xinfo;
+ MacOSError::check(SecTrustCopyExtendedResult(trust, &xinfo.aref()));
+ if (CFDateRef limit = CFDateRef(CFDictionaryGetValue(xinfo, kSecTrustExpirationDate))) {
+ double julianLimit = CFDateGetAbsoluteTime(limit) / 86400.0 + 2451910.5;
+ this->recordOutcome(code, allow, type, min(expires, julianLimit), id);
+ }
+ }
+ }
cfadd(result, "{%O=%B}", kSecAssessmentAssessmentVerdict, allow);
addAuthority(result, label, id);
return;
}
// no applicable authority. Deny by default
+ if (flags & kSecAssessmentFlagRequestOrigin) {
+ CFRef<CFDictionaryRef> info; // as needed
+ MacOSError::check(SecCodeCopySigningInformation(code, kSecCSSigningInformation, &info.aref()));
+ if (CFArrayRef chain = CFArrayRef(CFDictionaryGetValue(info, kSecCodeInfoCertificates)))
+ setOrigin(chain, result);
+ }
if (!(flags & kSecAssessmentFlagNoCache))
- this->recordOutcome(code, false, type, time(NULL) + NEGATIVE_HOLD, 0, NULL);
+ this->recordOutcome(code, false, type, this->julianNow() + NEGATIVE_HOLD, latentID);
cfadd(result, "{%O=%B}", kSecAssessmentAssessmentVerdict, false);
- addAuthority(result, NULL);
+ addAuthority(result, latentLabel.c_str(), latentID);
}
// Certs passed from caller (untrusted), no policy engine yet, no caching (since untrusted).
// The current "policy" is to trust any proper signature.
//
+static CFTypeRef installerPolicy();
+
void PolicyEngine::evaluateInstall(CFURLRef path, SecAssessmentFlags flags, CFDictionaryRef context, CFMutableDictionaryRef result)
{
const AuthorityType type = kAuthorityInstall;
+ SQLite3::int64 latentID = 0; // first (highest priority) disabled matching ID
+ std::string latentLabel; // ... and associated label, if any
Xar xar(cfString(path).c_str());
if (xar) {
if (!xar.isSigned()) {
return;
}
if (CFRef<CFArrayRef> certs = xar.copyCertChain()) {
- CFRef<SecPolicyRef> policy = SecPolicyCreateBasicX509();
+ CFRef<CFTypeRef> policy = installerPolicy();
CFRef<SecTrustRef> trust;
MacOSError::check(SecTrustCreateWithCertificates(certs, policy, &trust.aref()));
// MacOSError::check(SecTrustSetAnchorCertificates(trust, cfEmptyArray())); // no anchors
- MacOSError::check(SecTrustSetOptions(trust, kSecTrustOptionImplicitAnchors));
+ MacOSError::check(SecTrustSetOptions(trust, kSecTrustOptionAllowExpired | kSecTrustOptionImplicitAnchors));
SecTrustResultType trustResult;
MacOSError::check(SecTrustEvaluate(trust, &trustResult));
}
SQLite::Statement query(*this,
- "SELECT allow, requirement, inhibit_cache, expires, id, label FROM authority WHERE type = ?1 ORDER BY priority DESC;");
- query.bind(1).integer(type);
+ "SELECT allow, requirement, id, label, flags, disabled FROM scan_authority"
+ " WHERE type = :type"
+ " ORDER BY priority DESC;");
+ query.bind(":type").integer(type);
while (query.nextRow()) {
bool allow = int(query[0]);
const char *reqString = query[1];
- bool inhibit_cache = query[2];
- time_t expires = SQLite::int64(query[3]);
- SQLite3::int64 id = query[4];
- const char *label = query[5];
-
- if (expires && expires < time(NULL)) // no longer active
- continue;
+ SQLite3::int64 id = query[2];
+ const char *label = query[3];
+ //sqlite_uint64 ruleFlags = query[4];
+ SQLite3::int64 disabled = query[5];
CFRef<SecRequirementRef> requirement;
MacOSError::check(SecRequirementCreateWithString(CFTempString(reqString), kSecCSDefaultFlags, &requirement.aref()));
default: // broken in some way; all tests will fail like this so bail out
MacOSError::throwMe(rc);
}
+ if (disabled) {
+ if (latentID == 0) {
+ latentID = id;
+ if (label)
+ latentLabel = label;
+ }
+ continue; // the loop
+ }
// not adding to the object cache - we could, but it's not likely to be worth it
cfadd(result, "{%O=%B}", kSecAssessmentAssessmentVerdict, allow);
addAuthority(result, label, id);
// no applicable authority. Deny by default
cfadd(result, "{%O=#F}", kSecAssessmentAssessmentVerdict);
- addAuthority(result, NULL);
+ addAuthority(result, latentLabel.c_str(), latentID);
+}
+
+
+//
+// Create a suitable policy array for verification of installer signatures.
+//
+static SecPolicyRef makeCRLPolicy()
+{
+ CFRef<SecPolicyRef> policy;
+ MacOSError::check(SecPolicyCopy(CSSM_CERT_X_509v3, &CSSMOID_APPLE_TP_REVOCATION_CRL, &policy.aref()));
+ CSSM_APPLE_TP_CRL_OPTIONS options;
+ memset(&options, 0, sizeof(options));
+ options.Version = CSSM_APPLE_TP_CRL_OPTS_VERSION;
+ options.CrlFlags = CSSM_TP_ACTION_FETCH_CRL_FROM_NET | CSSM_TP_ACTION_CRL_SUFFICIENT;
+ CSSM_DATA optData = { sizeof(options), (uint8 *)&options };
+ MacOSError::check(SecPolicySetValue(policy, &optData));
+ return policy.yield();
+}
+
+static SecPolicyRef makeOCSPPolicy()
+{
+ CFRef<SecPolicyRef> policy;
+ MacOSError::check(SecPolicyCopy(CSSM_CERT_X_509v3, &CSSMOID_APPLE_TP_REVOCATION_OCSP, &policy.aref()));
+ CSSM_APPLE_TP_OCSP_OPTIONS options;
+ memset(&options, 0, sizeof(options));
+ options.Version = CSSM_APPLE_TP_OCSP_OPTS_VERSION;
+ options.Flags = CSSM_TP_ACTION_OCSP_SUFFICIENT;
+ CSSM_DATA optData = { sizeof(options), (uint8 *)&options };
+ MacOSError::check(SecPolicySetValue(policy, &optData));
+ return policy.yield();
+}
+
+static CFTypeRef installerPolicy()
+{
+ CFRef<SecPolicyRef> base = SecPolicyCreateBasicX509();
+ CFRef<SecPolicyRef> crl = makeCRLPolicy();
+ CFRef<SecPolicyRef> ocsp = makeOCSPPolicy();
+ return makeCFArray(3, base.get(), crl.get(), ocsp.get());
}
|| CFEqual(riskCategory, kLSRiskCategoryUnknown)
|| CFEqual(riskCategory, kLSRiskCategoryMayContainUnsafeExecutable)) {
cfadd(result, "{%O=#T}", kSecAssessmentAssessmentVerdict);
+ addAuthority(result, "_XProtect");
+ } else if (FileQuarantine(cfString(path).c_str()).flag(QTN_FLAG_ASSESSMENT_OK)) {
+ cfadd(result, "{%O=#T}", kSecAssessmentAssessmentVerdict);
+ addAuthority(result, "Prior Assessment");
} else {
cfadd(result, "{%O=#F}", kSecAssessmentAssessmentVerdict);
+ addAuthority(result, "_XProtect");
}
- addAuthority(result, "_XProtect");
addToAuthority(result, kLSDownloadRiskCategoryKey, riskCategory);
return;
}
}
// insufficient information from LS - deny by default
- cfadd(result, "{%O=%F}", kSecAssessmentAssessmentVerdict);
- addAuthority(result, NULL);
+ cfadd(result, "{%O=#F}", kSecAssessmentAssessmentVerdict);
+ addAuthority(result, "Insufficient Context");
}
+//
+// Result-creation helpers
+//
void PolicyEngine::addAuthority(CFMutableDictionaryRef parent, const char *label, SQLite::int64 row, CFTypeRef cacheInfo)
{
CFRef<CFMutableDictionaryRef> auth = makeCFMutableDictionary();
- if (label)
+ if (label && label[0])
cfadd(auth, "{%O=%s}", kSecAssessmentAssessmentSource, label);
if (row)
CFDictionaryAddValue(auth, kSecAssessmentAssessmentAuthorityRow, CFTempNumber(row));
//
// Add a rule to the policy database
//
-bool PolicyEngine::add(CFURLRef path, AuthorityType type, SecAssessmentFlags flags, CFDictionaryRef context)
+bool PolicyEngine::add(CFTypeRef inTarget, AuthorityType type, SecAssessmentFlags flags, CFDictionaryRef context)
{
- if (type != kAuthorityExecute)
- MacOSError::throwMe(errSecCSUnimplemented);
+ // default type to execution
+ if (type == kAuthorityInvalid)
+ type = kAuthorityExecute;
- double priority = 0;
- string label;
- if (context) {
- if (CFTypeRef pri = CFDictionaryGetValue(context, kSecAssessmentUpdateKeyPriority)) {
- if (CFGetTypeID(pri) != CFNumberGetTypeID())
- MacOSError::throwMe(errSecCSBadDictionaryFormat);
- CFNumberGetValue(CFNumberRef(pri), kCFNumberDoubleType, &priority);
- }
- if (CFTypeRef lab = CFDictionaryGetValue(context, kSecAssessmentUpdateKeyLabel)) {
- if (CFGetTypeID(lab) != CFStringGetTypeID())
- MacOSError::throwMe(errSecCSBadDictionaryFormat);
- label = cfString(CFStringRef(lab));
- }
+ authorizeUpdate(flags, context);
+ CFDictionary ctx(context, errSecCSInvalidAttributeValues);
+ CFCopyRef<CFTypeRef> target = inTarget;
+
+ if (type == kAuthorityOpenDoc) {
+ // handle document-open differently: use quarantine flags for whitelisting
+ if (!target || CFGetTypeID(target) != CFURLGetTypeID())
+ MacOSError::throwMe(errSecCSInvalidObjectRef);
+ std::string spath = cfString(target.as<CFURLRef>()).c_str();
+ FileQuarantine qtn(spath.c_str());
+ qtn.setFlag(QTN_FLAG_ASSESSMENT_OK);
+ qtn.applyTo(spath.c_str());
+ return true;
}
+
+ if (type == kAuthorityInstall) {
+ return cfmake<CFDictionaryRef>("{%O=%O}", kSecAssessmentAssessmentAuthorityOverride, CFSTR("virtual install"));
+ }
+
+ // resolve URLs to Requirements
+ normalizeTarget(target, ctx, true);
+
+ // if we now have anything else, we're busted
+ if (!target || CFGetTypeID(target) != SecRequirementGetTypeID())
+ MacOSError::throwMe(errSecCSInvalidObjectRef);
- CFRef<SecStaticCodeRef> code;
- MacOSError::check(SecStaticCodeCreateWithPath(path, kSecCSDefaultFlags, &code.aref()));
- CFRef<CFDictionaryRef> info;
- MacOSError::check(SecCodeCopySigningInformation(code, kSecCSDefaultFlags, &info.aref()));
- CFRef<SecRequirementRef> dr;
- MacOSError::check(SecCodeCopyDesignatedRequirement(code, kSecCSDefaultFlags, &dr.aref()));
+ double priority = 0;
+ string label;
+ bool allow = true;
+ double expires = never;
+ string remarks;
+
+ if (CFNumberRef pri = ctx.get<CFNumberRef>(kSecAssessmentUpdateKeyPriority))
+ CFNumberGetValue(pri, kCFNumberDoubleType, &priority);
+ if (CFStringRef lab = ctx.get<CFStringRef>(kSecAssessmentUpdateKeyLabel))
+ label = cfString(lab);
+ if (CFDateRef time = ctx.get<CFDateRef>(kSecAssessmentUpdateKeyExpires))
+ // we're using Julian dates here; convert from CFDate
+ expires = CFDateGetAbsoluteTime(time) / 86400.0 + 2451910.5;
+ if (CFBooleanRef allowing = ctx.get<CFBooleanRef>(kSecAssessmentUpdateKeyAllow))
+ allow = allowing == kCFBooleanTrue;
+ if (CFStringRef rem = ctx.get<CFStringRef>(kSecAssessmentUpdateKeyRemarks))
+ remarks = cfString(rem);
CFRef<CFStringRef> requirementText;
- MacOSError::check(SecRequirementCopyString(dr, kSecCSDefaultFlags, &requirementText.aref()));
+ MacOSError::check(SecRequirementCopyString(target.as<SecRequirementRef>(), kSecCSDefaultFlags, &requirementText.aref()));
+ SQLite::Transaction xact(*this, SQLite3::Transaction::deferred, "add_rule");
SQLite::Statement insert(*this,
- "INSERT INTO authority (type, allow, requirement, priority, label) VALUES (?1, ?2, ?3, ?4, ?5);");
- insert.bind(1).integer(type);
- insert.bind(2).integer(true);
- insert.bind(3) = requirementText.get();
- insert.bind(4) = priority;
- insert.bind(5) = label.c_str();
+ "INSERT INTO authority (type, allow, requirement, priority, label, expires, remarks)"
+ " VALUES (:type, :allow, :requirement, :priority, :label, :expires, :remarks);");
+ insert.bind(":type").integer(type);
+ insert.bind(":allow").integer(allow);
+ insert.bind(":requirement") = requirementText.get();
+ insert.bind(":priority") = priority;
+ if (!label.empty())
+ insert.bind(":label") = label.c_str();
+ insert.bind(":expires") = expires;
+ if (!remarks.empty())
+ insert.bind(":remarks") = remarks.c_str();
insert.execute();
+ this->purgeObjects(priority);
+ xact.commit();
+ notify_post(kNotifySecAssessmentUpdate);
return true;
}
+//
+// Perform an action on existing authority rule(s)
+//
+bool PolicyEngine::manipulateRules(const std::string &stanza,
+ CFTypeRef inTarget, AuthorityType type, SecAssessmentFlags flags, CFDictionaryRef context)
+{
+ authorizeUpdate(flags, context);
+ CFDictionary ctx(context, errSecCSInvalidAttributeValues);
+ CFCopyRef<CFTypeRef> target = inTarget;
+ normalizeTarget(target, ctx);
+
+ string label;
+
+ if (CFStringRef lab = ctx.get<CFStringRef>(kSecAssessmentUpdateKeyLabel))
+ label = cfString(CFStringRef(lab));
+
+ SQLite::Transaction xact(*this, SQLite3::Transaction::deferred, "rule_change");
+ SQLite::Statement action(*this);
+ if (!target) {
+ if (label.empty()) // underspecified
+ MacOSError::throwMe(errSecCSInvalidObjectRef);
+ if (type == kAuthorityInvalid) {
+ action.query(stanza + " WHERE label = :label");
+ } else {
+ action.query(stanza + " WHERE type = :type AND label = :label");
+ action.bind(":type").integer(type);
+ }
+ action.bind(":label") = label.c_str();
+ } else if (CFGetTypeID(target) == CFNumberGetTypeID()) {
+ action.query(stanza + " WHERE id = :id");
+ action.bind(":id").integer(cfNumber<uint64_t>(target.as<CFNumberRef>()));
+ } else if (CFGetTypeID(target) == SecRequirementGetTypeID()) {
+ if (type == kAuthorityInvalid)
+ type = kAuthorityExecute;
+ CFRef<CFStringRef> requirementText;
+ MacOSError::check(SecRequirementCopyString(target.as<SecRequirementRef>(), kSecCSDefaultFlags, &requirementText.aref()));
+ action.query(stanza + " WHERE type = :type AND requirement = :requirement");
+ action.bind(":type").integer(type);
+ action.bind(":requirement") = requirementText.get();
+ } else
+ MacOSError::throwMe(errSecCSInvalidObjectRef);
+
+ action.execute();
+ unsigned int changes = this->changes(); // latch change count
+ // We MUST purge objects with priority <= MAX(priority of any changed rules);
+ // but for now we just get lazy and purge them ALL.
+ if (changes) {
+ this->purgeObjects(1.0E100);
+ xact.commit();
+ notify_post(kNotifySecAssessmentUpdate);
+ return true;
+ }
+ // no change; return an error
+ MacOSError::throwMe(errSecCSNoMatches);
+}
+
+
+bool PolicyEngine::remove(CFTypeRef target, AuthorityType type, SecAssessmentFlags flags, CFDictionaryRef context)
+{
+ if (type == kAuthorityOpenDoc) {
+ // handle document-open differently: use quarantine flags for whitelisting
+ authorizeUpdate(flags, context);
+ if (!target || CFGetTypeID(target) != CFURLGetTypeID())
+ MacOSError::throwMe(errSecCSInvalidObjectRef);
+ std::string spath = cfString(CFURLRef(target)).c_str();
+ FileQuarantine qtn(spath.c_str());
+ qtn.clearFlag(QTN_FLAG_ASSESSMENT_OK);
+ qtn.applyTo(spath.c_str());
+ return true;
+ }
+ return manipulateRules("DELETE FROM authority", target, type, flags, context);
+}
+
+bool PolicyEngine::enable(CFTypeRef target, AuthorityType type, SecAssessmentFlags flags, CFDictionaryRef context)
+{
+ return manipulateRules("UPDATE authority SET disabled = 0", target, type, flags, context);
+}
+
+bool PolicyEngine::disable(CFTypeRef target, AuthorityType type, SecAssessmentFlags flags, CFDictionaryRef context)
+{
+ return manipulateRules("UPDATE authority SET disabled = 1", target, type, flags, context);
+}
+
+
+bool PolicyEngine::update(CFTypeRef target, SecAssessmentFlags flags, CFDictionaryRef context)
+{
+ AuthorityType type = typeFor(context, kAuthorityInvalid);
+ CFStringRef edit = CFStringRef(CFDictionaryGetValue(context, kSecAssessmentContextKeyUpdate));
+ if (CFEqual(edit, kSecAssessmentUpdateOperationAdd))
+ return this->add(target, type, flags, context);
+ else if (CFEqual(edit, kSecAssessmentUpdateOperationRemove))
+ return this->remove(target, type, flags, context);
+ else if (CFEqual(edit, kSecAssessmentUpdateOperationEnable))
+ return this->enable(target, type, flags, context);
+ else if (CFEqual(edit, kSecAssessmentUpdateOperationDisable))
+ return this->disable(target, type, flags, context);
+ else
+ MacOSError::throwMe(errSecCSInvalidAttributeValues);
+}
+
+
//
// Fill in extra information about the originator of cryptographic credentials found - if any
//
//
// Take an assessment outcome and record it in the object cache
//
-void PolicyEngine::recordOutcome(SecStaticCodeRef code, bool allow, AuthorityType type, time_t expires, int authority, const char *label)
+void PolicyEngine::recordOutcome(SecStaticCodeRef code, bool allow, AuthorityType type, double expires, int authority)
{
CFRef<CFDictionaryRef> info;
MacOSError::check(SecCodeCopySigningInformation(code, kSecCSDefaultFlags, &info.aref()));
CFDataRef cdHash = CFDataRef(CFDictionaryGetValue(info, kSecCodeInfoUnique));
+ assert(cdHash); // was signed
CFRef<CFURLRef> path;
MacOSError::check(SecCodeCopyPath(code, kSecCSDefaultFlags, &path.aref()));
- //@@@ should really be OR REPLACE IF EXPIRED... does it matter? @@@
+ assert(expires);
SQLite::Transaction xact(*this, SQLite3::Transaction::deferred, "caching");
SQLite::Statement insert(*this,
- "INSERT OR REPLACE INTO object (type, allow, hash, expires, authority, label, path) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)");
- insert.bind(1).integer(type);
- insert.bind(2).integer(allow);
- insert.bind(3) = cdHash;
- if (expires)
- insert.bind(4).integer(expires);
- insert.bind(5).integer(authority);
- if (label)
- insert.bind(6) = label;
- insert.bind(7) = cfString(path).c_str();
+ "INSERT OR REPLACE INTO object (type, allow, hash, expires, path, authority)"
+ " VALUES (:type, :allow, :hash, :expires, :path,"
+ " CASE :authority WHEN 0 THEN (SELECT id FROM authority WHERE label = 'No Matching Rule') ELSE :authority END"
+ " );");
+ insert.bind(":type").integer(type);
+ insert.bind(":allow").integer(allow);
+ insert.bind(":hash") = cdHash;
+ insert.bind(":expires") = expires;
+ insert.bind(":path") = cfString(path).c_str();
+ insert.bind(":authority").integer(authority);
insert.execute();
xact.commit();
}
+//
+// Perform update authorization processing.
+// Throws an exception if authorization is denied.
+//
+static void authorizeUpdate(SecCSFlags flags, CFDictionaryRef context)
+{
+ AuthorizationRef authorization = NULL;
+
+ if (context)
+ if (CFTypeRef authkey = CFDictionaryGetValue(context, kSecAssessmentUpdateKeyAuthorization))
+ if (CFGetTypeID(authkey) == CFDataGetTypeID()) {
+ CFDataRef authdata = CFDataRef(authkey);
+ MacOSError::check(AuthorizationCreateFromExternalForm((AuthorizationExternalForm *)CFDataGetBytePtr(authdata), &authorization));
+ }
+ if (authorization == NULL)
+ MacOSError::check(AuthorizationCreate(NULL, NULL, kAuthorizationFlagDefaults, &authorization));
+
+ AuthorizationItem right[] = {
+ { "com.apple.security.assessment.update", 0, NULL, 0 }
+ };
+ AuthorizationRights rights = { sizeof(right) / sizeof(right[0]), right };
+ MacOSError::check(AuthorizationCopyRights(authorization, &rights, NULL,
+ kAuthorizationFlagExtendRights | kAuthorizationFlagInteractionAllowed, NULL));
+
+ MacOSError::check(AuthorizationFree(authorization, kAuthorizationFlagDefaults));
+}
+
+
+//
+// Perform common argument normalizations for update operations
+//
+static void normalizeTarget(CFRef<CFTypeRef> &target, CFDictionary &context, bool signUnsigned)
+{
+ // turn CFURLs into (designated) SecRequirements
+ if (target && CFGetTypeID(target) == CFURLGetTypeID()) {
+ CFRef<SecStaticCodeRef> code;
+ MacOSError::check(SecStaticCodeCreateWithPath(target.as<CFURLRef>(), kSecCSDefaultFlags, &code.aref()));
+ CFRef<SecRequirementRef> requirement;
+ switch (OSStatus rc = SecCodeCopyDesignatedRequirement(code, kSecCSDefaultFlags, (SecRequirementRef *)&target.aref())) {
+ case noErr:
+ break;
+ case errSecCSUnsigned:
+ if (signUnsigned) {
+ // Ad-hoc sign the code in the system database. This requires root privileges.
+ CFRef<SecCodeSignerRef> signer;
+ CFTemp<CFDictionaryRef> arguments("{%O=#N, %O=#N}", kSecCodeSignerDetached, kSecCodeSignerIdentity);
+ MacOSError::check(SecCodeSignerCreate(arguments, kSecCSDefaultFlags, &signer.aref()));
+ MacOSError::check(SecCodeSignerAddSignature(signer, code, kSecCSDefaultFlags));
+ MacOSError::check(SecCodeCopyDesignatedRequirement(code, kSecCSDefaultFlags, (SecRequirementRef *)&target.aref()));
+ break;
+ }
+ // fall through
+ default:
+ MacOSError::check(rc);
+ }
+ if (context.get(kSecAssessmentUpdateKeyRemarks) == NULL) {
+ // no explicit remarks; add one with the path
+ CFRef<CFURLRef> path;
+ MacOSError::check(SecCodeCopyPath(code, kSecCSDefaultFlags, &path.aref()));
+ CFMutableDictionaryRef dict = makeCFMutableDictionary(context.get());
+ CFDictionaryAddValue(dict, kSecAssessmentUpdateKeyRemarks, CFTempString(cfString(path)));
+ context.take(dict);
+ }
+ }
+}
+
+
} // end namespace CodeSigning
} // end namespace Security
public:
void evaluate(CFURLRef path, AuthorityType type, SecAssessmentFlags flags, CFDictionaryRef context, CFMutableDictionaryRef result);
- bool add(CFURLRef path, AuthorityType type, SecAssessmentFlags flags, CFDictionaryRef context);
+ bool update(CFTypeRef target, SecAssessmentFlags flags, CFDictionaryRef context);
+ bool add(CFTypeRef target, AuthorityType type, SecAssessmentFlags flags, CFDictionaryRef context);
+ bool remove(CFTypeRef target, AuthorityType type, SecAssessmentFlags flags, CFDictionaryRef context);
+ bool enable(CFTypeRef target, AuthorityType type, SecAssessmentFlags flags, CFDictionaryRef context);
+ bool disable(CFTypeRef target, AuthorityType type, SecAssessmentFlags flags, CFDictionaryRef context);
+
+public:
static void addAuthority(CFMutableDictionaryRef parent, const char *label, SQLite::int64 row = 0, CFTypeRef cacheInfo = NULL);
static void addToAuthority(CFMutableDictionaryRef parent, CFStringRef key, CFTypeRef value);
void evaluateCode(CFURLRef path, SecAssessmentFlags flags, CFDictionaryRef context, CFMutableDictionaryRef result);
void evaluateInstall(CFURLRef path, SecAssessmentFlags flags, CFDictionaryRef context, CFMutableDictionaryRef result);
void evaluateDocOpen(CFURLRef path, SecAssessmentFlags flags, CFDictionaryRef context, CFMutableDictionaryRef result);
+
+ bool manipulateRules(const std::string &stanza,
+ CFTypeRef target, AuthorityType type, SecAssessmentFlags flags, CFDictionaryRef context);
void setOrigin(CFArrayRef chain, CFMutableDictionaryRef result);
- void recordOutcome(SecStaticCodeRef code, bool allow, AuthorityType type, time_t expires, int authority, const char *label);
+ void recordOutcome(SecStaticCodeRef code, bool allow, AuthorityType type, double expires, int authority);
};
--- /dev/null
+/*
+ * Copyright (c) 2011 Apple Inc. All Rights Reserved.
+ *
+ * @APPLE_LICENSE_HEADER_START@
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apple Public Source License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://www.opensource.apple.com/apsl/ and read it before using this
+ * file.
+ *
+ * The 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.
+ *
+ * @APPLE_LICENSE_HEADER_END@
+ */
+
+//
+// xar++ - interface to XAR-format archive files
+//
+#include "quarantine++.h"
+
+
+namespace Security {
+namespace CodeSigning {
+
+
+//
+// Check the int result of a qtn API call.
+// If the error is "not quarantined," note in the object (no error).
+// Other qtn-specific errors are arbitrarily mapped to ENOSYS (this isn't
+// important enough to subclass CommonError).
+//
+void FileQuarantine::check(int err)
+{
+ switch (err) {
+ case 0:
+ mQuarantined = true;
+ break;
+ case QTN_NOT_QUARANTINED:
+ mQuarantined = false;
+ return;
+ default: // some flavor of quarantine-not-available
+ UnixError::throwMe(err);
+ }
+}
+
+
+FileQuarantine::~FileQuarantine()
+{
+ if (mQtn)
+ qtn_file_free(mQtn);
+}
+
+
+FileQuarantine::FileQuarantine(const char *path)
+{
+ if (!(mQtn = qtn_file_alloc()))
+ UnixError::throwMe();
+ check(qtn_file_init_with_path(mQtn, path));
+}
+
+FileQuarantine::FileQuarantine(int fd)
+{
+ if (!(mQtn = qtn_file_alloc()))
+ UnixError::throwMe();
+ check(qtn_file_init_with_fd(mQtn, fd));
+}
+
+
+void FileQuarantine::setFlags(uint32_t flags)
+{
+ if (mQuarantined)
+ check(qtn_file_set_flags(mQtn, flags));
+}
+
+void FileQuarantine::setFlag(uint32_t flag)
+{
+ if (mQuarantined)
+ setFlags(flags() | flag);
+}
+
+void FileQuarantine::clearFlag(uint32_t flag)
+{
+ if (mQuarantined)
+ setFlags(flags() & ~flag);
+}
+
+void FileQuarantine::applyTo(const char *path)
+{
+ check(qtn_file_apply_to_path(mQtn, path));
+}
+
+void FileQuarantine::applyTo(int fd)
+{
+ check(qtn_file_apply_to_fd(mQtn, fd));
+}
+
+
+} // end namespace CodeSigning
+} // end namespace Security
--- /dev/null
+/*
+ * Copyright (c) 2011 Apple Inc. All Rights Reserved.
+ *
+ * @APPLE_LICENSE_HEADER_START@
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apple Public Source License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://www.opensource.apple.com/apsl/ and read it before using this
+ * file.
+ *
+ * The 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.
+ *
+ * @APPLE_LICENSE_HEADER_END@
+ */
+
+//
+// quarantine++ - interface to XAR-format archive files
+//
+#ifndef _H_QUARANTINEPLUSPLUS
+#define _H_QUARANTINEPLUSPLUS
+
+#include <security_utilities/utilities.h>
+#include <CoreFoundation/CoreFoundation.h>
+
+extern "C" {
+#include <quarantine.h>
+}
+
+namespace Security {
+namespace CodeSigning {
+
+
+//
+// A file quarantine object
+//
+class FileQuarantine {
+public:
+ FileQuarantine(const char *path);
+ FileQuarantine(int fd);
+ virtual ~FileQuarantine();
+
+ uint32_t flags() const
+ { return qtn_file_get_flags(mQtn); }
+ bool flag(uint32_t f) const
+ { return this->flags() & f; }
+
+ void setFlags(uint32_t flags);
+ void setFlag(uint32_t flag);
+ void clearFlag(uint32_t flag);
+
+ void applyTo(const char *path);
+ void applyTo(int fd);
+
+ operator bool() const { return mQtn != 0; }
+ bool quarantined() const { return mQuarantined; }
+
+private:
+ void check(int err);
+
+private:
+ qtn_file_t mQtn; // qtn handle
+ bool mQuarantined; // has quarantine information
+};
+
+
+} // end namespace CodeSigning
+} // end namespace Security
+
+#endif // !_H_QUARANTINEPLUSPLUS
} catch (const CommonError &err) {
if (debug) {
char errstr[80];
- snprintf(errstr, sizeof(errstr), " !! error %ld !!", err.osStatus());
+ snprintf(errstr, sizeof(errstr), " !! error %ld !!", (unsigned long)err.osStatus());
return dumper.value() + errstr;
}
throw;
_kSecAssessmentOperationTypeInstall
_kSecAssessmentOperationTypeOpenDocument
_kSecAssessmentContextKeyUpdate
-_kSecAssessmentUpdateOperationAddFile
-_kSecAssessmentUpdateOperationRemoveFile
+_kSecAssessmentUpdateOperationAdd
+_kSecAssessmentUpdateOperationRemove
+_kSecAssessmentUpdateOperationEnable
+_kSecAssessmentUpdateOperationDisable
+_kSecAssessmentUpdateKeyAuthorization
+_kSecAssessmentUpdateKeyAllow
+_kSecAssessmentUpdateKeyExpires
_kSecAssessmentUpdateKeyPriority
_kSecAssessmentUpdateKeyLabel
+_kSecAssessmentUpdateKeyRemarks
_kSecAssessmentAssessmentAuthority
_kSecAssessmentAssessmentAuthorityRow
_kSecAssessmentAssessmentFromCache
--
-- This is currently for sqlite3
--
+-- NOTES:
+-- Dates are uniformly in julian form. We use 5000000 as the canonical "never" expiration
+-- value; that's a day in the year 8977.
+--
+PRAGMA user_version = 1;
PRAGMA foreign_keys = true;
+PRAGMA legacy_file_format = false;
+PRAGMA recursive_triggers = true;
+
+--
+-- The feature table hold configuration features and options
+--
+CREATE TABLE feature (
+ id INTEGER PRIMARY KEY, -- canononical
+ name TEXT NOT NULL UNIQUE, -- name of option
+ value TEXT NULL, -- value of option, if any
+ remarks TEXT NOT NULL -- optional remarks string
+);
--
-- The primary authority. This table is conceptually scanned
--- in priority order, with the highest-priority matching record
+-- in priority order, with the highest-priority matching enabled record
-- determining the outcome.
--
CREATE TABLE authority (
- id INTEGER PRIMARY KEY,
- type INTEGER NOT NULL,
- requirement TEXT NOT NULL,
- allow INTEGER NOT NULL,
- expires INTEGER NULL,
- priority REAL NOT NULL DEFAULT (0),
- label TEXT NULL,
- inhibit_cache INTEGER NULL,
- flags INTEGER NOT NULL DEFAULT (0),
+ id INTEGER PRIMARY KEY AUTOINCREMENT, -- canonical
+ version INTEGER NOT NULL DEFAULT (1) -- semantic version of this rule
+ CHECK (version > 0),
+ type INTEGER NOT NULL, -- operation type
+ requirement TEXT NULL -- code requirement
+ CHECK ((requirement IS NULL) = ((flags & 1) != 0)),
+ allow INTEGER NOT NULL DEFAULT (1) -- allow (1) or deny (0)
+ CHECK (allow = 0 OR allow = 1),
+ disabled INTEGER NOT NULL DEFAULT (0) -- disable count (stacks; enabled if zero)
+ CHECK (disabled >= 0),
+ expires FLOAT NOT NULL DEFAULT (5000000), -- expiration of rule authority (Julian date)
+ priority REAL NOT NULL DEFAULT (0), -- rule priority (full float)
+ label TEXT NULL, -- text label for authority rule
+ flags INTEGER NOT NULL DEFAULT (0), -- amalgamated binary flags
-- following fields are for documentation only
- remarks TEXT NULL
+ ctime FLOAT NOT NULL DEFAULT (JULIANDAY('now')), -- rule creation time (Julian)
+ mtime FLOAT NOT NULL DEFAULT (JULIANDAY('now')), -- time rule was last changed (Julian)
+ user TEXT NULL, -- user requesting this rule (NULL if unknown)
+ remarks TEXT NULL -- optional remarks string
);
--- any Apple-signed installers of any kind
-insert into authority (type, allow, priority, label, requirement)
- values (2, 1, -1, 'Apple Installer', 'anchor apple generic');
+-- index
+CREATE INDEX authority_type ON authority (type);
+CREATE INDEX authority_priority ON authority (priority);
+CREATE INDEX authority_expires ON authority (expires);
+
+-- update mtime if a record is changed
+CREATE TRIGGER authority_update AFTER UPDATE ON authority
+BEGIN
+ UPDATE authority SET mtime = JULIANDAY('now') WHERE id = old.id;
+END;
+
+-- rules that are actively considered
+CREATE VIEW active_authority AS
+SELECT * from authority
+WHERE disabled = 0 AND JULIANDAY('now') < expires AND (flags & 1) = 0;
+
+-- rules subject to priority scan: active_authority but including disabled rules
+CREATE VIEW scan_authority AS
+SELECT * from authority
+WHERE JULIANDAY('now') < expires AND (flags & 1) = 0;
+
+
+--
+-- Initial canonical contents of a fresh database
+--
+
+-- virtual rule anchoring negative cache entries (no rule found)
+insert into authority (type, allow, priority, flags, label)
+ values (1, 0, -1.0E100, 1, 'No Matching Rule');
+
+-- any Apple-signed installers except Developer ID
+insert into authority (type, allow, priority, flags, label, requirement)
+ values (2, 1, -1, 2, 'Apple Installer', 'anchor apple generic and ! certificate 1[field.1.2.840.113635.100.6.2.6]');
-- Apple code signing
-insert into authority (type, allow, label, requirement)
- values (1, 1, 'Apple', 'anchor apple');
+insert into authority (type, allow, flags, label, requirement)
+ values (1, 1, 2, 'Apple System', 'anchor apple');
-- Mac App Store signing
-insert into authority (type, allow, label, requirement)
- values (1, 1, 'Mac App Store', 'anchor apple generic and certificate leaf[field.1.2.840.113635.100.6.1.9] exists');
+insert into authority (type, allow, flags, label, requirement)
+ values (1, 1, 2, 'Mac App Store', 'anchor apple generic and certificate leaf[field.1.2.840.113635.100.6.1.9] exists');
-insert into authority (type, allow, label, requirement)
- values (1, 1, 'Developer Seed', 'anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.13] exists');
-insert into authority (type, allow, label, requirement)
- values (2, 1, 'Developer Seed', 'anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.14] exists');
+-- Caspian code and archive signing
+insert into authority (type, allow, flags, label, requirement)
+ values (1, 1, 2, 'Developer ID', 'anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.13] exists');
+insert into authority (type, allow, flags, label, requirement)
+ values (2, 1, 2, 'Developer ID', 'anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.14] exists');
--
-- for individual objects (by object hash). Entries come from
-- full evaluations of authority records, or by explicitly inserting
-- override rules that preempt the normal authority.
+-- EACH object record must have a parent authority record from which it is derived;
+-- this may be a normal authority rule or an override rule. If the parent rule is deleted,
+-- all objects created from it are automatically removed (by sqlite itself).
--
CREATE TABLE object (
- id INTEGER PRIMARY KEY,
- type INTEGER NOT NULL,
- hash CDHASH NOT NULL UNIQUE,
- allow INTEGER NOT NULL,
- expires INTEGER NULL,
- label TEXT NULL,
- authority INTEGER NULL REFERENCES authority(id),
+ id INTEGER PRIMARY KEY, -- canonical
+ type INTEGER NOT NULL, -- operation type
+ hash CDHASH NOT NULL, -- canonical hash of object
+ allow INTEGER NOT NULL, -- allow (1) or deny (0)
+ expires FLOAT NOT NULL DEFAULT (5000000), -- expiration of object entry
+ authority INTEGER NOT NULL -- governing authority rule
+ REFERENCES authority(id) ON DELETE CASCADE,
-- following fields are for documentation only
- path TEXT NULL,
- created INTEGER NOT NULL default (strftime('%s','now')),
- remarks TEXT NULL
+ path TEXT NULL, -- path of object at record creation time
+ ctime FLOAT NOT NULL DEFAULT (JULIANDAY('now')), -- record creation time
+ mtime FLOAT NOT NULL DEFAULT (JULIANDAY('now')), -- record modification time
+ remarks TEXT NULL -- optional remarks string
);
+
+-- index
+CREATE INDEX object_type ON object (type);
+CREATE INDEX object_expires ON object (expires);
+CREATE UNIQUE INDEX object_hash ON object (hash);
+
+-- update mtime if a record is changed
+CREATE TRIGGER object_update AFTER UPDATE ON object
+BEGIN
+ UPDATE object SET mtime = JULIANDAY('now') WHERE id = old.id;
+END;
+
+
+--
+-- Some useful views on objects. These are for administration; they are not used by the assessor.
+--
+CREATE VIEW object_state AS
+SELECT object.id, object.type, object.allow,
+ CASE object.expires WHEN 5000000 THEN NULL ELSE STRFTIME('%Y-%m-%d %H:%M:%f', object.expires, 'localtime') END AS expiration,
+ (object.expires - JULIANDAY('now')) * 86400 as remaining,
+ authority.label,
+ object.authority,
+ object.path,
+ object.ctime,
+ authority.requirement,
+ authority.disabled,
+ object.remarks
+FROM object, authority
+WHERE object.authority = authority.id;
#include <syslog.h>
#include <CoreFoundation/CoreFoundation.h>
#include <security_utilities/cfutilities.h>
+#include <Security/CodeSigning.h>
namespace Security {
static void copyCFDictionary(const void *key, const void *value, void *ctx)
{
CFMutableDictionaryRef target = CFMutableDictionaryRef(ctx);
- if (CFEqual(key, kSecAssessmentContextKeyCertificates)) // legacy; drop it
+ if (CFEqual(key, kSecAssessmentContextKeyCertificates)) // obsolete
return;
if (CFGetTypeID(value) == CFURLGetTypeID()) {
CFRef<CFStringRef> path = CFURLCopyFileSystemPath(CFURLRef(value), kCFURLPOSIXPathStyle);
CFDictionaryApplyFunction(context, copyCFDictionary, ctx);
CFRef<CFDataRef> contextData = makeCFData(CFDictionaryRef(ctx));
xpc_dictionary_set_data(msg, "context", CFDataGetBytePtr(contextData), CFDataGetLength(contextData));
+
msg.send();
+
+ if (int64_t error = xpc_dictionary_get_int64(msg, "error"))
+ MacOSError::throwMe(error);
+
size_t resultLength;
const void *resultData = xpc_dictionary_get_data(msg, "result", &resultLength);
CFRef<CFDictionaryRef> resultDict = makeCFDictionaryFrom(resultData, resultLength);
}
+bool xpcEngineUpdate(CFTypeRef target, uint flags, CFDictionaryRef context)
+{
+ Message msg("update");
+ // target can be NULL, a CFURLRef, a SecRequirementRef, or a CFNumberRef
+ if (target) {
+ if (CFGetTypeID(target) == CFNumberGetTypeID())
+ xpc_dictionary_set_uint64(msg, "rule", cfNumber<int64_t>(CFNumberRef(target)));
+ else if (CFGetTypeID(target) == CFURLGetTypeID())
+ xpc_dictionary_set_string(msg, "url", cfString(CFURLRef(target)).c_str());
+ else if (CFGetTypeID(target) == SecRequirementGetTypeID()) {
+ CFRef<CFDataRef> data;
+ MacOSError::check(SecRequirementCopyData(SecRequirementRef(target), kSecCSDefaultFlags, &data.aref()));
+ xpc_dictionary_set_data(msg, "requirement", CFDataGetBytePtr(data), CFDataGetLength(data));
+ } else
+ MacOSError::throwMe(errSecCSInvalidObjectRef);
+ }
+ xpc_dictionary_set_int64(msg, "flags", flags);
+ CFRef<CFMutableDictionaryRef> ctx = makeCFMutableDictionary();
+ if (context)
+ CFDictionaryApplyFunction(context, copyCFDictionary, ctx);
+ AuthorizationRef localAuthorization = NULL;
+ if (CFDictionaryGetValue(ctx, kSecAssessmentUpdateKeyAuthorization) == NULL) { // no caller-provided authorization
+ MacOSError::check(AuthorizationCreate(NULL, NULL, kAuthorizationFlagDefaults, &localAuthorization));
+ AuthorizationExternalForm extForm;
+ MacOSError::check(AuthorizationMakeExternalForm(localAuthorization, &extForm));
+ CFDictionaryAddValue(ctx, kSecAssessmentUpdateKeyAuthorization, CFTempData(&extForm, sizeof(extForm)));
+ }
+ CFRef<CFDataRef> contextData = makeCFData(CFDictionaryRef(ctx));
+ xpc_dictionary_set_data(msg, "context", CFDataGetBytePtr(contextData), CFDataGetLength(contextData));
+
+ msg.send();
+
+ if (localAuthorization)
+ AuthorizationFree(localAuthorization, kAuthorizationFlagDefaults);
+
+ if (int64_t error = xpc_dictionary_get_int64(msg, "error"))
+ MacOSError::throwMe(error);
+
+ return true;
+}
+
+
bool xpcEngineControl(const char *control)
{
Message msg("control");
void xpcEngineAssess(CFURLRef path, uint flags, CFDictionaryRef context, CFMutableDictionaryRef result);
+bool xpcEngineUpdate(CFTypeRef target, uint flags, CFDictionaryRef context);
bool xpcEngineControl(const char *name);
buildPhases = (
C26AC0F0143BCF18001C98CE /* ShellScript */,
C26AC0F4143BD1C4001C98CE /* CopyFiles */,
+ C2F24DFE14BCBBF200309FCD /* ShellScript */,
);
dependencies = (
);
C2E911E20ADEBE3200275CB2 /* resources.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C2E911E00ADEBE3200275CB2 /* resources.cpp */; };
C2EF10100A49BD89005A44BB /* renum.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C2EF100E0A49BD89005A44BB /* renum.cpp */; };
C2EF10130A49BD89005A44BB /* renum.h in Headers */ = {isa = PBXBuildFile; fileRef = C2EF100F0A49BD89005A44BB /* renum.h */; settings = {ATTRIBUTES = (Public, ); }; };
+ C2F4439A14C626D4000A01E6 /* quarantine++.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C2F4439814C626D4000A01E6 /* quarantine++.cpp */; };
+ C2F4439B14C626D4000A01E6 /* quarantine++.h in Headers */ = {isa = PBXBuildFile; fileRef = C2F4439914C626D4000A01E6 /* quarantine++.h */; };
C2F6566E0BCBFB250078779E /* cserror.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C2F6566C0BCBFB250078779E /* cserror.cpp */; };
C2F656930BCBFFF40078779E /* cserror.h in Headers */ = {isa = PBXBuildFile; fileRef = C2F6566D0BCBFB250078779E /* cserror.h */; settings = {ATTRIBUTES = (Public, ); }; };
FEB30C9310DAC89D00557BA2 /* SecTask.c in Sources */ = {isa = PBXBuildFile; fileRef = FEB30C9210DAC89D00557BA2 /* SecTask.c */; };
C2E911E10ADEBE3200275CB2 /* resources.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = resources.h; sourceTree = "<group>"; };
C2EF100E0A49BD89005A44BB /* renum.cpp */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.cpp.cpp; path = renum.cpp; sourceTree = "<group>"; };
C2EF100F0A49BD89005A44BB /* renum.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = renum.h; sourceTree = "<group>"; };
+ C2F4439814C626D4000A01E6 /* quarantine++.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = "quarantine++.cpp"; sourceTree = "<group>"; };
+ C2F4439914C626D4000A01E6 /* quarantine++.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "quarantine++.h"; sourceTree = "<group>"; };
C2F6071B107D575700A83618 /* codesign-watch.d */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.dtrace; name = "codesign-watch.d"; path = "dtrace/codesign-watch.d"; sourceTree = SOURCE_ROOT; };
C2F6566C0BCBFB250078779E /* cserror.cpp */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.cpp.cpp; path = cserror.cpp; sourceTree = "<group>"; };
C2F6566D0BCBFB250078779E /* cserror.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = cserror.h; sourceTree = "<group>"; };
C2A976A80B8A2E36008B4EA0 /* csutilities.cpp */,
C235340E145F1B050073F964 /* xar++.h */,
C2353410145F1B110073F964 /* xar++.cpp */,
+ C2F4439914C626D4000A01E6 /* quarantine++.h */,
+ C2F4439814C626D4000A01E6 /* quarantine++.cpp */,
);
name = "Local Utilities";
sourceTree = "<group>";
C273606F1433F09000A9A5FF /* SecAssessment.h in Headers */,
C2A436160F2133B2007A41A6 /* slcrep.h in Headers */,
C24EABAB1421432800C16AA9 /* policydb.h in Headers */,
+ C2F4439B14C626D4000A01E6 /* quarantine++.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
);
runOnlyForDeploymentPostprocessing = 1;
shellPath = /bin/sh;
- shellScript = "mkdir -p \"$(dirname \"$SCRIPT_OUTPUT_FILE_0\")\"\nsqlite3 \"$SCRIPT_OUTPUT_FILE_0\" <<END\n.read \"$SCRIPT_INPUT_FILE_0\"\nEND\n";
+ shellScript = "if [ -f $(TEMPDIR)/SystemPolicy ]; then\n rm $(TEMPDIR)/SystemPolicy\nfi\nmkdir -p \"$(dirname \"$SCRIPT_OUTPUT_FILE_0\")\"\nsqlite3 \"$SCRIPT_OUTPUT_FILE_0\" <<END\n.read \"$SCRIPT_INPUT_FILE_0\"\nEND\n";
showEnvVarsInLog = 0;
};
C26AC7080DAEB3A7005BFB40 /* ShellScript */ = {
shellPath = /bin/bash;
shellScript = "antlr=/usr/local/bin/antlr.jar\nDEBUG=\"\"\nmkdir -p $TEMPDIR\nrm -f $TEMPDIR/Requirement{Parser,Lexer}*\njava -cp \"$antlr\" antlr.Tool -o $TEMPDIR $DEBUG requirements.grammar\nsed -n 's/^.*=\\(\".*\"\\)=.*$/ \\1,/p' $TEMPDIR/RequirementParserTokenTypes.txt >$TEMPDIR/RequirementKeywords.h\n";
};
+ C2F24DFE14BCBBF200309FCD /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 8;
+ files = (
+ );
+ inputPaths = (
+ "$(TEMPDIR)/SystemPolicy",
+ );
+ outputPaths = (
+ "$(DSTROOT)/private/var/db/.SystemPolicy-default",
+ );
+ runOnlyForDeploymentPostprocessing = 1;
+ shellPath = /bin/bash;
+ shellScript = "cp \"$SCRIPT_INPUT_FILE_0\" \"$SCRIPT_OUTPUT_FILE_0\"\nchmod 444 \"$SCRIPT_OUTPUT_FILE_0\"";
+ showEnvVarsInLog = 0;
+ };
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
C27360D51436866D00A9A5FF /* xpcengine.cpp in Sources */,
C2DC2DCA145F594000AD2A3A /* xar++.cpp in Sources */,
C2DC2DCB145F5CD000AD2A3A /* policyengine.cpp in Sources */,
+ C2F4439A14C626D4000A01E6 /* quarantine++.cpp in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
isa = XCBuildConfiguration;
buildSettings = {
BUILD_VARIANTS = debug;
- CURRENT_PROJECT_VERSION = 55032;
+ CURRENT_PROJECT_VERSION = 55037.4;
EXECUTABLE_PREFIX = "";
EXECUTABLE_SUFFIX = "";
FRAMEWORK_SEARCH_PATHS = (
normal,
debug,
);
- CURRENT_PROJECT_VERSION = 55032;
+ CURRENT_PROJECT_VERSION = 55037.4;
EXECUTABLE_PREFIX = "";
EXECUTABLE_SUFFIX = "";
FRAMEWORK_SEARCH_PATHS = (
normal,
debug,
);
- CURRENT_PROJECT_VERSION = 55032;
+ CURRENT_PROJECT_VERSION = 55037.4;
EXECUTABLE_PREFIX = "";
EXECUTABLE_SUFFIX = "";
FRAMEWORK_SEARCH_PATHS = (
{ maker.put(opAppleAnchor); }
| "generic"
{ maker.put(opAppleGenericAnchor); }
-| | { string name; } name=identifierString
+ | { string name; } name=identifierString
{ maker.put(opNamedAnchor); maker.put(name); }
;