Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ The callback handler is used to verify the CA certificate being sent by the SCEP

### Default Callback Mechanism

The default callback mechanism provides a `DefaultCallbackHandler` which delegates verification to a `CertificateVerifier` implementation. jscep supports several strategies for verifying a certificate, including pre-provisioned certificates or digests, and an interactive console verifier. The following example shows the steps necessary to configure the console verifier:
The default callback mechanism provides a `DefaultCallbackHandler` which delegates verification to a `CertificateVerifier` implementation and uses `PasswordCallback` for challengePassword fetching. jscep supports several strategies for verifying a certificate, including pre-provisioned certificates or digests, and an interactive console verifier. The following example shows the steps necessary to configure the console verifier:

```java
CertificateVerifier verifier = new ConsoleCertificateVerifier();
Expand All @@ -99,10 +99,21 @@ By default, jscep will request verification before each operation. If you are p
CertificateVerifier verifier = new CachingCertificateVerifier(consoleVerifier);
CallbackHandler handler = new DefaultCallbackHandler(verifier);
```
In SCEP, according to RFC8894 Section 3.1, if the key is encryption capable (for example, RSA), then the messageData is encrypted using the recipient's public key with the CMS KeyTransRecipientInfo mechanism. If the key is not encryption capable (for example, DSA or ECDSA), then the messageData is encrypted using the challengePassword with the CMS PasswordRecipientInfo mechanism.

Therefore, if the CA certificate contains an EC or DSA key, it is necessary to provide an extra challengePassword. This can be done by defining a hash table in which the key is the CA profile name and the value is the challengePassword value. This approach allows you to define many separate challenge passwords for different CA profiles.

```java
CertificateVerifier verifier = new ConsoleCertificateVerifier();
Map<String, String> passwords = new HashMap<>();
passwords.put("CA1", "secret1");
passwords.put("CA2", "secret2");
CallbackHandler handler = new DefaultCallbackHandler(verifier, passwords);
```

### Providing Your Own Callback Handler

If you wish to use your own `CallbackHandler`, you must handle the `CertificateVerificationCallback`.
If you wish to use your own `CallbackHandler`, you must handle the `CertificateVerificationCallback` and `PasswordCallback` as well.

# Creating the Client

Expand Down
30 changes: 27 additions & 3 deletions src/main/java/org/jscep/client/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.x500.X500Principal;

Expand Down Expand Up @@ -703,10 +704,11 @@ private PkiMessageEncoder getEncoder(final X509Certificate identity,
Capabilities caps = getCaCapabilities(profile);
CertStoreInspector certs = inspectorFactory.getInstance(store);
X509Certificate recipientCertificate = certs.getRecipient();
String challengePassword = getChallengePassword(profile);
PkcsPkiEnvelopeEncoder envEncoder = new PkcsPkiEnvelopeEncoder(
recipientCertificate, caps.getStrongestCipher());
recipientCertificate, challengePassword, caps.getStrongestCipher());

String sigAlg = caps.getStrongestSignatureAlgorithm();
String sigAlg = caps.getStrongestSignatureAlgorithm(priKey.getAlgorithm());
return new PkiMessageEncoder(priKey, identity, envEncoder, sigAlg);
}

Expand All @@ -715,8 +717,9 @@ private PkiMessageDecoder getDecoder(final X509Certificate identity,
final CertStore store = getCaCertificate(profile);
CertStoreInspector certs = inspectorFactory.getInstance(store);
X509Certificate signer = certs.getSigner();
String challengePassword = getChallengePassword(profile);
PkcsPkiEnvelopeDecoder envDecoder = new PkcsPkiEnvelopeDecoder(
identity, key);
identity, key, challengePassword);

return new PkiMessageDecoder(signer, envDecoder);
}
Expand All @@ -736,6 +739,27 @@ private Transport createTransport(final String profile) {
}
}

/**
* Get challenge password using CallbackHandler and PasswordCallback
* @param profile the SCEP server profile
* @return challenge password or null
*/
private String getChallengePassword(String profile) throws ClientException {
try {
LOGGER.debug("Requesting challenge password.");
PasswordCallback callback = new PasswordCallback("Enter challenge password"
+ (profile != null ? " for " + profile : ""), false);
Callback[] callbacks = new Callback[1];
callbacks[0] = callback;
handler.handle(callbacks);
char[] password = callback.getPassword();
return password != null ? new String(password) : null;
} catch (Exception e) {
LOGGER.debug("Requesting challenge password failed.");
throw new ClientException(e);
}
}

