diff --git a/services/core/java/com/android/server/pm/SELinuxMMAC.java b/services/core/java/com/android/server/pm/SELinuxMMAC.java index 98f299721c5d8..95ed7bc4855ce 100644 --- a/services/core/java/com/android/server/pm/SELinuxMMAC.java +++ b/services/core/java/com/android/server/pm/SELinuxMMAC.java @@ -35,13 +35,22 @@ import java.io.IOException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; /** - * Centralized access to SELinux MMAC (middleware MAC) implementation. + * Centralized access to SELinux MMAC (middleware MAC) implementation. This + * class is responsible for loading the appropriate mac_permissions.xml file + * as well as providing an interface for assigning seinfo values to apks. + * * {@hide} */ public final class SELinuxMMAC { @@ -51,11 +60,9 @@ public final class SELinuxMMAC { private static final boolean DEBUG_POLICY = false; private static final boolean DEBUG_POLICY_INSTALL = DEBUG_POLICY || false; - // Signature seinfo values read from policy. - private static HashMap sSigSeinfo = new HashMap(); - - // Default seinfo read from policy. - private static String sDefaultSeinfo = null; + // All policy stanzas read from mac_permissions.xml. This is also the lock + // to synchronize access during policy load and access attempts. + private static final List sPolicies = new ArrayList(); // Data policy override version file. private static final String DATA_VERSION_FILE = @@ -94,293 +101,289 @@ public final class SELinuxMMAC { private static final String SEAPP_HASH_FILE = Environment.getDataDirectory().toString() + "/system/seapp_hash"; - - // Signature policy stanzas - static class Policy { - private String seinfo; - private final HashMap pkgMap; - - Policy() { - seinfo = null; - pkgMap = new HashMap(); - } - - void putSeinfo(String seinfoValue) { - seinfo = seinfoValue; - } - - void putPkg(String pkg, String seinfoValue) { - pkgMap.put(pkg, seinfoValue); - } - - // Valid policy stanza means there exists a global - // seinfo value or at least one package policy. - boolean isValid() { - return (seinfo != null) || (!pkgMap.isEmpty()); - } - - String checkPolicy(String pkgName) { - // Check for package name seinfo value first. - String seinfoValue = pkgMap.get(pkgName); - if (seinfoValue != null) { - return seinfoValue; - } - - // Return the global seinfo value. - return seinfo; - } - } - - private static void flushInstallPolicy() { - sSigSeinfo.clear(); - sDefaultSeinfo = null; - } - + /** + * Load the mac_permissions.xml file containing all seinfo assignments used to + * label apps. The loaded mac_permissions.xml file is determined by the + * MAC_PERMISSIONS class variable which is set at class load time which itself + * is based on the USE_OVERRIDE_POLICY class variable. For further guidance on + * the proper structure of a mac_permissions.xml file consult the source code + * located at external/sepolicy/mac_permissions.xml. + * + * @return boolean indicating if policy was correctly loaded. A value of false + * typically indicates a structural problem with the xml or incorrectly + * constructed policy stanzas. A value of true means that all stanzas + * were loaded successfully; no partial loading is possible. + */ public static boolean readInstallPolicy() { - return readInstallPolicy(MAC_PERMISSIONS); - } + // Temp structure to hold the rules while we parse the xml file. We add + // all the rules once we know there's no problems. + List policies = new ArrayList<>(); - public static boolean readInstallPolicy(String macPermsPath) { - if (macPermsPath == null) { - throw new NullPointerException("mac_permissions.xml file path is null"); - } + // A separate structure to hold the default stanza. We need to add this to + // the end of the policies list structure. + Policy defaultPolicy = null; - // Temp structures to hold the rules while we parse the xml file. - // We add all the rules together once we know there's no structural problems. - HashMap sigSeinfo = new HashMap(); - String defaultSeinfo = null; + // Track sets of known policy certs so we can enforce rules across stanzas. + Set> knownCerts = new HashSet<>(); FileReader policyFile = null; + XmlPullParser parser = Xml.newPullParser(); try { - policyFile = new FileReader(macPermsPath); - Slog.d(TAG, "Using policy file " + macPermsPath); + policyFile = new FileReader(MAC_PERMISSIONS); + Slog.d(TAG, "Using policy file " + MAC_PERMISSIONS); - XmlPullParser parser = Xml.newPullParser(); parser.setInput(policyFile); + parser.nextTag(); + parser.require(XmlPullParser.START_TAG, null, "policy"); - XmlUtils.beginDocument(parser, "policy"); - while (true) { - XmlUtils.nextElement(parser); - if (parser.getEventType() == XmlPullParser.END_DOCUMENT) { - break; + while (parser.next() != XmlPullParser.END_TAG) { + if (parser.getEventType() != XmlPullParser.START_TAG) { + continue; } String tagName = parser.getName(); if ("signer".equals(tagName)) { - String cert = parser.getAttributeValue(null, "signature"); - if (cert == null) { - Slog.w(TAG, " without signature at " - + parser.getPositionDescription()); - XmlUtils.skipCurrentTag(parser); - continue; - } - Signature signature; - try { - signature = new Signature(cert); - } catch (IllegalArgumentException e) { - Slog.w(TAG, " with bad signature at " - + parser.getPositionDescription(), e); - XmlUtils.skipCurrentTag(parser); - continue; - } - Policy policy = readPolicyTags(parser); - if (policy.isValid()) { - sigSeinfo.put(signature, policy); + Policy signerPolicy = readSignerOrThrow(parser); + // Return of a Policy instance ensures certain invariants have + // passed, however, we still want to do some cross policy checking. + // Thus, check that we haven't seen the certs in another stanza. + Set certs = signerPolicy.getSignatures(); + if (knownCerts.contains(certs)) { + String msg = "Separate stanzas have identical certs"; + throw new IllegalStateException(msg); } + knownCerts.add(certs); + policies.add(signerPolicy); } else if ("default".equals(tagName)) { - // Value is null if default tag is absent or seinfo tag is malformed. - defaultSeinfo = readSeinfoTag(parser); - if (DEBUG_POLICY_INSTALL) - Slog.i(TAG, " tag assigned seinfo=" + defaultSeinfo); - + Policy defPolicy = readDefaultOrThrow(parser); + // Return of a Policy instance ensures certain invariants have + // passed, however, we still want to do some cross policy checking. + // Thus, check that we haven't already seen a default stanza. + if (defaultPolicy != null) { + String msg = "Multiple default stanzas identified"; + throw new IllegalStateException(msg); + } + defaultPolicy = defPolicy; } else { - XmlUtils.skipCurrentTag(parser); + skip(parser); } } - } catch (XmlPullParserException xpe) { - Slog.w(TAG, "Got exception parsing " + macPermsPath, xpe); + } catch (IllegalStateException | IllegalArgumentException | + XmlPullParserException ex) { + StringBuilder sb = new StringBuilder("Exception @"); + sb.append(parser.getPositionDescription()); + sb.append(" while parsing "); + sb.append(MAC_PERMISSIONS); + sb.append(":"); + sb.append(ex); + Slog.w(TAG, sb.toString()); return false; } catch (IOException ioe) { - Slog.w(TAG, "Got exception parsing " + macPermsPath, ioe); + Slog.w(TAG, "Exception parsing " + MAC_PERMISSIONS, ioe); return false; } finally { IoUtils.closeQuietly(policyFile); } - flushInstallPolicy(); - sSigSeinfo = sigSeinfo; - sDefaultSeinfo = defaultSeinfo; + // Add the default policy to the end if there is one. This will ensure that + // the default stanza is consulted last when performing policy lookups. + if (defaultPolicy != null) { + policies.add(defaultPolicy); + } + + synchronized (sPolicies) { + sPolicies.clear(); + sPolicies.addAll(policies); + } return true; } - private static Policy readPolicyTags(XmlPullParser parser) throws - IOException, XmlPullParserException { + /** + * Loop over a signer tag looking for seinfo, package and cert tags. A {@link Policy} + * instance will be created and returned in the process. During the pass all other + * tag elements will be skipped. + * + * @param parser an XmlPullParser object representing a signer element. + * @return the constructed {@link Policy} instance + * @throws IOException + * @throws XmlPullParserException + * @throws IllegalArgumentException if any of the validation checks fail while + * parsing tag values. + * @throws IllegalStateException if any of the invariants fail when constructing + * the {@link Policy} instance. + */ + private static Policy readSignerOrThrow(XmlPullParser parser) throws IOException, + XmlPullParserException { - int type; - int outerDepth = parser.getDepth(); - Policy policy = new Policy(); - while ((type=parser.next()) != XmlPullParser.END_DOCUMENT - && (type != XmlPullParser.END_TAG - || parser.getDepth() > outerDepth)) { - if (type == XmlPullParser.END_TAG - || type == XmlPullParser.TEXT) { + parser.require(XmlPullParser.START_TAG, null, "signer"); + Policy.PolicyBuilder pb = new Policy.PolicyBuilder(); + + // Check for a cert attached to the signer tag. We allow a signature + // to appear as an attribute as well as those attached to cert tags. + String cert = parser.getAttributeValue(null, "signature"); + if (cert != null) { + pb.addSignature(cert); + } + + while (parser.next() != XmlPullParser.END_TAG) { + if (parser.getEventType() != XmlPullParser.START_TAG) { continue; } String tagName = parser.getName(); if ("seinfo".equals(tagName)) { - String seinfo = parseSeinfo(parser); - if (seinfo != null) { - policy.putSeinfo(seinfo); - } - XmlUtils.skipCurrentTag(parser); + String seinfo = parser.getAttributeValue(null, "value"); + pb.setGlobalSeinfoOrThrow(seinfo); + readSeinfo(parser); } else if ("package".equals(tagName)) { - String pkg = parser.getAttributeValue(null, "name"); - if (!validatePackageName(pkg)) { - Slog.w(TAG, " without valid name at " - + parser.getPositionDescription()); - XmlUtils.skipCurrentTag(parser); - continue; - } - - String seinfo = readSeinfoTag(parser); - if (seinfo != null) { - policy.putPkg(pkg, seinfo); - } + readPackageOrThrow(parser, pb); + } else if ("cert".equals(tagName)) { + String sig = parser.getAttributeValue(null, "signature"); + pb.addSignature(sig); + readCert(parser); } else { - XmlUtils.skipCurrentTag(parser); + skip(parser); } } - return policy; + + return pb.build(); } - private static String readSeinfoTag(XmlPullParser parser) throws - IOException, XmlPullParserException { + /** + * Loop over a default element looking for seinfo child tags. A {@link Policy} + * instance will be created and returned in the process. All other tags encountered + * will be skipped. + * + * @param parser an XmlPullParser object representing a default element. + * @return the constructed {@link Policy} instance + * @throws IOException + * @throws XmlPullParserException + * @throws IllegalArgumentException if any of the validation checks fail while + * parsing tag values. + * @throws IllegalStateException if any of the invariants fail when constructing + * the {@link Policy} instance. + */ + private static Policy readDefaultOrThrow(XmlPullParser parser) throws IOException, + XmlPullParserException { - int type; - int outerDepth = parser.getDepth(); - String seinfo = null; - while ((type=parser.next()) != XmlPullParser.END_DOCUMENT - && (type != XmlPullParser.END_TAG - || parser.getDepth() > outerDepth)) { - if (type == XmlPullParser.END_TAG - || type == XmlPullParser.TEXT) { + parser.require(XmlPullParser.START_TAG, null, "default"); + Policy.PolicyBuilder pb = new Policy.PolicyBuilder(); + pb.setAsDefaultPolicy(); + + while (parser.next() != XmlPullParser.END_TAG) { + if (parser.getEventType() != XmlPullParser.START_TAG) { continue; } String tagName = parser.getName(); if ("seinfo".equals(tagName)) { - seinfo = parseSeinfo(parser); + String seinfo = parser.getAttributeValue(null, "value"); + pb.setGlobalSeinfoOrThrow(seinfo); + readSeinfo(parser); + } else { + skip(parser); } - XmlUtils.skipCurrentTag(parser); } - return seinfo; - } - private static String parseSeinfo(XmlPullParser parser) { - - String seinfoValue = parser.getAttributeValue(null, "value"); - if (!validateValue(seinfoValue)) { - Slog.w(TAG, " without valid value at " - + parser.getPositionDescription()); - seinfoValue = null; - } - return seinfoValue; + return pb.build(); } /** - * General validation routine for package names. - * Returns a boolean indicating if the passed string - * is a valid android package name. + * Loop over a package element looking for seinfo child tags. If found return the + * value attribute of the seinfo tag, otherwise return null. All other tags encountered + * will be skipped. + * + * @param parser an XmlPullParser object representing a package element. + * @param pb a Policy.PolicyBuilder instance to build + * @throws IOException + * @throws XmlPullParserException + * @throws IllegalArgumentException if any of the validation checks fail while + * parsing tag values. + * @throws IllegalStateException if there is a duplicate seinfo tag for the current + * package tag. */ - private static boolean validatePackageName(String name) { - if (name == null) - return false; + private static void readPackageOrThrow(XmlPullParser parser, Policy.PolicyBuilder pb) throws + IOException, XmlPullParserException { + parser.require(XmlPullParser.START_TAG, null, "package"); + String pkgName = parser.getAttributeValue(null, "name"); - final int N = name.length(); - boolean hasSep = false; - boolean front = true; - for (int i=0; i= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { - front = false; + while (parser.next() != XmlPullParser.END_TAG) { + if (parser.getEventType() != XmlPullParser.START_TAG) { continue; } - if (!front) { - if ((c >= '0' && c <= '9') || c == '_') { - continue; - } + + String tagName = parser.getName(); + if ("seinfo".equals(tagName)) { + String seinfo = parser.getAttributeValue(null, "value"); + pb.addInnerPackageMapOrThrow(pkgName, seinfo); + readSeinfo(parser); + } else { + skip(parser); + } + } + } + + private static void readCert(XmlPullParser parser) throws IOException, + XmlPullParserException { + parser.require(XmlPullParser.START_TAG, null, "cert"); + parser.nextTag(); + } + + private static void readSeinfo(XmlPullParser parser) throws IOException, + XmlPullParserException { + parser.require(XmlPullParser.START_TAG, null, "seinfo"); + parser.nextTag(); + } + + private static void skip(XmlPullParser p) throws IOException, XmlPullParserException { + if (p.getEventType() != XmlPullParser.START_TAG) { + throw new IllegalStateException(); + } + int depth = 1; + while (depth != 0) { + switch (p.next()) { + case XmlPullParser.END_TAG: + depth--; + break; + case XmlPullParser.START_TAG: + depth++; + break; } - if (c == '.') { - hasSep = true; - front = true; - continue; - } - return false; } - return hasSep; } /** - * General validation routine for tag values. - * Returns a boolean indicating if the passed string - * contains only letters or underscores. - */ - private static boolean validateValue(String name) { - if (name == null) - return false; - - final int N = name.length(); - if (N == 0) - return false; - - for (int i = 0; i < N; i++) { - final char c = name.charAt(i); - if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c != '_')) { - return false; - } - } - return true; - } - - /** - * Labels a package based on an seinfo tag from install policy. - * The label is attached to the ApplicationInfo instance of the package. + * Applies a security label to a package based on an seinfo tag taken from a matched + * policy. All signature based policy stanzas are consulted first and, if no match + * is found, the default policy stanza is then consulted. The security label is + * attached to the ApplicationInfo instance of the package in the event that a matching + * policy was found. + * * @param pkg object representing the package to be labeled. - * @return boolean which determines whether a non null seinfo label - * was assigned to the package. A null value simply meaning that - * no policy matched. + * @return boolean which determines whether a non null seinfo label was assigned + * to the package. A null value simply represents that no policy matched. */ public static boolean assignSeinfoValue(PackageParser.Package pkg) { - - // We just want one of the signatures to match. - for (Signature s : pkg.mSignatures) { - if (s == null) - continue; - - Policy policy = sSigSeinfo.get(s); - if (policy != null) { - String seinfo = policy.checkPolicy(pkg.packageName); + synchronized (sPolicies) { + for (Policy policy : sPolicies) { + String seinfo = policy.getMatchedSeinfo(pkg); if (seinfo != null) { pkg.applicationInfo.seinfo = seinfo; - if (DEBUG_POLICY_INSTALL) - Slog.i(TAG, "package (" + pkg.packageName + - ") labeled with seinfo=" + seinfo); - + if (DEBUG_POLICY_INSTALL) { + Slog.i(TAG, "package (" + pkg.packageName + ") labeled with " + + "seinfo=" + seinfo); + } return true; } } } - // If we have a default seinfo value then great, otherwise - // we set a null object and that is what we started with. - pkg.applicationInfo.seinfo = sDefaultSeinfo; - if (DEBUG_POLICY_INSTALL) - Slog.i(TAG, "package (" + pkg.packageName + ") labeled with seinfo=" - + (sDefaultSeinfo == null ? "null" : sDefaultSeinfo)); - - return (sDefaultSeinfo != null); + if (DEBUG_POLICY_INSTALL) { + Slog.i(TAG, "package (" + pkg.packageName + ") doesn't match any policy; " + + "seinfo will remain null"); + } + return false; } /** @@ -485,3 +488,312 @@ public final class SELinuxMMAC { return false; } } + +/** + * Holds valid policy representations of individual stanzas from a mac_permissions.xml + * file. Each instance can further be used to assign seinfo values to apks using the + * {@link Policy#getMatchedSeinfo} method. To create an instance of this use the + * {@link PolicyBuilder} pattern class, where each instance is validated against a set + * of invariants before being built and returned. Each instance can be guaranteed to + * hold one valid policy stanza as outlined in the external/sepolicy/mac_permissions.xml + * file. + *

+ * The following is an example of how to use {@link Policy.PolicyBuilder} to create a + * signer based Policy instance. + *

+ *
+ * {@code
+ * Policy policy = new Policy.PolicyBuilder()
+ *         .addSignature("308204a8...")
+ *         .addSignature("483538c8...")
+ *         .setGlobalSeinfoOrThrow("paltform")
+ *         .addInnerPackageMapOrThrow("com.foo.", "bar")
+ *         .addInnerPackageMapOrThrow("com.foo.other", "bar")
+ *         .build();
+ * }
+ * 
+ *

+ * An example of how to use {@link Policy.PolicyBuilder} to create a default based Policy + * instance. + *

+ *
+ * {@code
+ * Policy policy = new Policy.PolicyBuilder()
+ *         .setAsDefaultPolicy()
+ *         .setGlobalSeinfoOrThrow("defualt")
+ *         .build();
+ * }
+ * 
+ */ +final class Policy { + + private final String mSeinfo; + private final boolean mDefaultStanza; + private final Set mCerts; + private final Map mPkgMap; + + // Use the PolicyBuilder pattern to instantiate + private Policy(PolicyBuilder builder) { + mSeinfo = builder.mSeinfo; + mDefaultStanza = builder.mDefaultStanza; + mCerts = Collections.unmodifiableSet(builder.mCerts); + mPkgMap = Collections.unmodifiableMap(builder.mPkgMap); + } + + /** + * Return all the certs stored with this policy stanza. + * + * @return A set of Signature objects representing all the certs stored + * with the policy. + */ + public Set getSignatures() { + return mCerts; + } + + /** + *

+ * Determine the seinfo value to assign to an apk. The appropriate seinfo value + * is determined using the following steps: + *

+ *
    + *
  • If this Policy instance is defined as a default stanza: + *
    • Return the global seinfo value
    + *
  • + *
  • If this Policy instance is defined as a signer stanza: + *
      + *
    • All certs used to sign the apk and all certs stored with this policy + * instance are tested for set equality. If this fails then null is returned. + *
    • + *
    • If all certs match then an appropriate inner package stanza is + * searched based on package name alone. If matched, the stored seinfo + * value for that mapping is returned. + *
    • + *
    • If all certs matched and no inner package stanza matches then return + * the global seinfo value. The returned value can be null in this case. + *
    • + *
    + *
  • + *
+ *

+ * In all cases, a return value of null should be interpreted as the apk failing + * to match this Policy instance; i.e. failing this policy stanza. + *

+ * @param pkg the apk to check given as a PackageParser.Package object + * @return A string representing the seinfo matched during policy lookup. + * A value of null can also be returned if no match occured. + */ + public String getMatchedSeinfo(PackageParser.Package pkg) { + if (!mDefaultStanza) { + // Check for exact signature matches across all certs. + Signature[] certs = mCerts.toArray(new Signature[0]); + if (!Signature.areExactMatch(certs, pkg.mSignatures)) { + return null; + } + + // Check for inner package name matches given that the + // signature checks already passed. + String seinfoValue = mPkgMap.get(pkg.packageName); + if (seinfoValue != null) { + return seinfoValue; + } + } + + // Return the global seinfo value (even if it's null). + return mSeinfo; + } + + /** + * A nested builder class to create {@link Policy} instances. A {@link Policy} + * class instance represents one valid policy stanza found in a mac_permissions.xml + * file. A valid policy stanza is defined to be either a signer or default stanza + * which obeys the rules outlined in external/sepolicy/mac_permissions.xml. The + * {@link #build} method ensures a set of invariants are upheld enforcing the correct + * stanza structure before returning a valid Policy object. + */ + public static final class PolicyBuilder { + + private String mSeinfo; + private boolean mDefaultStanza; + private final Set mCerts; + private final Map mPkgMap; + + public PolicyBuilder() { + mCerts = new HashSet(2); + mPkgMap = new HashMap(2); + } + + /** + * Sets this stanza as a defualt stanza. All policy stanzas are assumed to + * be signer stanzas unless this method is explicitly called. Default stanzas + * are treated differently with respect to allowable child tags, ordering and + * when and how policy decisions are enforced. + * + * @return The reference to this PolicyBuilder. + */ + public PolicyBuilder setAsDefaultPolicy() { + mDefaultStanza = true; + return this; + } + + /** + * Adds a signature to the set of certs used for validation checks. The purpose + * being that all contained certs will need to be matched against all certs + * contained with an apk. + * + * @param cert the signature to add given as a String. + * @return The reference to this PolicyBuilder. + * @throws IllegalArgumentException if the cert value fails validation; + * null or is an invalid hex-encoded ASCII string. + */ + public PolicyBuilder addSignature(String cert) { + if (cert == null) { + String err = "Invalid signature value " + cert; + throw new IllegalArgumentException(err); + } + + mCerts.add(new Signature(cert)); + return this; + } + + /** + * Set the global seinfo tag for this policy stanza. The global seinfo tag + * represents the seinfo element that is used in one of two ways depending on + * its context. When attached to a signer tag the global seinfo represents an + * assignment when there isn't a further inner package refinement in policy. + * When used with a default tag, it represents the only allowable assignment + * value. + * + * @param seinfo the seinfo value given as a String. + * @return The reference to this PolicyBuilder. + * @throws IllegalArgumentException if the seinfo value fails validation; + * null, zero length or contains non-valid characters [^a-zA-Z_\._0-9]. + * @throws IllegalStateException if an seinfo value has already been found + */ + public PolicyBuilder setGlobalSeinfoOrThrow(String seinfo) { + if (!validateValue(seinfo)) { + String err = "Invalid seinfo value " + seinfo; + throw new IllegalArgumentException(err); + } + + if (mSeinfo != null && !mSeinfo.equals(seinfo)) { + String err = "Duplicate seinfo tag found"; + throw new IllegalStateException(err); + } + + mSeinfo = seinfo; + return this; + } + + /** + * Create a package name to seinfo value mapping. Each mapping represents + * the seinfo value that will be assigned to the described package name. + * These localized mappings allow the global seinfo to be overriden. This + * mapping provides no value when used in conjunction with a default stanza; + * enforced through the {@link #build} method. + * + * @param pkgName the android package name given to the app + * @param seinfo the seinfo value that will be assigned to the passed pkgName + * @return The reference to this PolicyBuilder. + * @throws IllegalArgumentException if the seinfo value fails validation; + * null, zero length or contains non-valid characters [^a-zA-Z_\.0-9]. + * Or, if the package name isn't a valid android package name. + * @throws IllegalStateException if trying to reset a package mapping with a + * different seinfo value. + */ + public PolicyBuilder addInnerPackageMapOrThrow(String pkgName, String seinfo) { + if (!validateValue(pkgName)) { + String err = "Invalid package name " + pkgName; + throw new IllegalArgumentException(err); + } + if (!validateValue(seinfo)) { + String err = "Invalid seinfo value " + seinfo; + throw new IllegalArgumentException(err); + } + + String pkgValue = mPkgMap.get(pkgName); + if (pkgValue != null && !pkgValue.equals(seinfo)) { + String err = "Conflicting seinfo value found"; + throw new IllegalStateException(err); + } + + mPkgMap.put(pkgName, seinfo); + return this; + } + + /** + * General validation routine for the attribute strings of an element. Checks + * if the string is non-null, positive length and only contains [a-zA-Z_\.0-9]. + * + * @param name the string to validate. + * @return boolean indicating if the string was valid. + */ + private boolean validateValue(String name) { + if (name == null) + return false; + + // Want to match on [0-9a-zA-Z_.] + if (!name.matches("\\A[\\.\\w]+\\z")) { + return false; + } + + return true; + } + + /** + *

+ * Create a {@link Policy} instance based on the current configuration. This + * method checks for certain policy invariants used to enforce certain guarantees + * about the expected structure of a policy stanza. + * Those invariants are: + *

+ *
    + *
  • If a default stanza + *
      + *
    • an attached global seinfo tag must be present
    • + *
    • no signatures and no package names can be present
    • + *
    + *
  • + *
  • If a signer stanza + *
      + *
    • at least one cert must be found
    • + *
    • either a global seinfo value is present OR at least one + * inner package mapping must be present.
    • + *
    + *
  • + *
+ * + * @return an instance of {@link Policy} with the options set from this builder + * @throws IllegalStateException if an invariant is violated. + */ + public Policy build() { + Policy p = new Policy(this); + + if (p.mDefaultStanza) { + if (p.mSeinfo == null) { + String err = "Missing global seinfo tag with default stanza."; + throw new IllegalStateException(err); + } + if (p.mCerts.size() != 0) { + String err = "Certs not allowed with default stanza."; + throw new IllegalStateException(err); + } + if (!p.mPkgMap.isEmpty()) { + String err = "Inner package mappings not allowed with default stanza."; + throw new IllegalStateException(err); + } + } else { + if (p.mCerts.size() == 0) { + String err = "Missing certs with signer tag. Expecting at least one."; + throw new IllegalStateException(err); + } + if ((p.mSeinfo == null) && (p.mPkgMap.isEmpty())) { + String err = "Missing seinfo OR package tags with signer tag. At " + + "least one must be present."; + throw new IllegalStateException(err); + } + } + + return p; + } + } +}