/*
*
* Copyright (c) 2013 - 2017 Lijun Liao
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License version 3
* as published by the Free Software Foundation with the addition of the
* following permission added to Section 15 as permitted in Section 7(a):
*
* FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
* THE AUTHOR LIJUN LIAO. LIJUN LIAO DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
* OF THIRD PARTY RIGHTS.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License.
*
* You can be released from the requirements of the license by purchasing
* a commercial license. Buying such a license is mandatory as soon as you
* develop commercial activities involving the XiPKI software without
* disclosing the source code of your own applications.
*
* For more information, please contact Lijun Liao at this
* address: lijun.liao@gmail.com
*/
package org.xipki.pki.ca.server.mgmt.api.x509;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.xipki.commons.common.ConfPairs;
import org.xipki.commons.common.InvalidConfException;
import org.xipki.commons.common.TripleState;
import org.xipki.commons.common.util.CollectionUtil;
import org.xipki.commons.common.util.ParamUtil;
import org.xipki.commons.common.util.StringUtil;
/**
*<pre>
* Example configuration
* updateMode=<'interval'|'onDemand'>
*
* # For all updateMode
*
* # Whether expired certificates are considered. Default is false
* expiredCerts.included=<'true'|'false'>
*
* # Whether XiPKI-customized extension xipki-CrlCertSet is included. Default is false
* xipki.certset=<'true'|'false'>
*
* # Whether the extension xipki-CrlCertSet contains the raw certificates. Default is true
* xipki.certset.certs=<'true'|'false'>
*
* # Whether the extension xipki-CrlCertSet contains the profile name of the certificate.
* # Default is true
* xipki.certset.profilename=<'true'|'false'>
*
* # List of OIDs of extensions to be embedded in CRL,
* # Unspecified or empty extensions indicates that the CA decides.
* extensions=<comma delimited OIDs of extensions>
*
* # The following settings are only for updateMode 'interval'
*
* # Number of intervals to generate a full CRL. Default is 1
* # Should be greater than 0
* fullCRL.intervals=<integer>
*
* # should be 0 or not greater than baseCRL.intervals. Default is 0.
* # 0 indicates that no deltaCRL will be generated
* deltaCRL.intervals=<integer>
*
* overlap.minutes=<minutes of overlap>
*
* # should be less than fullCRL.intervals.
* # If activated, a deltaCRL will be generated only between two full CRLs
* deltaCRL.intervals=<integer>
*
* # Exactly one of interval.minutes and interval.days should be specified
* # Number of minutes of one interval. At least 60 minutes
* interval.minutes=<minutes of one interval>
*
* # UTC time of generation of CRL, one interval covers 1 day.
* interval.time=<updatet time (hh:mm of UTC time)>
*
* # Whether the nextUpdate of a fullCRL is the update time of the fullCRL
* # Default is false
* fullCRL.extendedNextUpdate=<'true'|'false'>
*
* # Whether only user certificates are considered in CRL
* # Default is false
* onlyContainsUserCerts=<'true'|'false'>
*
* # Whether only CA certificates are considered in CRL
* # Default if false
* onlyContainsCACerts=<'true'|'false'>
*
* # Whether Revocation reason is contained in CRL
* # Default is false
* excludeReason=<'true'|'false'>
*
* # How the CRL entry extension invalidityDate is considered in CRL
* # Default is false
* invalidityDate=<'required'|'optional'|'forbidden'>
*
* </pre>
* @author Lijun Liao
* @since 2.0.0
*/
public class CrlControl {
public enum UpdateMode {
interval,
onDemand;
public static UpdateMode forName(final String mode) {
ParamUtil.requireNonNull("mode", mode);
for (UpdateMode v : values()) {
if (v.name().equalsIgnoreCase(mode)) {
return v;
}
}
throw new IllegalArgumentException("invalid UpdateMode " + mode);
}
} // enum UpdateMode
public static class HourMinute {
private final int hour;
private final int minute;
public HourMinute(final int hour, final int minute) {
this.hour = ParamUtil.requireRange("hour", hour, 0, 23);
this.minute = ParamUtil.requireRange("minute", minute, 0, 59);
}
public int getHour() {
return hour;
}
public int getMinute() {
return minute;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(100);
if (hour < 10) {
sb.append("0");
}
sb.append(hour);
sb.append(":");
if (minute < 10) {
sb.append("0");
}
sb.append(minute);
return sb.toString();
}
@Override
public int hashCode() {
return toString().hashCode();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof HourMinute)) {
return false;
}
HourMinute hm = (HourMinute) obj;
return hour == hm.hour && minute == hm.minute;
}
} // class HourMinute
public static final String KEY_UPDATE_MODE = "updateMode";
public static final String KEY_EYTENSIONS = "extensions";
public static final String KEY_EXPIRED_CERTS_INCLUDED = "expiredCerts.included";
public static final String KEY_XIPKI_CERTSET = "xipki.certset";
public static final String KEY_XIPKI_CERTSET_CERTS = "xipki.certset.certs";
public static final String KEY_XIPKI_CERTSET_PROFILENAME = "xipki.certset.profilename";
public static final String KEY_FULLCRL_INTERVALS = "fullCRL.intervals";
public static final String KEY_DELTACRL_INTERVALS = "deltaCRL.intervals";
public static final String KEY_OVERLAP_MINUTES = "overlap.minutes";
public static final String KEY_INTERVAL_MINUTES = "interval.minutes";
public static final String KEY_INTERVAL_TIME = "interval.time";
public static final String KEY_FULLCRL_EXTENDED_NEXTUPDATE = "fullCRL.extendedNextUpdate";
public static final String KEY_ONLY_CONTAINS_USERCERTS = "onlyContainsUserCerts";
public static final String KEY_ONLY_CONTAINS_CACERTS = "onlyContainsCACerts";
public static final String KEY_EXCLUDE_REASON = "excludeReason";
public static final String KEY_INVALIDITY_DATE = "invalidityDate";
private UpdateMode updateMode = UpdateMode.interval;
private boolean xipkiCertsetIncluded;
private boolean xipkiCertsetCertIncluded = true;
private boolean xipkiCertsetProfilenameIncluded = true;
private boolean includeExpiredCerts;
private int fullCrlIntervals = 1;
private int deltaCrlIntervals;
private int overlapMinutes = 10;
private boolean extendedNextUpdate;
private Integer intervalMinutes;
private HourMinute intervalDayTime;
private boolean onlyContainsUserCerts;
private boolean onlyContainsCaCerts;
private boolean excludeReason;
private TripleState invalidityDateMode = TripleState.OPTIONAL;
private final Set<String> extensionOids;
public CrlControl(final String conf) throws InvalidConfException {
ConfPairs props;
try {
props = new ConfPairs(conf);
} catch (RuntimeException ex) {
throw new InvalidConfException(ex.getClass().getName() + ": " + ex.getMessage(), ex);
}
String str = props.getValue(KEY_UPDATE_MODE);
this.updateMode = (str == null) ? UpdateMode.interval : UpdateMode.forName(str);
str = props.getValue(KEY_INVALIDITY_DATE);
if (str != null) {
this.invalidityDateMode = TripleState.forValue(str);
}
this.includeExpiredCerts = getBoolean(props, KEY_EXPIRED_CERTS_INCLUDED, false);
this.xipkiCertsetIncluded = getBoolean(props, KEY_XIPKI_CERTSET, false);
this.xipkiCertsetCertIncluded = getBoolean(props, KEY_XIPKI_CERTSET_CERTS, true);
this.xipkiCertsetProfilenameIncluded = getBoolean(props,
KEY_XIPKI_CERTSET_PROFILENAME, true);
str = props.getValue(KEY_EYTENSIONS);
if (str == null) {
this.extensionOids = Collections.emptySet();
} else {
Set<String> oids = StringUtil.splitAsSet(str, ", ");
// check the OID
for (String oid : oids) {
try {
new ASN1ObjectIdentifier(oid);
} catch (IllegalArgumentException ex) {
throw new InvalidConfException(oid + " is not a valid OID");
}
}
this.extensionOids = oids;
}
this.onlyContainsCaCerts = getBoolean(props, KEY_ONLY_CONTAINS_CACERTS, false);
this.onlyContainsUserCerts = getBoolean(props, KEY_ONLY_CONTAINS_USERCERTS, false);
this.excludeReason = getBoolean(props, KEY_EXCLUDE_REASON, false);
if (this.updateMode != UpdateMode.onDemand) {
this.fullCrlIntervals = getInteger(props, KEY_FULLCRL_INTERVALS, 1);
this.deltaCrlIntervals = getInteger(props, KEY_DELTACRL_INTERVALS, 0);
this.extendedNextUpdate = getBoolean(props, KEY_FULLCRL_EXTENDED_NEXTUPDATE, false);
this.overlapMinutes = getInteger(props, KEY_OVERLAP_MINUTES, 60);
str = props.getValue(KEY_INTERVAL_TIME);
if (str != null) {
List<String> tokens = StringUtil.split(str.trim(), ":");
if (tokens.size() != 2) {
throw new InvalidConfException(
"invalid " + KEY_INTERVAL_TIME + ": '" + str + "'");
}
try {
int hour = Integer.parseInt(tokens.get(0));
int minute = Integer.parseInt(tokens.get(1));
this.intervalDayTime = new HourMinute(hour, minute);
} catch (IllegalArgumentException ex) {
throw new InvalidConfException("invalid " + KEY_INTERVAL_TIME + ": '"
+ str + "'");
}
} else {
int minutes = getInteger(props, KEY_INTERVAL_MINUTES, 0);
if (minutes < this.overlapMinutes + 30) {
throw new InvalidConfException("invalid " + KEY_INTERVAL_MINUTES + ": '"
+ minutes + " is less than than 30 + " + this.overlapMinutes);
}
this.intervalMinutes = minutes;
}
}
validate();
} // constructor
public String getConf() {
ConfPairs pairs = new ConfPairs();
pairs.putPair(KEY_UPDATE_MODE, updateMode.name());
pairs.putPair(KEY_EXPIRED_CERTS_INCLUDED, Boolean.toString(includeExpiredCerts));
pairs.putPair(KEY_XIPKI_CERTSET, Boolean.toString(xipkiCertsetIncluded));
pairs.putPair(KEY_XIPKI_CERTSET_CERTS, Boolean.toString(xipkiCertsetCertIncluded));
pairs.putPair(KEY_XIPKI_CERTSET, Boolean.toString(xipkiCertsetIncluded));
pairs.putPair(KEY_ONLY_CONTAINS_CACERTS, Boolean.toString(onlyContainsCaCerts));
pairs.putPair(KEY_ONLY_CONTAINS_USERCERTS, Boolean.toString(onlyContainsUserCerts));
pairs.putPair(KEY_EXCLUDE_REASON, Boolean.toString(excludeReason));
pairs.putPair(KEY_INVALIDITY_DATE, invalidityDateMode.name());
if (updateMode != UpdateMode.onDemand) {
pairs.putPair(KEY_FULLCRL_INTERVALS, Integer.toString(fullCrlIntervals));
pairs.putPair(KEY_FULLCRL_EXTENDED_NEXTUPDATE, Boolean.toString(extendedNextUpdate));
pairs.putPair(KEY_DELTACRL_INTERVALS, Integer.toString(deltaCrlIntervals));
if (intervalDayTime != null) {
pairs.putPair(KEY_INTERVAL_TIME, intervalDayTime.toString());
}
if (intervalMinutes != null) {
pairs.putPair(KEY_INTERVAL_MINUTES, intervalMinutes.toString());
}
}
if (CollectionUtil.isNonEmpty(extensionOids)) {
StringBuilder extensionsSb = new StringBuilder(200);
for (String oid : extensionOids) {
extensionsSb.append(oid).append(",");
}
extensionsSb.deleteCharAt(extensionsSb.length() - 1);
pairs.putPair(KEY_EYTENSIONS, extensionsSb.toString());
}
return pairs.getEncoded();
} // method getConf
@Override
public String toString() {
return getConf();
}
public UpdateMode getUpdateMode() {
return updateMode;
}
public boolean isXipkiCertsetIncluded() {
return xipkiCertsetIncluded;
}
public boolean isXipkiCertsetCertIncluded() {
return xipkiCertsetCertIncluded;
}
public boolean isXipkiCertsetProfilenameIncluded() {
return xipkiCertsetProfilenameIncluded;
}
public boolean isIncludeExpiredCerts() {
return includeExpiredCerts;
}
public int getFullCrlIntervals() {
return fullCrlIntervals;
}
public int getDeltaCrlIntervals() {
return deltaCrlIntervals;
}
public int getOverlapMinutes() {
return overlapMinutes;
}
public Integer getIntervalMinutes() {
return intervalMinutes;
}
public HourMinute getIntervalDayTime() {
return intervalDayTime;
}
public Set<String> getExtensionOids() {
return extensionOids;
}
public boolean isExtendedNextUpdate() {
return extendedNextUpdate;
}
public boolean isOnlyContainsUserCerts() {
return onlyContainsUserCerts;
}
public boolean isOnlyContainsCaCerts() {
return onlyContainsCaCerts;
}
public boolean isExcludeReason() {
return excludeReason;
}
public TripleState getInvalidityDateMode() {
return invalidityDateMode;
}
public void validate() throws InvalidConfException {
if (onlyContainsCaCerts && onlyContainsUserCerts) {
throw new InvalidConfException(
"onlyContainsCACerts and onlyContainsUserCerts can not be both true");
}
if (updateMode == UpdateMode.onDemand) {
return;
}
if (fullCrlIntervals < deltaCrlIntervals) {
throw new InvalidConfException(
"fullCRLIntervals must not be less than deltaCRLIntervals "
+ fullCrlIntervals + " < " + deltaCrlIntervals);
}
if (fullCrlIntervals < 1) {
throw new InvalidConfException(
"fullCRLIntervals must not be less than 1: " + fullCrlIntervals);
}
if (deltaCrlIntervals < 0) {
throw new InvalidConfException(
"deltaCRLIntervals must not be less than 0: " + deltaCrlIntervals);
}
}
@Override
public int hashCode() {
return toString().hashCode();
}
@Override
public boolean equals(final Object obj) {
if (!(obj instanceof CrlControl)) {
return false;
}
CrlControl obj2 = (CrlControl) obj;
if (deltaCrlIntervals != obj2.deltaCrlIntervals
|| xipkiCertsetIncluded != obj2.xipkiCertsetIncluded
|| xipkiCertsetCertIncluded != obj2.xipkiCertsetCertIncluded
|| xipkiCertsetProfilenameIncluded != obj2.xipkiCertsetProfilenameIncluded
|| extendedNextUpdate != obj2.extendedNextUpdate
|| fullCrlIntervals != obj2.fullCrlIntervals
|| includeExpiredCerts != obj2.includeExpiredCerts
|| onlyContainsCaCerts != obj2.onlyContainsCaCerts
|| onlyContainsUserCerts != obj2.onlyContainsUserCerts) {
return false;
}
if (extensionOids == null) {
if (obj2.extensionOids != null) {
return false;
}
} else if (!extensionOids.equals(obj2.extensionOids)) {
return false;
}
if (intervalMinutes == null) {
if (obj2.intervalMinutes != null) {
return false;
}
} else if (!intervalMinutes.equals(obj2.intervalMinutes)) {
return false;
}
if (intervalDayTime == null) {
if (obj2.intervalDayTime != null) {
return false;
}
} else if (!intervalDayTime.equals(obj2.intervalDayTime)) {
return false;
}
if (updateMode == null) {
if (obj2.updateMode != null) {
return false;
}
} else if (!updateMode.equals(obj2.updateMode)) {
return false;
}
return true;
} // method equals
private static int getInteger(final ConfPairs props, final String propKey, final int dfltValue)
throws InvalidConfException {
String str = props.getValue(propKey);
if (str != null) {
try {
return Integer.parseInt(str.trim());
} catch (NumberFormatException ex) {
throw new InvalidConfException(propKey + " does not have numeric value: " + str);
}
}
return dfltValue;
}
private static boolean getBoolean(final ConfPairs props, final String propKey,
final boolean dfltValue) throws InvalidConfException {
String str = props.getValue(propKey);
if (str != null) {
str = str.trim();
if ("true".equalsIgnoreCase(str)) {
return Boolean.TRUE;
} else if ("false".equalsIgnoreCase(str)) {
return Boolean.FALSE;
} else {
throw new InvalidConfException(propKey + " does not have boolean value: " + str);
}
}
return dfltValue;
}
}