/*
* Copyright 1999-2010 University of Chicago
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS,WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied.
*
* See the License for the specific language governing permissions and limitations under the License.
*/
package org.globus.gsi;
import org.globus.gsi.util.CertificateUtil;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.regex.Pattern;
import javax.security.auth.x500.X500Principal;
/**
* Signing policy BCNF grammar as implemented here: (based on C implementation)
* <p/>
* eacl ::= {eacl_entry} eacl_entry ::= {access_identity} pos_rights
* {restriction} {pos_rights {restriction}} | {access_identity} neg_rights
* access_identity ::= access_identity_type def_authority value \n
* access_identity_type ::= "access_id_HOST" | "access_id_USER" |
* "access_id_GROUP" | "access_id_CA" | "access_id_APPLICATION" |
* "access_id_ANYBODY" pos_rights ::= "pos_rights" def_authority value
* {"pos_rights" def_authority value} neg_rights ::= "neg_rights" def_authority
* value {"neg_rights" def_authority value} restriction ::= condition_type
* def_authority value \n condition_type ::= alphanumeric_string def_authority
* ::= alphanumeric_string value ::= alphanumeric_string
* <p/>
* This class take a signing policy file as input and parses it to extract the
* policy that is enforced. Only the following policy is enforced: access_id_CA
* with defining authority as X509 with CA DN as value. Any positive rights
* following it with globus as defining authority and value CA:sign. Lastly,
* restriction "cond_subjects" with globus as defining authority and the DNs the
* CA is authorized to sign. restrictions are assumed to start with cond_. Order
* of rights matter, so the first occurance of CA:Sign with allowedDNs is used
* and rest of the policy is ignored.
* <p/>
* For a given signing policy file, only policy with the particular CA's DN is
* parsed.
* <p/>
* subject names may include the following wildcard characters: * Matches
* zero or any number of characters. ? Matches any single character.
* <p/>
* All subject names should be in Globus format, with slashes and should NOT be
* revered.
* <p/>
* The allowed DN patterns are returned as a vector of java.util.regexp.Pattern.
* The BCNF grammar that uses wildcard (*) and single character (?) are replaced
* with the regexp grammar needed by the Pattern class.
*/
// COMMENT: BCB: moved over from crux-security-core: different parse-function name, stricter check for parameters
public class SigningPolicyParser {
public static final String ACCESS_ID_PREFIX = "access_id_";
public static final String ACCESS_ID_CA = "access_id_CA";
public static final String DEF_AUTH_X509 = "X509";
public static final String DEF_AUTH_GLOBUS = "globus";
public static final String POS_RIGHTS = "pos_rights";
public static final String NEG_RIGHTS = "neg_rights";
public static final String CONDITION_PREFIX = "cond_";
public static final String CONDITION_SUBJECT = "cond_subjects";
public static final String VALUE_CA_SIGN = "CA:sign";
public static final String SINGLE_CHAR = "?";
public static final String WILDCARD = "*";
public static final String SINGLE_PATTERN = "[\\p{Print}\\p{Blank}]";
public static final String WILDCARD_PATTERN = SINGLE_PATTERN + "*";
private static final int MIN_TOKENS_PER_LINE = 3;
static final String[] ALLOWED_LINE_START = new String[]{ACCESS_ID_PREFIX, POS_RIGHTS, NEG_RIGHTS, CONDITION_PREFIX};
private Log logger = LogFactory.getLog(SigningPolicyParser.class.getName());
/**
* Parses the file to extract signing policy defined for CA with the
* specified DN. If the policy file does not exist, a SigningPolicy object
* with only CA DN is created. If policy path exists, but no relevant policy
* exisit, SigningPolicy object with CA DN and file path is created.
*
* @param fileName Name of the signing policy file
* @return SigningPolicy object that contains the information. If no policy
* is found, SigningPolicy object with only the CA DN is returned.
* @throws org.globus.gsi.SigningPolicyException
* Any errors with parsing the signing policy file.
* @throws FileNotFoundException If the signing policy file does not exist.
*/
public Map<X500Principal, SigningPolicy> parse(String fileName)
throws FileNotFoundException, SigningPolicyException {
if ((fileName == null) || (fileName.trim().isEmpty())) {
throw new IllegalArgumentException();
}
logger.debug("Signing policy file name " + fileName);
FileReader fileReader = null;
try {
fileReader = new FileReader(fileName);
return parse(fileReader);
} catch (Exception e) {
throw new SigningPolicyException(e);
} finally {
if (fileReader != null) {
try {
fileReader.close();
} catch (Exception exp) {
logger.debug("Error closing file reader", exp);
}
}
}
}
/**
* Parses input stream to extract signing policy defined for CA with the
* specified DN.
*
* @param reader Reader to any input stream to get the signing policy
* information.
* @return signing policy map defined by the signing policy file
* @throws org.globus.gsi.SigningPolicyException
* Any errors with parsing the signing policy.
*/
public Map<X500Principal, SigningPolicy> parse(Reader reader)
throws SigningPolicyException {
Map<X500Principal, SigningPolicy> policies = new HashMap<X500Principal, SigningPolicy>();
BufferedReader bufferedReader = new BufferedReader(reader);
try {
String line;
while ((line = bufferedReader.readLine()) != null) {
line = line.trim();
// read line until some line that needs to be parsed.
if (!isValidLine(line)) {
continue;
}
logger.debug("Line to parse: " + line);
String caDN = null;
if (line.startsWith(ACCESS_ID_PREFIX)) {
logger.debug("Check if it is CA and get the DN " + line);
caDN = getCaDN(line, caDN);
boolean usefulEntry = true;
Boolean posNegRights = null;
// check for neg or pos rights with restrictions
checkRights(policies, bufferedReader, caDN, usefulEntry, posNegRights);
}
// JGLOBUS-94
}
} catch (IOException exp) {
throw new SigningPolicyException("", exp);
} finally {
cleanupReaders(reader, bufferedReader);
}
return policies;
}
private void checkRights(Map<X500Principal, SigningPolicy> policies, BufferedReader bufferedReader, String caDN,
boolean usefulEntry, Boolean posNegRights) throws IOException, SigningPolicyException {
boolean tmpUsefulEntry = usefulEntry;
Boolean tmpPosNegRights = posNegRights;
String line = bufferedReader.readLine();
while (line != null) {
if (!isValidLine(line)) {
line = bufferedReader.readLine();
continue;
}
line = line.trim();
logger.debug("Line is " + line);
if (line.startsWith(POS_RIGHTS)) {
validatePositiveRights(tmpPosNegRights);
if (tmpUsefulEntry) {
tmpUsefulEntry = isUsefulEntry(line);
}
tmpPosNegRights = Boolean.TRUE;
} else if (line.startsWith(NEG_RIGHTS)) {
tmpPosNegRights = handleNegativeRights(tmpPosNegRights);
} else if (line.startsWith(CONDITION_PREFIX)) {
if (handleConditionalLine(policies, line, caDN, tmpUsefulEntry, tmpPosNegRights)) {
break;
}
} else {
String err = "invalidLine";
// no valid start with
// String err = i18n.getMessage("invalidLine", line);
throw new SigningPolicyException(err + line);
}
line = bufferedReader.readLine();
}
}
private boolean handleConditionalLine(Map<X500Principal, SigningPolicy> policies, String line, String caDN, boolean usefulEntry, Boolean posNegRights) throws SigningPolicyException {
if (!Boolean.TRUE.equals(posNegRights)) {
String err = "invalidRestrictions";
// i18n.getMessage("invalidRestrictions", line);
throw new SigningPolicyException(err);
}
if (usefulEntry && line.startsWith(CONDITION_SUBJECT)) {
logger.debug("Read in subject condition.");
int startIndex = CONDITION_SUBJECT.length();
int endIndex = line.length();
Vector<Pattern> allowedDNs = getAllowedDNs(line.substring(startIndex, endIndex));
// Some IGTF CA signing policies include all the various versions of having the emailAddress
// in the DN. The "E=" variant causes an exception to be thrown in modern JVMs.
// Hence, we ignore invalid DNs. Luckily, the signing policies contain all variants so
// it is safe to ignore.
try {
X500Principal caPrincipal = CertificateUtil.toPrincipal(caDN);
SigningPolicy policy = new SigningPolicy(caPrincipal, allowedDNs);
policies.put(caPrincipal, policy);
} catch (java.lang.IllegalArgumentException e) {
if (caDN == null) {
throw e;
}
String [] components = caDN.split("/");
boolean hasE = false;
for (int i=0; i<components.length; i++) {
String attribute = components[i].split("=")[0];
if (attribute.equals("E")) {
hasE = true;
break;
}
}
if (hasE) {
logger.warn("Invalid DN (" + caDN + ") in the CA policy");
logger.debug("Invalid DN in the CA policy", e);
return true;
} else {
throw e;
}
}
return true;
}
return false;
}
private String getCaDN(String line, String caDN) throws SigningPolicyException {
String outCaDN = caDN;
if (line.startsWith(ACCESS_ID_CA)) {
outCaDN = getCA(line.substring(ACCESS_ID_CA.length(),
line.length()));
logger.debug("CA DN is " + outCaDN);
}
return outCaDN;
}
private void validatePositiveRights(Boolean posNegRights) throws SigningPolicyException {
if (Boolean.FALSE.equals(posNegRights)) {
String err = "invalidPosRights";
// i18n.getMessage("invalidPosRights", line);
throw new SigningPolicyException(err);
}
}
private boolean isUsefulEntry(String line) throws SigningPolicyException {
boolean usefulEntry;
logger.debug("Parse pos_rights here");
int startIndex = POS_RIGHTS.length();
int endIndex = line.length();
// if it is not CASignRight, then
// usefulentry will be false. Otherwise
// other restrictions will be useful.
usefulEntry = isCASignRight(line.substring(startIndex, endIndex));
return usefulEntry;
}
private Boolean handleNegativeRights(Boolean posNegRights) throws SigningPolicyException {
if (Boolean.TRUE.equals(posNegRights)) {
String err = "invalidNegRights";
// i18n.getMessage("invalidNegRights", line);
throw new SigningPolicyException(err);
}
logger.debug("Ignore neg_rights");
return Boolean.FALSE;
}
private void cleanupReaders(Reader reader, BufferedReader bufferedReader) {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (Exception exp) {
//Nothing we can do
logger.debug("Unable to close bufferedReader", exp);
}
}
if (reader != null) {
try {
reader.close();
} catch (Exception e) {
//Nothing we can do
logger.debug("Unable to close reader", e);
}
}
}
private boolean isValidLine(String line)
throws SigningPolicyException {
String trimmedLine = line.trim();
// if line is empty or comment character, skip it.
if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) {
return false;
}
// Validate that there are atleast three tokens on the line
StringTokenizer tokenizer = new StringTokenizer(trimmedLine);
if (tokenizer.countTokens() < MIN_TOKENS_PER_LINE) {
// String err = i18n.getMessage("invalidTokens", line);
String err = "invalidTokens";
throw new SigningPolicyException(err + " on line \"" + trimmedLine + "\"");
}
for (String allowedLineStart : ALLOWED_LINE_START) {
if (trimmedLine.startsWith(allowedLineStart)) {
return true;
}
}
throw new SigningPolicyException("Line starts incorrectly");
}
private Vector<Pattern> getAllowedDNs(String line)
throws SigningPolicyException {
String trimmedLine = line.trim();
int index = findIndex(trimmedLine);
if (index == -1) {
String err = "invalid tokens";
// i18n.getMessage("invalidTokens", line);
throw new SigningPolicyException(err);
}
String defAuth = trimmedLine.substring(0, index);
if (DEF_AUTH_GLOBUS.equals(defAuth)) {
String value = trimmedLine.substring(index + 1, trimmedLine.length());
value = value.trim();
int startIndex = 0;
int endIndex = value.length();
if (value.charAt(startIndex) == '\'') {
startIndex++;
int endOfDNIndex = value.indexOf('\'', startIndex);
if (endOfDNIndex == -1) {
String err = "invlaid subjects";
//i18n.getMessage("invalidSubjects",
// lineForErr);
throw new SigningPolicyException(err);
}
endIndex = endOfDNIndex;
}
value = value.substring(startIndex, endIndex);
value = value.trim();
if (value.isEmpty()) {
String err = "empty subjects";
//i18n.getMessage("emptySubjects", lineForErr);
throw new SigningPolicyException(err);
}
Vector<Pattern> vector = new Vector<Pattern>();
startIndex = 0;
endIndex = value.length();
if (value.indexOf("\"") == -1) {
vector.add(getPattern(value));
} else {
while (startIndex < endIndex) {
int quot1 = value.indexOf("\"", startIndex);
int quot2 = value.indexOf("\"", quot1 + 1);
if (quot2 == -1) {
String err = "unmatched quotes";
//i18n.getMessage("unmatchedQuotes",
// lineForErr);
throw new SigningPolicyException(err);
}
String token = value.substring(quot1 + 1, quot2);
vector.add(getPattern(token));
startIndex = quot2 + 1;
}
}
return vector;
}
return null;
}
private boolean isCASignRight(String line)
throws SigningPolicyException {
String trimmedLine = line.trim();
int index = findIndex(trimmedLine);
if (index == -1) {
String err = "invalid tokens";
// i18n.getMessage("invalidTokens", line);
throw new SigningPolicyException(err);
}
String defAuth = trimmedLine.substring(0, index);
if (DEF_AUTH_GLOBUS.equals(defAuth)) {
trimmedLine = trimmedLine.substring(index + 1, trimmedLine.length());
trimmedLine = trimmedLine.trim();
// check if it is CA:Sign
String value = trimmedLine.substring(0, trimmedLine.length());
if (VALUE_CA_SIGN.equals(value)) {
return true;
}
}
return false;
}
private String getCA(String inputLine)
throws SigningPolicyException {
String line = inputLine.trim();
int index = findIndex(line);
if (index == -1) {
String err = "invalid tokens";
// i18n.getMessage("invalidTokens", line);
throw new SigningPolicyException(err);
}
String defAuth = line.substring(0, index);
if (DEF_AUTH_X509.equals(defAuth)) {
line = line.substring(index + 1, line.length());
line = line.trim();
// String dnString = line.substring(0, line.length());
String caDN;
// find CA DN
int caDNLocation = 0;
if (line.charAt(caDNLocation) == '\'') {
caDNLocation++;
int endofDNIndex = line.indexOf('\'', caDNLocation + 1);
if (endofDNIndex == -1) {
// String err = i18n.getMessage("invalidCaDN", inputLine);
String err = "invalid ca dn";
throw new SigningPolicyException(err);
}
caDN = line.substring(caDNLocation, endofDNIndex);
} else {
caDN = line.substring(caDNLocation, line.length() - 1);
}
caDN = caDN.trim();
return caDN;
}
return null;
}
/**
* Method that takes a pattern string as described in the signing policy
* file with * for zero or many characters and ? for single character, and
* converts it into java.util.regexp.Pattern object. This requires replacing
* the wildcard characters with equivalent expression in regexp grammar.
*
* @param patternStr Pattern string as described in the signing policy file
* with for zero or many characters and ? for single
* character
* @return Pattern object with the expression equivalent to patternStr.
*/
public static Pattern getPattern(String patternStr) {
if (patternStr == null) {
throw new IllegalArgumentException();
}
int startIndex = 0;
int endIndex = patternStr.length();
StringBuffer buffer = new StringBuffer("");
while (startIndex < endIndex) {
int star = patternStr.indexOf(WILDCARD, startIndex);
if (star == -1) {
star = endIndex;
String preStr = patternStr.substring(startIndex, star);
buffer = buffer.append(preStr);
} else {
String preStr = patternStr.substring(startIndex, star);
buffer = buffer.append(preStr).append(WILDCARD_PATTERN);
}
startIndex = star + 1;
}
String tmpPatternStr = buffer.toString();
startIndex = 0;
endIndex = tmpPatternStr.length();
buffer = new StringBuffer("");
while (startIndex < endIndex) {
int qMark = tmpPatternStr.indexOf(SINGLE_CHAR, startIndex);
if (qMark == -1) {
qMark = endIndex;
String preStr = tmpPatternStr.substring(startIndex, qMark);
buffer = buffer.append(preStr);
} else {
String preStr = tmpPatternStr.substring(startIndex, qMark);
buffer = buffer.append(preStr).append(SINGLE_PATTERN);
}
startIndex = qMark + 1;
}
tmpPatternStr = buffer.toString();
LogFactory.getLog(SigningPolicyParser.class.getCanonicalName()).debug("String with replaced pattern is " + tmpPatternStr);
return Pattern.compile(tmpPatternStr, Pattern.CASE_INSENSITIVE);
}
// find first space or tab as separator.
private int findIndex(String line) {
int index = -1;
if (line == null) {
return index;
}
String trimmedLine = line.trim();
int spaceIndex = trimmedLine.indexOf(" ");
int tabIndex = trimmedLine.indexOf("\t");
if (spaceIndex != -1) {
if (tabIndex != -1) {
if (spaceIndex < tabIndex) {
index = spaceIndex;
} else {
index = tabIndex;
}
} else {
index = spaceIndex;
}
} else {
index = tabIndex;
}
return index;
}
}