/* * * 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.impl.cmp; import java.security.InvalidKeyException; import java.security.SecureRandom; import java.security.cert.X509Certificate; import java.text.ParseException; import java.util.Date; import org.bouncycastle.asn1.ASN1GeneralizedTime; import org.bouncycastle.asn1.ASN1OctetString; import org.bouncycastle.asn1.DEROctetString; import org.bouncycastle.asn1.cmp.ErrorMsgContent; import org.bouncycastle.asn1.cmp.PKIBody; import org.bouncycastle.asn1.cmp.PKIFailureInfo; import org.bouncycastle.asn1.cmp.PKIFreeText; import org.bouncycastle.asn1.cmp.PKIHeader; import org.bouncycastle.asn1.cmp.PKIHeaderBuilder; import org.bouncycastle.asn1.cmp.PKIMessage; import org.bouncycastle.asn1.cmp.PKIStatus; import org.bouncycastle.asn1.cmp.PKIStatusInfo; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; import org.bouncycastle.asn1.x509.GeneralName; import org.bouncycastle.cert.cmp.CMPException; import org.bouncycastle.cert.cmp.GeneralPKIMessage; import org.bouncycastle.cert.cmp.ProtectedPKIMessage; import org.bouncycastle.operator.ContentVerifierProvider; import org.bouncycastle.operator.OperatorCreationException; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xipki.commons.audit.AuditEvent; import org.xipki.commons.audit.AuditLevel; import org.xipki.commons.audit.AuditStatus; import org.xipki.commons.common.util.LogUtil; import org.xipki.commons.common.util.ParamUtil; import org.xipki.commons.common.util.RandomUtil; import org.xipki.commons.security.ConcurrentContentSigner; import org.xipki.commons.security.SecurityFactory; import org.xipki.commons.security.util.X509Util; import org.xipki.pki.ca.common.cmp.CmpUtil; import org.xipki.pki.ca.common.cmp.ProtectionResult; import org.xipki.pki.ca.common.cmp.ProtectionVerificationResult; import org.xipki.pki.ca.server.impl.CaAuditConstants; import org.xipki.pki.ca.server.mgmt.api.CmpControl; import org.xipki.pki.ca.server.mgmt.api.RequestorInfo; /** * @author Lijun Liao * @since 2.0.0 */ abstract class CmpResponder { private static final Logger LOG = LoggerFactory.getLogger(CmpResponder.class); protected final SecurityFactory securityFactory; private final SecureRandom random = new SecureRandom(); protected CmpResponder(final SecurityFactory securityFactory) { this.securityFactory = ParamUtil.requireNonNull("securityFactory", securityFactory); } protected abstract ConcurrentContentSigner getSigner(); protected abstract GeneralName getSender(); protected abstract boolean intendsMe(GeneralName requestRecipient); public boolean isInService() { try { return getSigner() != null; } catch (Exception ex) { LogUtil.error(LOG, ex, "could not get responder signer"); return false; } } /** * @return never returns {@code null}. */ protected abstract CmpControl getCmpControl(); public abstract CmpRequestorInfo getRequestor(X500Name requestorSender); public abstract CmpRequestorInfo getRequestor(X509Certificate requestorCert); private CmpRequestorInfo getRequestor(final PKIHeader reqHeader) { GeneralName requestSender = reqHeader.getSender(); if (requestSender.getTagNo() != GeneralName.directoryName) { return null; } return getRequestor((X500Name) requestSender.getName()); } // method getRequestor protected abstract PKIMessage doProcessPkiMessage(@Nullable PKIMessage request, @NonNull RequestorInfo requestor, @NonNull ASN1OctetString transactionId, @NonNull GeneralPKIMessage pkiMessage, @NonNull String msgId, @NonNull AuditEvent event); public PKIMessage processPkiMessage(final PKIMessage pkiMessage, final X509Certificate tlsClientCert, final String tidStr, final AuditEvent event) { ParamUtil.requireNonNull("pkiMessage", pkiMessage); ParamUtil.requireNonNull("event", event); GeneralPKIMessage message = new GeneralPKIMessage(pkiMessage); PKIHeader reqHeader = message.getHeader(); ASN1OctetString tid = reqHeader.getTransactionID(); String msgId = null; if (event != null) { msgId = RandomUtil.nextHexLong(); event.addEventData(CaAuditConstants.NAME_mid, msgId); } if (tid == null) { byte[] randomBytes = randomTransactionId(); tid = new DEROctetString(randomBytes); } CmpControl cmpControl = getCmpControl(); Integer failureCode = null; String statusText = null; Date messageTime = null; if (reqHeader.getMessageTime() != null) { try { messageTime = reqHeader.getMessageTime().getDate(); } catch (ParseException ex) { LogUtil.error(LOG, ex, "tid=" + tidStr + ": could not parse messageTime"); } } GeneralName recipient = reqHeader.getRecipient(); boolean intentMe = (recipient == null) ? true : intendsMe(recipient); if (!intentMe) { LOG.warn("tid={}: I am not the intended recipient, but '{}'", tid, reqHeader.getRecipient()); failureCode = PKIFailureInfo.badRequest; statusText = "I am not the intended recipient"; } else if (messageTime == null) { if (cmpControl.isMessageTimeRequired()) { failureCode = PKIFailureInfo.missingTimeStamp; statusText = "missing time-stamp"; } } else { long messageTimeBias = cmpControl.getMessageTimeBias(); if (messageTimeBias < 0) { messageTimeBias *= -1; } long msgTimeMs = messageTime.getTime(); long currentTimeMs = System.currentTimeMillis(); long bias = (msgTimeMs - currentTimeMs) / 1000L; if (bias > messageTimeBias) { failureCode = PKIFailureInfo.badTime; statusText = "message time is in the future"; } else if (bias * -1 > messageTimeBias) { failureCode = PKIFailureInfo.badTime; statusText = "message too old"; } } if (failureCode != null) { if (event != null) { event.setLevel(AuditLevel.INFO); event.setStatus(AuditStatus.FAILED); event.addEventData(CaAuditConstants.NAME_message, statusText); } return buildErrorPkiMessage(tid, reqHeader, failureCode, statusText); } boolean isProtected = message.hasProtection(); CmpRequestorInfo requestor; String errorStatus; if (isProtected) { try { ProtectionVerificationResult verificationResult = verifyProtection(tidStr, message, cmpControl); ProtectionResult pr = verificationResult.getProtectionResult(); switch (pr) { case VALID: errorStatus = null; break; case INVALID: errorStatus = "request is protected by signature but invalid"; break; case NOT_SIGNATURE_BASED: errorStatus = "request is not protected by signature"; break; case SENDER_NOT_AUTHORIZED: errorStatus = "request is protected by signature but the requestor is not authorized"; break; case SIGALGO_FORBIDDEN: errorStatus = "request is protected by signature but the protection algorithm" + " is forbidden"; break; default: throw new RuntimeException( "should not reach here, unknown ProtectionResult " + pr); } // end switch requestor = (CmpRequestorInfo) verificationResult.getRequestor(); } catch (Exception ex) { LogUtil.error(LOG, ex, "tid=" + tidStr + ": could not verify the signature"); errorStatus = "request has invalid signature based protection"; requestor = null; } } else if (tlsClientCert != null) { boolean authorized = false; requestor = getRequestor(reqHeader); if (requestor != null) { if (tlsClientCert.equals(requestor.getCert().getCert())) { authorized = true; } } if (authorized) { errorStatus = null; } else { LOG.warn("tid={}: not authorized requestor (TLS client '{}')", tid, X509Util.getRfc4519Name(tlsClientCert.getSubjectX500Principal())); errorStatus = "requestor (TLS client certificate) is not authorized"; } } else { errorStatus = "request has no protection"; requestor = null; } if (errorStatus != null) { if (event != null) { event.setLevel(AuditLevel.INFO); event.setStatus(AuditStatus.FAILED); event.addEventData(CaAuditConstants.NAME_message, errorStatus); } return buildErrorPkiMessage(tid, reqHeader, PKIFailureInfo.badMessageCheck, errorStatus); } PKIMessage resp = doProcessPkiMessage(pkiMessage, requestor, tid, message, msgId, event); if (isProtected) { resp = addProtection(resp, event); } else { // protected by TLS connection } return resp; } // method processPkiMessage protected byte[] randomTransactionId() { byte[] bytes = new byte[10]; random.nextBytes(bytes); return bytes; } private ProtectionVerificationResult verifyProtection(final String tid, final GeneralPKIMessage pkiMessage, final CmpControl cmpControl) throws CMPException, InvalidKeyException, OperatorCreationException { ProtectedPKIMessage protectedMsg = new ProtectedPKIMessage(pkiMessage); if (protectedMsg.hasPasswordBasedMacProtection()) { LOG.warn("NOT_SIGNAUTRE_BASED: {}", pkiMessage.getHeader().getProtectionAlg().getAlgorithm().getId()); return new ProtectionVerificationResult(null, ProtectionResult.NOT_SIGNATURE_BASED); } PKIHeader header = protectedMsg.getHeader(); AlgorithmIdentifier protectionAlg = header.getProtectionAlg(); if (!cmpControl.getSigAlgoValidator().isAlgorithmPermitted(protectionAlg)) { LOG.warn("SIG_ALGO_FORBIDDEN: {}", pkiMessage.getHeader().getProtectionAlg().getAlgorithm().getId()); return new ProtectionVerificationResult(null, ProtectionResult.SIGALGO_FORBIDDEN); } CmpRequestorInfo requestor = getRequestor(header); if (requestor == null) { LOG.warn("tid={}: not authorized requestor '{}'", tid, header.getSender()); return new ProtectionVerificationResult(null, ProtectionResult.SENDER_NOT_AUTHORIZED); } ContentVerifierProvider verifierProvider = securityFactory.getContentVerifierProvider( requestor.getCert().getCert()); if (verifierProvider == null) { LOG.warn("tid={}: not authorized requestor '{}'", tid, header.getSender()); return new ProtectionVerificationResult(requestor, ProtectionResult.SENDER_NOT_AUTHORIZED); } boolean signatureValid = protectedMsg.verify(verifierProvider); return new ProtectionVerificationResult(requestor, signatureValid ? ProtectionResult.VALID : ProtectionResult.INVALID); } // method verifyProtection private PKIMessage addProtection(final PKIMessage pkiMessage, final AuditEvent event) { try { return CmpUtil.addProtection(pkiMessage, getSigner(), getSender(), getCmpControl().isSendResponderCert()); } catch (Exception ex) { LogUtil.error(LOG, ex, "could not add protection to the PKI message"); PKIStatusInfo status = generateRejectionStatus( PKIFailureInfo.systemFailure, "could not sign the PKIMessage"); event.setLevel(AuditLevel.ERROR); event.setStatus(AuditStatus.FAILED); event.addEventData(CaAuditConstants.NAME_message, "could not sign the PKIMessage"); PKIBody body = new PKIBody(PKIBody.TYPE_ERROR, new ErrorMsgContent(status)); return new PKIMessage(pkiMessage.getHeader(), body); } } // method addProtection protected PKIMessage buildErrorPkiMessage(final ASN1OctetString tid, final PKIHeader requestHeader, final int failureCode, final String statusText) { GeneralName respRecipient = requestHeader.getSender(); PKIHeaderBuilder respHeader = new PKIHeaderBuilder( requestHeader.getPvno().getValue().intValue(), getSender(), respRecipient); respHeader.setMessageTime(new ASN1GeneralizedTime(new Date())); if (tid != null) { respHeader.setTransactionID(tid); } PKIStatusInfo status = generateRejectionStatus(failureCode, statusText); ErrorMsgContent error = new ErrorMsgContent(status); PKIBody body = new PKIBody(PKIBody.TYPE_ERROR, error); return new PKIMessage(respHeader.build(), body); } // method buildErrorPkiMessage protected PKIStatusInfo generateRejectionStatus(final Integer info, final String errorMessage) { return generateRejectionStatus(PKIStatus.rejection, info, errorMessage); } // method generateCmpRejectionStatus protected PKIStatusInfo generateRejectionStatus(final PKIStatus status, final Integer info, final String errorMessage) { PKIFreeText statusMessage = (errorMessage == null) ? null : new PKIFreeText(errorMessage); PKIFailureInfo failureInfo = (info == null) ? null : new PKIFailureInfo(info); return new PKIStatusInfo(status, statusMessage, failureInfo); } // method generateCmpRejectionStatus public X500Name getResponderSubject() { GeneralName sender = getSender(); return (sender == null) ? null : (X500Name) sender.getName(); } public X509Certificate getResponderCert() { ConcurrentContentSigner signer = getSigner(); return (signer == null) ? null : signer.getCertificate(); } }