/* * * 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.scep.client; import java.io.IOException; import java.math.BigInteger; import java.net.MalformedURLException; import java.security.PrivateKey; import java.security.cert.CRLException; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.X509CRL; import java.security.cert.X509Certificate; import java.security.spec.InvalidKeySpecException; import java.util.Arrays; import java.util.Date; import java.util.LinkedList; import java.util.List; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.cms.ContentInfo; import org.bouncycastle.asn1.cms.IssuerAndSerialNumber; import org.bouncycastle.asn1.cms.SignedData; import org.bouncycastle.asn1.pkcs.CertificationRequest; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cms.CMSAlgorithm; import org.bouncycastle.cms.CMSException; import org.bouncycastle.cms.CMSSignedData; import org.bouncycastle.util.CollectionStore; import org.bouncycastle.util.encoders.Base64; import org.eclipse.jdt.annotation.NonNull; import org.xipki.commons.common.util.ParamUtil; import org.xipki.pki.scep.client.exception.OperationNotSupportedException; import org.xipki.pki.scep.client.exception.ScepClientException; import org.xipki.pki.scep.crypto.ScepHashAlgoType; import org.xipki.pki.scep.exception.MessageDecodingException; import org.xipki.pki.scep.exception.MessageEncodingException; import org.xipki.pki.scep.message.AuthorityCertStore; import org.xipki.pki.scep.message.CaCaps; import org.xipki.pki.scep.message.DecodedNextCaMessage; import org.xipki.pki.scep.message.DecodedPkiMessage; import org.xipki.pki.scep.message.IssuerAndSubject; import org.xipki.pki.scep.message.PkiMessage; import org.xipki.pki.scep.transaction.CaCapability; import org.xipki.pki.scep.transaction.MessageType; import org.xipki.pki.scep.transaction.Operation; import org.xipki.pki.scep.transaction.TransactionId; import org.xipki.pki.scep.util.ScepConstants; import org.xipki.pki.scep.util.ScepUtil; /** * @author Lijun Liao * @since 2.0.0 */ public abstract class Client { public static final String REQ_CONTENT_TYPE = "application/octet-stream"; // 5 minutes public static final long DEFAULT_SIGNINGTIME_BIAS = 5L * 60 * 1000; protected final CaIdentifier caId; protected CaCaps caCaps; private final CaCertValidator caCertValidator; private long maxSigningTimeBiasInMs = DEFAULT_SIGNINGTIME_BIAS; private AuthorityCertStore authorityCertStore; private CollectionStore<X509CertificateHolder> responseSignerCerts; private boolean httpGetOnly; private boolean useInsecureAlgorithms; public Client(final CaIdentifier caId, final CaCertValidator caCertValidator) throws MalformedURLException { this.caId = ParamUtil.requireNonNull("caId", caId); this.caCertValidator = ParamUtil.requireNonNull("caCertValidator", caCertValidator); } protected abstract ScepHttpResponse httpPost(@NonNull final String url, @NonNull final String requestContentType, @NonNull final byte[] request) throws ScepClientException; protected abstract ScepHttpResponse httpGet(@NonNull final String url) throws ScepClientException; public boolean isHttpGetOnly() { return httpGetOnly; } public void setHttpGetOnly(final boolean httpGetOnly) { this.httpGetOnly = httpGetOnly; } public boolean isUseInsecureAlgorithms() { return useInsecureAlgorithms; } public void setUseInsecureAlgorithms(final boolean useInsecureAlgorithms) { this.useInsecureAlgorithms = useInsecureAlgorithms; } public long getMaxSigningTimeBiasInMs() { return maxSigningTimeBiasInMs; } /** * Set the maximal signing time bias in milliseconds. * @param maxSigningTimeBiasInMs zero or negative value deactivates the message time check */ public void setMaxSigningTimeBiasInMs(final long maxSigningTimeBiasInMs) { this.maxSigningTimeBiasInMs = maxSigningTimeBiasInMs; } private ScepHttpResponse httpSend(final Operation operation, final ContentInfo pkiMessage) throws ScepClientException { byte[] request = null; if (pkiMessage != null) { try { request = pkiMessage.getEncoded(); } catch (IOException ex) { throw new ScepClientException(ex); } } if (Operation.GetCACaps == operation || Operation.GetCACert == operation || Operation.GetNextCACert == operation) { String url = caId.buildGetUrl(operation, caId.getProfile()); return httpGet(url); } else { if (!httpGetOnly && caCaps.containsCapability(CaCapability.POSTPKIOperation)) { String url = caId.buildPostUrl(operation); return httpPost(url, REQ_CONTENT_TYPE, request); } else { String url = caId.buildGetUrl(operation, (request == null) ? null : Base64.toBase64String(request)); return httpGet(url); } } // end if } private ScepHttpResponse httpSend(final Operation operation) throws ScepClientException { return httpSend(operation, null); } public void init() throws ScepClientException { refresh(); } public void refresh() throws ScepClientException { // getCACaps ScepHttpResponse getCaCapsResp = httpSend(Operation.GetCACaps); this.caCaps = CaCaps.getInstance(new String(getCaCapsResp.getContentBytes())); // getCACert ScepHttpResponse getCaCertResp = httpSend(Operation.GetCACert); this.authorityCertStore = retrieveCaCertStore(getCaCertResp, caCertValidator); X509CertificateHolder certHolder; try { certHolder = new X509CertificateHolder( this.authorityCertStore.getSignatureCert().getEncoded()); } catch (CertificateEncodingException ex) { throw new ScepClientException(ex); } catch (IOException ex) { throw new ScepClientException(ex); } this.responseSignerCerts = new CollectionStore<X509CertificateHolder>( Arrays.asList(certHolder)); } public CaCaps getCaCaps() throws ScepClientException { initIfNotInited(); return caCaps; } public CaIdentifier getCaId() throws ScepClientException { initIfNotInited(); return caId; } public CaCertValidator getCaCertValidator() throws ScepClientException { initIfNotInited(); return caCertValidator; } public AuthorityCertStore getAuthorityCertStore() throws ScepClientException { initIfNotInited(); return authorityCertStore; } public X509CRL scepGetCrl(final PrivateKey identityKey, final X509Certificate identityCert, final X500Name issuer, final BigInteger serialNumber) throws ScepClientException { ParamUtil.requireNonNull("identityKey", identityKey); ParamUtil.requireNonNull("identityCert", identityCert); ParamUtil.requireNonNull("issuer", issuer); ParamUtil.requireNonNull("serialNumber", serialNumber); initIfNotInited(); PkiMessage pkiMessage = new PkiMessage(TransactionId.randomTransactionId(), MessageType.GetCRL); IssuerAndSerialNumber isn = new IssuerAndSerialNumber(issuer, serialNumber); pkiMessage.setMessageData(isn); ContentInfo request = encryptThenSign(pkiMessage, identityKey, identityCert); ScepHttpResponse httpResp = httpSend(Operation.PKIOperation, request); CMSSignedData cmsSignedData = parsePkiMessage(httpResp.getContentBytes()); PkiMessage response = decode(cmsSignedData, identityKey, identityCert); ContentInfo messageData = ContentInfo.getInstance(response.getMessageData()); try { return ScepUtil.getCrlFromPkiMessage(SignedData.getInstance(messageData.getContent())); } catch (CRLException ex) { throw new ScepClientException(ex.getMessage(), ex); } } public List<X509Certificate> scepGetCert(final PrivateKey identityKey, final X509Certificate identityCert, final X500Name issuer, final BigInteger serialNumber) throws ScepClientException { ParamUtil.requireNonNull("identityKey", identityKey); ParamUtil.requireNonNull("identityCert", identityCert); ParamUtil.requireNonNull("issuer", issuer); ParamUtil.requireNonNull("serialNumber", serialNumber); initIfNotInited(); PkiMessage request = new PkiMessage(TransactionId.randomTransactionId(), MessageType.GetCert); IssuerAndSerialNumber isn = new IssuerAndSerialNumber(issuer, serialNumber); request.setMessageData(isn); ContentInfo envRequest = encryptThenSign(request, identityKey, identityCert); ScepHttpResponse httpResp = httpSend(Operation.PKIOperation, envRequest); CMSSignedData cmsSignedData = parsePkiMessage(httpResp.getContentBytes()); DecodedPkiMessage response = decode(cmsSignedData, identityKey, identityCert); ContentInfo messageData = ContentInfo.getInstance(response.getMessageData()); try { return ScepUtil.getCertsFromSignedData( SignedData.getInstance(messageData.getContent())); } catch (CertificateException ex) { throw new ScepClientException(ex.getMessage(), ex); } } public EnrolmentResponse scepCertPoll(final PrivateKey identityKey, final X509Certificate identityCert, final CertificationRequest csr, final X500Name issuer) throws ScepClientException { ParamUtil.requireNonNull("csr", csr); TransactionId tid; try { tid = TransactionId.sha1TransactionId( csr.getCertificationRequestInfo().getSubjectPublicKeyInfo()); } catch (InvalidKeySpecException ex) { throw new ScepClientException(ex.getMessage(), ex); } return scepCertPoll(identityKey, identityCert, tid, issuer, csr.getCertificationRequestInfo().getSubject()); } public EnrolmentResponse scepCertPoll(final PrivateKey identityKey, final X509Certificate identityCert, final TransactionId transactionId, final X500Name issuer, final X500Name subject) throws ScepClientException { ParamUtil.requireNonNull("identityKey", identityKey); ParamUtil.requireNonNull("identityCert", identityCert); ParamUtil.requireNonNull("issuer", issuer); ParamUtil.requireNonNull("transactionId", transactionId); initIfNotInited(); PkiMessage pkiMessage = new PkiMessage(transactionId, MessageType.CertPoll); IssuerAndSubject is = new IssuerAndSubject(issuer, subject); pkiMessage.setMessageData(is); ContentInfo envRequest = encryptThenSign(pkiMessage, identityKey, identityCert); ScepHttpResponse httpResp = httpSend(Operation.PKIOperation, envRequest); CMSSignedData cmsSignedData = parsePkiMessage(httpResp.getContentBytes()); DecodedPkiMessage response = decode(cmsSignedData, identityKey, identityCert); assertSameNonce(pkiMessage, response); return new EnrolmentResponse(response); } public EnrolmentResponse scepEnrol(final CertificationRequest csr, final PrivateKey identityKey, final X509Certificate identityCert) throws ScepClientException { ParamUtil.requireNonNull("csr", csr); ParamUtil.requireNonNull("identityKey", identityKey); ParamUtil.requireNonNull("identityCert", identityCert); initIfNotInited(); // draft-nourse-scep if (!isGutmannScep()) { return scepPkcsReq(csr, identityKey, identityCert); } // draft-gutmann-scep if (!ScepUtil.isSelfSigned(identityCert)) { X509Certificate caCert = authorityCertStore.getCaCert(); if (identityCert.getIssuerX500Principal().equals(caCert.getSubjectX500Principal())) { if (caCaps.containsCapability(CaCapability.Renewal)) { return scepRenewalReq(csr, identityKey, identityCert); } } else { if (caCaps.containsCapability(CaCapability.Update)) { return scepUpdateReq(csr, identityKey, identityCert); } } } // end if return scepPkcsReq(csr, identityKey, identityCert); } public EnrolmentResponse scepPkcsReq(final CertificationRequest csr, final PrivateKey identityKey, final X509Certificate identityCert) throws ScepClientException { ParamUtil.requireNonNull("csr", csr); ParamUtil.requireNonNull("identityKey", identityKey); ParamUtil.requireNonNull("identityCert", identityCert); initIfNotInited(); boolean selfSigned = ScepUtil.isSelfSigned(identityCert); if (!selfSigned) { throw new IllegalArgumentException("identityCert is not self-signed"); } return doEnrol(MessageType.PKCSReq, csr, identityKey, identityCert); } public EnrolmentResponse scepRenewalReq(final CertificationRequest csr, final PrivateKey identityKey, final X509Certificate identityCert) throws ScepClientException { initIfNotInited(); if (!caCaps.containsCapability(CaCapability.Renewal)) { throw new OperationNotSupportedException( "unsupported messageType '" + MessageType.RenewalReq + "'"); } boolean selfSigned = ScepUtil.isSelfSigned(identityCert); if (selfSigned) { throw new IllegalArgumentException("identityCert must not be self-signed"); } return doEnrol(MessageType.RenewalReq, csr, identityKey, identityCert); } public EnrolmentResponse scepUpdateReq(final CertificationRequest csr, final PrivateKey identityKey, final X509Certificate identityCert) throws ScepClientException { initIfNotInited(); if (!caCaps.containsCapability(CaCapability.Update)) { throw new OperationNotSupportedException( "unsupported messageType '" + MessageType.UpdateReq + "'"); } boolean selfSigned = ScepUtil.isSelfSigned(identityCert); if (selfSigned) { throw new IllegalArgumentException("identityCert must not be self-signed"); } return doEnrol(MessageType.UpdateReq, csr, identityKey, identityCert); } private EnrolmentResponse doEnrol(final MessageType messageType, final CertificationRequest csr, final PrivateKey identityKey, final X509Certificate identityCert) throws ScepClientException { TransactionId tid; try { tid = TransactionId.sha1TransactionId( csr.getCertificationRequestInfo().getSubjectPublicKeyInfo()); } catch (InvalidKeySpecException ex) { throw new ScepClientException(ex.getMessage(), ex); } PkiMessage pkiMessage = new PkiMessage(tid, messageType); pkiMessage.setMessageData(csr); ContentInfo envRequest = encryptThenSign(pkiMessage, identityKey, identityCert); ScepHttpResponse httpResp = httpSend(Operation.PKIOperation, envRequest); CMSSignedData cmsSignedData = parsePkiMessage(httpResp.getContentBytes()); DecodedPkiMessage response = decode(cmsSignedData, identityKey, identityCert); assertSameNonce(pkiMessage, response); return new EnrolmentResponse(response); } public AuthorityCertStore scepNextCaCert() throws ScepClientException { initIfNotInited(); if (!this.caCaps.containsCapability(CaCapability.GetNextCACert)) { throw new OperationNotSupportedException( "unsupported operation '" + Operation.GetNextCACert.getCode() + "'"); } ScepHttpResponse resp = httpSend(Operation.GetNextCACert); return retrieveNextCaAuthorityCertStore(resp); } private ContentInfo encryptThenSign(final PkiMessage request, final PrivateKey identityKey, final X509Certificate identityCert) throws ScepClientException { ScepHashAlgoType hashAlgo = caCaps.getMostSecureHashAlgo(); if (hashAlgo == ScepHashAlgoType.MD5 && !useInsecureAlgorithms) { throw new ScepClientException( "Scep server supports only MD5 but it not permitted in client"); } String signatureAlgorithm = ScepUtil.getSignatureAlgorithm(identityKey, hashAlgo); ASN1ObjectIdentifier encAlgId; if (caCaps.containsCapability(CaCapability.AES)) { encAlgId = CMSAlgorithm.AES128_CBC; } else if (caCaps.containsCapability(CaCapability.DES3)) { encAlgId = CMSAlgorithm.DES_EDE3_CBC; } else if (useInsecureAlgorithms) { encAlgId = CMSAlgorithm.DES_CBC; } else { // no support of DES throw new ScepClientException("DES will not be supported by this client"); } try { return request.encode(identityKey, signatureAlgorithm, identityCert, new X509Certificate[]{identityCert}, authorityCertStore.getEncryptionCert(), encAlgId); } catch (MessageEncodingException ex) { throw new ScepClientException(ex); } } public void destroy() { } private AuthorityCertStore retrieveNextCaAuthorityCertStore(final ScepHttpResponse httpResp) throws ScepClientException { String ct = httpResp.getContentType(); if (!ScepConstants.CT_X509_NEXT_CA_CERT.equalsIgnoreCase(ct)) { throw new ScepClientException("invalid Content-Type '" + ct + "'"); } CMSSignedData cmsSignedData; try { cmsSignedData = new CMSSignedData(httpResp.getContentBytes()); } catch (CMSException ex) { throw new ScepClientException("invalid SignedData message: " + ex.getMessage(), ex); } catch (IllegalArgumentException ex) { throw new ScepClientException("invalid SignedData message: " + ex.getMessage(), ex); } DecodedNextCaMessage resp; try { resp = DecodedNextCaMessage.decode(cmsSignedData, responseSignerCerts); } catch (MessageDecodingException ex) { throw new ScepClientException("could not decode response: " + ex.getMessage(), ex); } if (resp.getFailureMessage() != null) { throw new ScepClientException("Error: " + resp.getFailureMessage()); } Boolean bo = resp.isSignatureValid(); if (bo != null && !bo.booleanValue()) { throw new ScepClientException("Signature is invalid"); } Date signingTime = resp.getSigningTime(); long maxSigningTimeBias = getMaxSigningTimeBiasInMs(); if (maxSigningTimeBias > 0) { if (signingTime == null) { throw new ScepClientException("CMS signingTime attribute is not present"); } long now = System.currentTimeMillis(); long diff = now - signingTime.getTime(); if (diff < 0) { diff = -1 * diff; } if (diff > maxSigningTimeBias) { throw new ScepClientException("CMS signingTime is out of permitted period"); } } if (!resp.getSignatureCert().equals(authorityCertStore.getSignatureCert())) { throw new ScepClientException("the signature certificate must not be trusted"); } return resp.getAuthorityCertStore(); } // method retrieveNextCaAuthorityCertStore private void initIfNotInited() throws ScepClientException { if (caCaps == null) { init(); } } private DecodedPkiMessage decode(final CMSSignedData pkiMessage, final PrivateKey recipientKey, final X509Certificate recipientCert) throws ScepClientException { DecodedPkiMessage resp; try { resp = DecodedPkiMessage.decode(pkiMessage, recipientKey, recipientCert, responseSignerCerts); } catch (MessageDecodingException ex) { throw new ScepClientException(ex); } if (resp.getFailureMessage() != null) { throw new ScepClientException("Error: " + resp.getFailureMessage()); } Boolean bo = resp.isSignatureValid(); if (bo != null && !bo.booleanValue()) { throw new ScepClientException("Signature is invalid"); } bo = resp.isDecryptionSuccessful(); if (bo != null && !bo.booleanValue()) { throw new ScepClientException("Decryption failed"); } Date signingTime = resp.getSigningTime(); long maxSigningTimeBias = getMaxSigningTimeBiasInMs(); if (maxSigningTimeBias > 0) { if (signingTime == null) { throw new ScepClientException("CMS signingTime attribute is not present"); } long now = System.currentTimeMillis(); long diff = now - signingTime.getTime(); if (diff < 0) { diff = -1 * diff; } if (diff > maxSigningTimeBias) { throw new ScepClientException("CMS signingTime is out of permitted period"); } } if (!resp.getSignatureCert().equals(authorityCertStore.getSignatureCert())) { throw new ScepClientException("the signature certificate must not be trusted"); } return resp; } // method decode private boolean isGutmannScep() { return caCaps.containsCapability(CaCapability.AES) || caCaps.containsCapability(CaCapability.Update); } private static X509Certificate parseCert(final byte[] certBytes) throws ScepClientException { try { return ScepUtil.parseCert(certBytes); } catch (CertificateException ex) { throw new ScepClientException(ex); } } private static CMSSignedData parsePkiMessage(final byte[] messageBytes) throws ScepClientException { try { return new CMSSignedData(messageBytes); } catch (CMSException ex) { throw new ScepClientException(ex); } } private static AuthorityCertStore retrieveCaCertStore(final ScepHttpResponse resp, final CaCertValidator caValidator) throws ScepClientException { String ct = resp.getContentType(); X509Certificate caCert = null; List<X509Certificate> raCerts = new LinkedList<X509Certificate>(); if (ScepConstants.CT_X509_CA_CERT.equalsIgnoreCase(ct)) { caCert = parseCert(resp.getContentBytes()); } else if (ScepConstants.CT_X509_CA_RA_CERT.equalsIgnoreCase(ct)) { ContentInfo contentInfo = ContentInfo.getInstance(resp.getContentBytes()); SignedData signedData; try { signedData = SignedData.getInstance(contentInfo.getContent()); } catch (IllegalArgumentException ex) { throw new ScepClientException("invalid SignedData message: " + ex.getMessage(), ex); } List<X509Certificate> certs; try { certs = ScepUtil.getCertsFromSignedData(signedData); } catch (CertificateException ex) { throw new ScepClientException(ex.getMessage(), ex); } final int n = certs.size(); if (n < 2) { throw new ScepClientException( "at least 2 certificates are expected, but only " + n + " is available"); } for (int i = 0; i < n; i++) { X509Certificate cert = certs.get(i); if (cert.getBasicConstraints() > -1) { if (caCert != null) { throw new ScepClientException( "multiple CA certificates is returned, but exactly 1 is expected"); } caCert = cert; } else { raCerts.add(cert); } } if (caCert == null) { throw new ScepClientException("no CA certificate is returned"); } } else { throw new ScepClientException("invalid Content-Type '" + ct + "'"); } if (!caValidator.isTrusted(caCert)) { throw new ScepClientException( "CA certificate '" + caCert.getSubjectX500Principal() + "' is not trusted"); } if (raCerts.isEmpty()) { return AuthorityCertStore.getInstance(caCert); } else { AuthorityCertStore cs = AuthorityCertStore.getInstance( caCert, raCerts.toArray(new X509Certificate[0])); X509Certificate raEncCert = cs.getEncryptionCert(); X509Certificate raSignCert = cs.getSignatureCert(); try { if (!ScepUtil.issues(caCert, raEncCert)) { throw new ScepClientException("RA certificate '" + raEncCert.getSubjectX500Principal() + " is not issued by the CA"); } if (raSignCert != raEncCert && ScepUtil.issues(caCert, raSignCert)) { throw new ScepClientException("RA certificate '" + raSignCert.getSubjectX500Principal() + " is not issued by the CA"); } } catch (CertificateException ex) { throw new ScepClientException("invalid certificate: " + ex.getMessage(), ex); } return cs; } } // method retrieveCaCertStore private static void assertSameNonce(final PkiMessage request, final PkiMessage response) throws ScepClientException { if (request.getSenderNonce().equals(response.getRecipientNonce())) { throw new ScepClientException( "SenderNonce of the request and RecipientNonce of response are not the same"); } } }