private void verifyCA(final X509Certificate cert) throws ClientException {
CertificateVerificationCallback callback = new CertificateVerificationCallback(
cert);
Expand Down
43 changes: 42 additions & 1 deletion src/main/java/org/jscep/client/DefaultCallbackHandler.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package org.jscep.client;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;

import org.jscep.client.verification.CertificateVerifier;
Expand All @@ -16,15 +19,27 @@ public final class DefaultCallbackHandler implements CallbackHandler {
* The verifier.
*/
private final CertificateVerifier verifier;
private final Map<String, String> passwords;

/**
* Default callback handler that delegates verification to a verifier.
*
* @param verifier
* the verifier to use.
*/
public DefaultCallbackHandler(final CertificateVerifier verifier) {
public DefaultCallbackHandler(final CertificateVerifier verifier, final Map<String, String> passwords) {
this.verifier = verifier;
this.passwords = passwords;
}

/**
* Default callback handler that delegates verification to a verifier.
*
* @param verifier
* the verifier to use.
*/
public DefaultCallbackHandler(final CertificateVerifier verifier) {
this(verifier, new HashMap<>());
}

/**
Expand All @@ -36,6 +51,8 @@ public void handle(final Callback[] callbacks) throws IOException,
for (Callback callback : callbacks) {
if (callback instanceof CertificateVerificationCallback) {
verify(CertificateVerificationCallback.class.cast(callback));
} else if (callback instanceof PasswordCallback) {
handle(PasswordCallback.class.cast(callback));
} else {
throw new UnsupportedCallbackException(callback);
}
Expand All @@ -52,4 +69,28 @@ private void verify(final CertificateVerificationCallback callback) {
callback.setVerified(verifier.verify(callback.getCertificate()));
}

/**
* Provide specific password based on profile name in callback's prompt.
*
* @param callback the callback to handle
*/
private void handle(final PasswordCallback callback) {
if (passwords == null) {
return;
}
if (passwords.size() == 1) {
// we have only one password, just return it
String password = passwords.get(passwords.keySet().iterator().next());
if (password != null) {
callback.setPassword(password.toCharArray());
}
} else {
// if we have many passwords, return one selected by profile name included in prompt
for (String key : passwords.keySet()) {
if (callback.getPrompt().contains(key)) {
callback.setPassword(passwords.get(key).toCharArray());
}
}
}
}
}
33 changes: 24 additions & 9 deletions src/main/java/org/jscep/message/PkcsPkiEnvelopeDecoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,8 @@
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.cms.EnvelopedData;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.cms.CMSEnvelopedData;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.RecipientInformation;
import org.bouncycastle.cms.RecipientInformationStore;
import org.bouncycastle.cms.RecipientOperator;
import org.bouncycastle.cms.*;
import org.bouncycastle.cms.bc.BcPasswordEnvelopedRecipient;
import org.bouncycastle.cms.jcajce.JceKeyTransEnvelopedRecipient;
import org.bouncycastle.cms.jcajce.JceKeyTransRecipientId;
import org.bouncycastle.operator.InputDecryptor;
Expand All @@ -38,6 +35,7 @@ public final class PkcsPkiEnvelopeDecoder {
private static final Logger LOGGER = getLogger(PkcsPkiEnvelopeDecoder.class);
private final X509Certificate recipient;
private final PrivateKey privKey;
private final String challengePassword;

/**
* Creates a {@code PkcsPkiEnveloperDecoder} for the provided certificate
Expand All @@ -52,9 +50,10 @@ public final class PkcsPkiEnvelopeDecoder {
* the key to unwrap the symmetric encrypting key.
*/
public PkcsPkiEnvelopeDecoder(final X509Certificate recipient,
final PrivateKey privKey) {
final PrivateKey privKey, final String challengePassword) {
this.recipient = recipient;
this.privKey = privKey;
this.challengePassword = challengePassword;
}

/**
Expand All @@ -81,15 +80,27 @@ public byte[] decode(final CMSEnvelopedData pkcsPkiEnvelope)
.get(new JceKeyTransRecipientId(recipient));

if (info == null) {
throw new MessageDecodingException(
info = recipientInfos.get(new PasswordRecipientId());

if (info == null) {
throw new MessageDecodingException(
"Missing expected key transfer recipient " + recipient.getSubjectX500Principal());
}
}

LOGGER.debug("pkcsPkiEnvelope encryption algorithm: {}", info
.getKeyEncryptionAlgorithm().getAlgorithm());

try {
byte[] messageData = info.getContent(getKeyTransRecipient());
byte[] messageData;
if (info.getRID().getType() == RecipientId.keyTrans) {
messageData = info.getContent(getKeyTransRecipient());
} else if (info.getRID().getType() == RecipientId.password) {
messageData = info.getContent(getPasswordRecipient());
} else {
throw new MessageDecodingException(
"Unsupported recipient type: " + info.getRID().getType());
}
LOGGER.debug("Finished decoding pkcsPkiEnvelope");
return messageData;
} catch (CMSException e) {
Expand All @@ -101,14 +112,18 @@ private JceKeyTransEnvelopedRecipient getKeyTransRecipient() {
return new InternalKeyTransEnvelopedRecipient(privKey);
}

private Recipient getPasswordRecipient() {
return new BcPasswordEnvelopedRecipient(challengePassword.toCharArray());
}

private void validate(final CMSEnvelopedData pkcsPkiEnvelope) {
EnvelopedData ed = EnvelopedData.getInstance(pkcsPkiEnvelope
.toASN1Structure().getContent());
LOGGER.debug("pkcsPkiEnvelope version: {}", ed.getVersion());
LOGGER.debug("pkcsPkiEnvelope encryptedContentInfo contentType: {}", ed
.getEncryptedContentInfo().getContentType());
}

private static class InternalKeyTransEnvelopedRecipient extends JceKeyTransEnvelopedRecipient {
private static final String RSA = "RSA/ECB/PKCS1Padding";
private static final String DES = "DES";
Expand Down
25 changes: 23 additions & 2 deletions src/main/java/org/jscep/message/PkcsPkiEnvelopeEncoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.bouncycastle.cms.RecipientInfoGenerator;
import org.bouncycastle.cms.jcajce.JceCMSContentEncryptorBuilder;
import org.bouncycastle.cms.jcajce.JceKeyTransRecipientInfoGenerator;
import org.bouncycastle.cms.jcajce.JcePasswordRecipientInfoGenerator;
import org.bouncycastle.operator.OutputEncryptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -32,6 +33,7 @@ public final class PkcsPkiEnvelopeEncoder {
private static final Logger LOGGER = LoggerFactory
.getLogger(PkcsPkiEnvelopeEncoder.class);
private final X509Certificate recipient;
private final String challengePassword;
private final ASN1ObjectIdentifier encAlgId;

/**
Expand All @@ -43,7 +45,7 @@ public final class PkcsPkiEnvelopeEncoder {
*/
@Deprecated
public PkcsPkiEnvelopeEncoder(final X509Certificate recipient) {
this(recipient, "DES");
this(recipient, null, "DES");
}

/**
Expand All @@ -56,8 +58,10 @@ public PkcsPkiEnvelopeEncoder(final X509Certificate recipient) {
* the encryption algorithm to use.
*/
public PkcsPkiEnvelopeEncoder(final X509Certificate recipient,
final String challengePassword,
final String encAlg) {
this.recipient = recipient;
this.challengePassword = challengePassword;
this.encAlgId = getAlgorithmId(encAlg);
}

Expand All @@ -77,8 +81,13 @@ public CMSEnvelopedData encode(final byte[] messageData)
CMSTypedData envelopable = new CMSProcessableByteArray(messageData);
RecipientInfoGenerator recipientGenerator;
try {
recipientGenerator = new JceKeyTransRecipientInfoGenerator(
if (isRecipientEncryptionCapable()) {
recipientGenerator = new JceKeyTransRecipientInfoGenerator(
recipient);
} else {
recipientGenerator = new JcePasswordRecipientInfoGenerator(
encAlgId, challengePassword.toCharArray());
}
} catch (CertificateEncodingException e) {
throw new MessageEncodingException(e);
}
Expand Down Expand Up @@ -124,4 +133,16 @@ else if ("DESede".equals(encAlg)) {
throw new IllegalArgumentException("Unknown algorithm: " + encAlg);
}
}

/**
* Check if recipient's key can encrypt data.
* @return true if it can encrypt data
*/
private boolean isRecipientEncryptionCapable() {
// RFC8894 Section 3.1: If the key is encryption capable (for example, RSA), then the
// messageData is encrypted using the recipient's public key with the CMS KeyTransRecipientInfo
// mechanism. If the key is not encryption capable (for example, DSA or ECDSA), then the messageData is
// encrypted using the challengePassword with the CMS PasswordRecipientInfo mechanism.
return recipient != null && recipient.getPublicKey().getAlgorithm().equals("RSA");
}
}
11 changes: 9 additions & 2 deletions src/main/java/org/jscep/server/ScepServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ public final void service(final HttpServletRequest req,
PkiMessage<?> msg;
try {
PkcsPkiEnvelopeDecoder envDecoder = new PkcsPkiEnvelopeDecoder(
getRecipient(), getRecipientKey());
getRecipient(), getRecipientKey(), getChallengePassword());
PkiMessageDecoder decoder = new PkiMessageDecoder(reqCert,
envDecoder);
msg = decoder.decode(sd);
Expand Down Expand Up @@ -313,7 +313,7 @@ public final void service(final HttpServletRequest req,
}

PkcsPkiEnvelopeEncoder envEncoder = new PkcsPkiEnvelopeEncoder(
reqCert, "DESede");
reqCert, getChallengePassword(), "DESede");
PkiMessageEncoder encoder = new PkiMessageEncoder(getSignerKey(),
getSigner(), getSignerCertificateChain(), envEncoder);
CMSSignedData signedData;
Expand Down Expand Up @@ -571,6 +571,13 @@ protected abstract List<X509Certificate> doEnrol(
final X509Certificate sender,
final TransactionId transId) throws Exception;

/**
* Returns challenge password.
*
* @return challenge password
*/
protected abstract String getChallengePassword();

/**
* Returns the private key of the recipient entity represented by this SCEP
* server.
Expand Down
Loading