/* * * 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.rest; import java.io.EOFException; import java.io.IOException; import java.math.BigInteger; import java.net.URLDecoder; import java.security.cert.X509CRL; import java.security.cert.X509Certificate; import java.util.Date; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.bouncycastle.asn1.pkcs.CertificationRequest; import org.bouncycastle.asn1.pkcs.CertificationRequestInfo; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x509.Extensions; import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; import org.bouncycastle.util.Arrays; import org.bouncycastle.util.encoders.Base64; 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.AuditService; import org.xipki.commons.audit.AuditServiceRegister; import org.xipki.commons.audit.AuditStatus; import org.xipki.commons.common.util.DateUtil; import org.xipki.commons.common.util.IoUtil; import org.xipki.commons.common.util.LogUtil; import org.xipki.commons.common.util.RandomUtil; import org.xipki.commons.common.util.StringUtil; import org.xipki.commons.security.CrlReason; import org.xipki.commons.security.X509Cert; import org.xipki.pki.ca.api.InsuffientPermissionException; import org.xipki.pki.ca.api.NameId; import org.xipki.pki.ca.api.OperationException; import org.xipki.pki.ca.api.OperationException.ErrorCode; import org.xipki.pki.ca.api.RequestType; import org.xipki.pki.ca.api.RestfulAPIConstants; import org.xipki.pki.ca.api.publisher.x509.X509CertificateInfo; import org.xipki.pki.ca.server.impl.CaAuditConstants; import org.xipki.pki.ca.server.impl.CertTemplateData; import org.xipki.pki.ca.server.impl.ClientCertCache; import org.xipki.pki.ca.server.impl.HttpRespAuditException; import org.xipki.pki.ca.server.impl.X509Ca; import org.xipki.pki.ca.server.impl.cmp.CmpResponderManager; import org.xipki.pki.ca.server.impl.util.CaUtil; import org.xipki.pki.ca.server.mgmt.api.CaStatus; import org.xipki.pki.ca.server.mgmt.api.PermissionConstants; import org.xipki.pki.ca.server.mgmt.api.RequestorInfo; /** * @author Lijun Liao * @since 2.1.0 */ public class HttpRestServlet extends HttpServlet { private static final Logger LOG = LoggerFactory.getLogger(HttpRestServlet.class); private static final long serialVersionUID = 1L; private CmpResponderManager responderManager; private AuditServiceRegister auditServiceRegister; private boolean sslCertInHttpHeader; public HttpRestServlet() { } @Override public void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { doService(true, request, response); } @Override public void doPost(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { doService(false, request, response); } private void doService(final boolean perRequest, final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { AuditService auditService = auditServiceRegister.getAuditService(); AuditEvent event = new AuditEvent(new Date()); event.setApplicationName(CaAuditConstants.APPNAME); event.setName(CaAuditConstants.NAME_PERF); event.addEventData(CaAuditConstants.NAME_reqType, RequestType.REST.name()); String msgId = RandomUtil.nextHexLong(); event.addEventData(CaAuditConstants.NAME_mid, msgId); AuditLevel auditLevel = AuditLevel.INFO; AuditStatus auditStatus = AuditStatus.SUCCESSFUL; String auditMessage = null; try { if (responderManager == null) { String message = "responderManager in servlet not configured"; LOG.error(message); throw new HttpRespAuditException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null, message, AuditLevel.ERROR, AuditStatus.FAILED); } String requestUri = request.getRequestURI(); String servletPath = request.getServletPath(); String caName = null; String command = null; X509Ca ca = null; int len = servletPath.length(); if (requestUri.length() > len + 1) { String coreUri = URLDecoder.decode(requestUri.substring(len + 1), "UTF-8"); int sepIndex = coreUri.indexOf('/'); if (sepIndex == -1 || sepIndex == coreUri.length() - 1) { String message = "invalid requestURI " + requestUri; LOG.error(message); throw new HttpRespAuditException(HttpServletResponse.SC_NOT_FOUND, null, message, AuditLevel.ERROR, AuditStatus.FAILED); } String caAlias = coreUri.substring(0, sepIndex).toUpperCase(); command = coreUri.substring(sepIndex + 1); caName = responderManager.getCaNameForAlias(caAlias); if (caName == null) { caName = caAlias; } caName = caName.toUpperCase(); ca = responderManager.getX509CaResponder(caName).getCa(); } if (caName == null || ca == null || ca.getCaInfo().getStatus() != CaStatus.ACTIVE) { String message; if (caName == null) { message = "no CA is specified"; } else if (ca == null) { message = "unknown CA '" + caName + "'"; } else { message = "CA '" + caName + "' is out of service"; } LOG.warn(message); throw new HttpRespAuditException(HttpServletResponse.SC_NOT_FOUND, null, message, AuditLevel.INFO, AuditStatus.FAILED); } event.addEventData(CaAuditConstants.NAME_CA, ca.getCaIdent().getName()); event.addEventType(command); RequestorInfo requestor; // Retrieve the user:password String hdrValue = request.getHeader("Authorization"); if (hdrValue != null && hdrValue.startsWith("Basic ")) { String user = null; byte[] password = null; if (hdrValue.length() > 6) { String b64 = hdrValue.substring(6); byte[] userPwd = Base64.decode(b64); int idx = -1; for (int i = 0; i < userPwd.length; i++) { if (userPwd[i] == ':') { idx = i; break; } } if (idx != -1 && idx < userPwd.length - 1) { user = new String(Arrays.copyOfRange(userPwd, 0, idx)); password = Arrays.copyOfRange(userPwd, idx + 1, userPwd.length); } } if (user == null) { throw new HttpRespAuditException(HttpServletResponse.SC_UNAUTHORIZED, "invalid Authorization information", AuditLevel.INFO, AuditStatus.FAILED); } NameId userIdent = ca.authenticateUser(user, password); if (userIdent == null) { throw new HttpRespAuditException(HttpServletResponse.SC_UNAUTHORIZED, "could not authencate user", AuditLevel.INFO, AuditStatus.FAILED); } requestor = ca.getByUserRequestor(userIdent); } else { X509Certificate clientCert = ClientCertCache.getTlsClientCert(request, sslCertInHttpHeader); if (clientCert == null) { throw new HttpRespAuditException(HttpServletResponse.SC_UNAUTHORIZED, null, "no client certificate", AuditLevel.INFO, AuditStatus.FAILED); } requestor = ca.getRequestor(clientCert); } if (requestor == null) { throw new OperationException(ErrorCode.NOT_PERMITTED, "no requestor specified"); } event.addEventData(CaAuditConstants.NAME_requestor, requestor.getIdent().getName()); String respCt = null; byte[] respBytes = null; if (RestfulAPIConstants.CMD_cacert.equalsIgnoreCase(command)) { respCt = RestfulAPIConstants.CT_pkix_cert; respBytes = ca.getCaInfo().getCertificate().getEncodedCert(); } else if (RestfulAPIConstants.CMD_enroll_cert.equalsIgnoreCase(command)) { String profile = request.getParameter(RestfulAPIConstants.PARAM_profile); if (StringUtil.isBlank(profile)) { throw new HttpRespAuditException(HttpServletResponse.SC_BAD_REQUEST, null, "required parameter " + RestfulAPIConstants.PARAM_profile + " not specified", AuditLevel.INFO, AuditStatus.FAILED); } profile = profile.toUpperCase(); try { requestor.assertPermitted(PermissionConstants.ENROLL_CERT); } catch (InsuffientPermissionException ex) { throw new OperationException(ErrorCode.NOT_PERMITTED, ex.getMessage()); } if (!requestor.isCertProfilePermitted(profile)) { throw new OperationException(ErrorCode.NOT_PERMITTED, "certProfile " + profile + " is not allowed"); } String ct = request.getContentType(); if (!RestfulAPIConstants.CT_pkcs10.equalsIgnoreCase(ct)) { String message = "unsupported media type " + ct; throw new HttpRespAuditException(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE, message, AuditLevel.INFO, AuditStatus.FAILED); } String strNotBefore = request.getParameter(RestfulAPIConstants.PARAM_not_before); Date notBefore = (strNotBefore == null) ? null : DateUtil.parseUtcTimeyyyyMMddhhmmss(strNotBefore); String strNotAfter = request.getParameter(RestfulAPIConstants.PARAM_not_after); Date notAfter = (strNotAfter == null) ? null : DateUtil.parseUtcTimeyyyyMMddhhmmss(strNotAfter); byte[] encodedCsr = IoUtil.read(request.getInputStream()); CertificationRequest csr = CertificationRequest.getInstance(encodedCsr); ca.checkCsr(csr); CertificationRequestInfo certTemp = csr.getCertificationRequestInfo(); X500Name subject = certTemp.getSubject(); SubjectPublicKeyInfo publicKeyInfo = certTemp.getSubjectPublicKeyInfo(); Extensions extensions = CaUtil.getExtensions(certTemp); CertTemplateData certTemplate = new CertTemplateData(subject, publicKeyInfo, notBefore, notAfter, extensions, profile); X509CertificateInfo certInfo = ca.generateCertificate(certTemplate, requestor, RequestType.REST, null, msgId); if (ca.getCaInfo().isSaveRequest()) { long dbId = ca.addRequest(encodedCsr); ca.addRequestCert(dbId, certInfo.getCert().getCertId()); } X509Cert cert = certInfo.getCert(); if (cert == null) { String message = "could not generate certificate"; LOG.warn(message); throw new HttpRespAuditException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null, message, AuditLevel.INFO, AuditStatus.FAILED); } respCt = RestfulAPIConstants.CT_pkix_cert; respBytes = cert.getEncodedCert(); } else if (RestfulAPIConstants.CMD_revoke_cert.equalsIgnoreCase(command) || RestfulAPIConstants.CMD_delete_cert.equalsIgnoreCase(command)) { int permission; if (RestfulAPIConstants.CMD_revoke_cert.equalsIgnoreCase(command)) { permission = PermissionConstants.REVOKE_CERT; } else { permission = PermissionConstants.REMOVE_CERT; } try { requestor.assertPermitted(permission); } catch (InsuffientPermissionException ex) { throw new OperationException(ErrorCode.NOT_PERMITTED, ex.getMessage()); } String strCaSha1 = request.getParameter(RestfulAPIConstants.PARAM_ca_sha1); if (StringUtil.isBlank(strCaSha1)) { throw new HttpRespAuditException(HttpServletResponse.SC_BAD_REQUEST, null, "required parameter " + RestfulAPIConstants.PARAM_ca_sha1 + " not specified", AuditLevel.INFO, AuditStatus.FAILED); } String strSerialNumber = request.getParameter( RestfulAPIConstants.PARAM_serial_number); if (StringUtil.isBlank(strSerialNumber)) { throw new HttpRespAuditException(HttpServletResponse.SC_BAD_REQUEST, null, "required parameter " + RestfulAPIConstants.PARAM_serial_number + " not specified", AuditLevel.INFO, AuditStatus.FAILED); } if (!strCaSha1.equalsIgnoreCase(ca.getHexSha1OfCert())) { throw new HttpRespAuditException(HttpServletResponse.SC_BAD_REQUEST, null, "unknown " + RestfulAPIConstants.PARAM_ca_sha1, AuditLevel.INFO, AuditStatus.FAILED); } BigInteger serialNumber = toBigInt(strSerialNumber); if (RestfulAPIConstants.CMD_revoke_cert.equalsIgnoreCase(command)) { String strReason = request.getParameter(RestfulAPIConstants.PARAM_reason); CrlReason reason = (strReason == null) ? CrlReason.UNSPECIFIED : CrlReason.forNameOrText(strReason); Date invalidityTime = null; String strInvalidityTime = request.getParameter( RestfulAPIConstants.PARAM_invalidity_time); if (StringUtil.isNotBlank(strInvalidityTime)) { invalidityTime = DateUtil.parseUtcTimeyyyyMMddhhmmss(strInvalidityTime); } ca.revokeCertificate(serialNumber, reason, invalidityTime, msgId); } else if (RestfulAPIConstants.CMD_delete_cert.equalsIgnoreCase(command)) { ca.removeCertificate(serialNumber, msgId); } } else if (RestfulAPIConstants.CMD_crl.equalsIgnoreCase(command)) { try { requestor.assertPermitted(PermissionConstants.GET_CRL); } catch (InsuffientPermissionException ex) { throw new OperationException(ErrorCode.NOT_PERMITTED, ex.getMessage()); } String strCrlNumber = request.getParameter(RestfulAPIConstants.PARAM_crl_number); BigInteger crlNumber = null; if (StringUtil.isNotBlank(strCrlNumber)) { try { crlNumber = toBigInt(strCrlNumber); } catch (NumberFormatException ex) { String message = "invalid crlNumber '" + strCrlNumber + "'"; LOG.warn(message); throw new HttpRespAuditException(HttpServletResponse.SC_BAD_REQUEST, null, message, AuditLevel.INFO, AuditStatus.FAILED); } } X509CRL crl = ca.getCrl(crlNumber); if (crl == null) { String message = "could not get CRL"; LOG.warn(message); throw new HttpRespAuditException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null, message, AuditLevel.INFO, AuditStatus.FAILED); } respCt = RestfulAPIConstants.CT_pkix_crl; respBytes = crl.getEncoded(); } else if (RestfulAPIConstants.CMD_new_crl.equalsIgnoreCase(command)) { try { requestor.assertPermitted(PermissionConstants.GEN_CRL); } catch (InsuffientPermissionException ex) { throw new OperationException(ErrorCode.NOT_PERMITTED, ex.getMessage()); } X509CRL crl = ca.generateCrlOnDemand(msgId); if (crl == null) { String message = "could not generate CRL"; LOG.warn(message); throw new HttpRespAuditException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null, message, AuditLevel.INFO, AuditStatus.FAILED); } respCt = RestfulAPIConstants.CT_pkix_crl; respBytes = crl.getEncoded(); } else { String message = "invalid command '" + command + "'"; LOG.error(message); throw new HttpRespAuditException(HttpServletResponse.SC_NOT_FOUND, message, AuditLevel.INFO, AuditStatus.FAILED); } response.setStatus(HttpServletResponse.SC_OK); response.setHeader(RestfulAPIConstants.HEADER_PKISTATUS, RestfulAPIConstants.PKISTATUS_accepted); if (StringUtil.isNotBlank(respCt)) { response.setContentType(respCt); } if (respBytes != null) { response.setContentLength(respBytes.length); response.getOutputStream().write(respBytes); } } catch (OperationException ex) { ErrorCode code = ex.getErrorCode(); LOG.warn("generate certificate, OperationException: code={}, message={}", code.name(), ex.getErrorMessage()); int sc; String failureInfo; switch (code) { case ALREADY_ISSUED: sc = HttpServletResponse.SC_BAD_REQUEST; failureInfo = RestfulAPIConstants.FAILINFO_badRequest; break; case BAD_CERT_TEMPLATE: sc = HttpServletResponse.SC_BAD_REQUEST; failureInfo = RestfulAPIConstants.FAILINFO_badCertTemplate; break; case BAD_REQUEST: sc = HttpServletResponse.SC_BAD_REQUEST; failureInfo = RestfulAPIConstants.FAILINFO_badRequest; break; case CERT_REVOKED: sc = HttpServletResponse.SC_CONFLICT; failureInfo = RestfulAPIConstants.FAILINFO_certRevoked; break; case CRL_FAILURE: sc = HttpServletResponse.SC_INTERNAL_SERVER_ERROR; failureInfo = RestfulAPIConstants.FAILINFO_systemFailure; break; case DATABASE_FAILURE: sc = HttpServletResponse.SC_INTERNAL_SERVER_ERROR; failureInfo = RestfulAPIConstants.FAILINFO_systemFailure; break; case NOT_PERMITTED: sc = HttpServletResponse.SC_UNAUTHORIZED; failureInfo = RestfulAPIConstants.FAILINFO_notAuthorized; break; case INVALID_EXTENSION: sc = HttpServletResponse.SC_BAD_REQUEST; failureInfo = RestfulAPIConstants.FAILINFO_badRequest; break; case SYSTEM_FAILURE: sc = HttpServletResponse.SC_INTERNAL_SERVER_ERROR; failureInfo = RestfulAPIConstants.FAILINFO_systemFailure; break; case SYSTEM_UNAVAILABLE: sc = HttpServletResponse.SC_SERVICE_UNAVAILABLE; failureInfo = RestfulAPIConstants.FAILINFO_systemUnavail; break; case UNKNOWN_CERT: sc = HttpServletResponse.SC_BAD_REQUEST; failureInfo = RestfulAPIConstants.FAILINFO_badCertId; break; case UNKNOWN_CERT_PROFILE: sc = HttpServletResponse.SC_BAD_REQUEST; failureInfo = RestfulAPIConstants.FAILINFO_badCertTemplate; break; default: sc = HttpServletResponse.SC_INTERNAL_SERVER_ERROR; failureInfo = RestfulAPIConstants.FAILINFO_systemFailure; break; } // end switch (code) event.setStatus(AuditStatus.FAILED); event.addEventData(CaAuditConstants.NAME_message, code.name()); switch (code) { case DATABASE_FAILURE: case SYSTEM_FAILURE: auditMessage = code.name(); break; default: auditMessage = code.name() + ": " + ex.getErrorMessage(); break; } // end switch code response.setContentLength(0); response.setStatus(sc); response.setHeader(RestfulAPIConstants.HEADER_PKISTATUS, RestfulAPIConstants.PKISTATUS_rejection); if (StringUtil.isNotBlank(failureInfo)) { response.setHeader(RestfulAPIConstants.HEADER_failInfo, failureInfo); } } catch (HttpRespAuditException ex) { auditStatus = ex.getAuditStatus(); auditLevel = ex.getAuditLevel(); auditMessage = ex.getAuditMessage(); response.setContentLength(0); response.setStatus(ex.getHttpStatus()); } catch (EOFException ex) { LogUtil.warn(LOG, ex, "connection reset by peer"); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); response.setContentLength(0); } catch (Throwable th) { final String message = "Throwable thrown, this should not happen!"; LogUtil.error(LOG, th, message); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); response.setContentLength(0); auditLevel = AuditLevel.ERROR; auditStatus = AuditStatus.FAILED; auditMessage = "internal error"; } finally { try { response.flushBuffer(); } finally { audit(auditService, event, auditLevel, auditStatus, auditMessage); } } } // method doService public void setResponderManager(final CmpResponderManager responderManager) { this.responderManager = responderManager; } public void setAuditServiceRegister(final AuditServiceRegister auditServiceRegister) { this.auditServiceRegister = auditServiceRegister; } public void setSslCertInHttpHeader(final boolean sslCertInHttpHeader) { this.sslCertInHttpHeader = sslCertInHttpHeader; } private static void audit(final AuditService auditService, final AuditEvent event, final AuditLevel auditLevel, final AuditStatus auditStatus, final String auditMessage) { if (auditLevel != null) { event.setLevel(auditLevel); } if (auditStatus != null) { event.setStatus(auditStatus); } if (auditMessage != null) { event.addEventData(CaAuditConstants.NAME_message, auditMessage); } event.finish(); auditService.logEvent(event); } // method audit private static BigInteger toBigInt(final String str) { String tmpStr = str.trim(); if (tmpStr.startsWith("0x") || tmpStr.startsWith("0X")) { if (tmpStr.length() > 2) { return new BigInteger(tmpStr.substring(2), 16); } else { throw new NumberFormatException("invalid integer '" + tmpStr + "'"); } } return new BigInteger(tmpStr); } }