Skip to content

Commit

Permalink
Ticket #2619 Allow CA to process user-signed CMC revocation requests
Browse files Browse the repository at this point in the history
First of all, the original CMC revocation only supports agent-signed CMC revocation requests from the UI where CMCRevReqServlet handles it with CMCAuth.  It is in violation with https://tools.ietf.org/html/rfc5273 CMC Transport Protocols, as for HTTP/HTTPS, the body of the message is the binary value of the BER encoding of the PKI Request or Response,so HTML is not an approved method.The other way is through profileSubmitCMCFullServlet (or maybe not, as it was completely broken).

One thing that's much less crucial, but goes along with rfc update is the name of the revocation request ASN1 structure. In the new rfc5272, it is now called RevokeRequest insead of RevRequest.

This patch revamped the CMC revocation provision and fixing what's broken and adding what's missing.

On the client side:

CMCRequest

- Commented out the code where it made an assumption to use OtherMsg for the signer information. This makes no sense as the outer layer SignedData would have the same information when signing happens.

- removed the revRequest.nickname parameter from the configuration.  From the code it seems like it expects the certificate to be revoked to exist in the user database, and it uses the same certificate to sign the revocation request.  The RFC does allow for self-signed revocation, but it also allows for signing with another certificate provided that it has same subject.  By removing the revRequest.nickname parameter, I am using the "nickname" parameter as the signer cert, which may or may not be the same certificate specified in revRequest.serial.  It is less confusing. The change also eliminate the need for the cert to be revoked to be present in the db.  In addition, revRequest.issuer only needs to be specified if revRequest.sharedSecret is used. The code will extract the issuer info from the signing cert.

- added support for unsigned data in support of shared secret in both CMCRequest and server;  The original code assumed that a cmc revocation request that relies on shared secret still requires agent signing.

CMCRevoke

- The original code assumed that the nss db password is the same as Shared Secret (!!).  This patch added a "-t" to accept shred secret, and keep the -p for the nss db password.

- The original code printed out b64 encoded request to the screen output as well as the file CMCRevoke.out.  Both are unusable directly.  This patch fixes it so that the output to the screen can be directly copied and pasted into the CMC revocate ui at ee (processed by CMCRevReqServlet);  Again, this is not RFC conforming, but I fixed it anyways;

- The output to the file CMCRevoke.out has been fixed so that it is the BER encoding of the request, which can be fed directly into the updated server that now conforms to the RFC (see below)

- This code still requires the signer certificate nickname to run, making the shared secret method moot.  Since CMCRequest has been updated to work properly, we can leave this for now.

On the server side.

CMCUserSignedAuth has been updated to handle unsigned DATA;  Recall that the original CMC revocation only handled SIGNED_DATA (making assumption that agent always signs the requests).  This addition is important to support shared secrets properly.

Another thing that's important change on the server side is that it now checks the revoking cert's subject against the signer's subject, if authenticated by CMCUserSignedAuth.  The original code did not do that, I think it is because it always checks if it's an agent or not.

Something that could be improved on is to have its own servlet.  However, due to the time restriction, I only updated existing EnrollProfile, ProfileSubmitCMCServlet, and CMCOutputTemplate to handle the rfc conforming cmc revocation requests.

The shared secret handling is left in the CMCOutputTemplate for now.  Logically it would make sense to go into CMCUserSignedAuth. This could be left as a possible later ticket for improvement.   Shared Token plugin implementation will be added in later ticket as well.

Previously missed signing cert validation is also added for more complete check.
Some SHA1 are turned into SHA2

Finally, some auditing are added, but it is not finalized.  It will be done in the next ticket(s).
  • Loading branch information
ladycfu committed Jun 9, 2017
1 parent 5bf30f2 commit 698192f
Show file tree
Hide file tree
Showing 16 changed files with 769 additions and 426 deletions.
10 changes: 10 additions & 0 deletions base/common/src/com/netscape/certsrv/apps/CMS.java
Expand Up @@ -36,6 +36,7 @@
import org.mozilla.jss.CryptoManager.CertificateUsage;
import org.mozilla.jss.util.PasswordCallback;

import com.netscape.certsrv.authentication.ISharedToken;
import com.netscape.certsrv.acls.EACLsException;
import com.netscape.certsrv.acls.IACL;
import com.netscape.certsrv.authentication.IAuthSubsystem;
Expand Down Expand Up @@ -1574,6 +1575,15 @@ public static IPasswordCheck getPasswordChecker() {
return _engine.getPasswordChecker();
}

/**
* Retrieves the SharedToken class.
*
* @return named SharedToken class
*/
public static ISharedToken getSharedTokenClass(String configName) {
return _engine.getSharedTokenClass(configName);
}

/**
* Puts a password entry into the single-sign on cache.
*
Expand Down
8 changes: 8 additions & 0 deletions base/common/src/com/netscape/certsrv/apps/ICMSEngine.java
Expand Up @@ -38,6 +38,7 @@

import com.netscape.certsrv.acls.EACLsException;
import com.netscape.certsrv.acls.IACL;
import com.netscape.certsrv.authentication.ISharedToken;
import com.netscape.certsrv.authority.IAuthority;
import com.netscape.certsrv.base.EBaseException;
import com.netscape.certsrv.base.IArgBlock;
Expand Down Expand Up @@ -680,6 +681,13 @@ public LDAPConnection getBoundConnection(String id, String host, int port,
*/
public ILdapConnFactory getLdapAnonConnFactory(String id) throws ELdapException;

/**
* Retrieves the named SharedToken class
*
* @return named shared token class
*/
public ISharedToken getSharedTokenClass(String configName);

/**
* Retrieves the password check.
*
Expand Down
5 changes: 5 additions & 0 deletions base/common/src/com/netscape/certsrv/base/SessionContext.java
Expand Up @@ -52,6 +52,11 @@ public class SessionContext extends Hashtable<Object, Object> {
*/
public static final String AUTH_MANAGER_ID = "authManagerId"; // String

/**
* Principal name object of the signed CMC request
*/
public static final String CMC_SIGNER_PRINCIPAL = "cmcSignerPrincipal";

/**
* User object of the authenticated user in the current thread.
*/
Expand Down
251 changes: 164 additions & 87 deletions base/java-tools/src/com/netscape/cmstools/CMCRequest.java

Large diffs are not rendered by default.

133 changes: 71 additions & 62 deletions base/java-tools/src/com/netscape/cmstools/CMCRevoke.java
Expand Up @@ -75,6 +75,7 @@ public class CMCRevoke {
public static final String RFC7468_TRAILER = "-----END CERTIFICATE REQUEST-----";
static String dValue = null, nValue = null, iValue = null, sValue = null, mValue = null, hValue = null,
pValue = null, cValue = null;
static String tValue = null;

public static final String CMS_BASE_CA_SIGNINGCERT_NOT_FOUND = "CA signing certificate not found";
public static final String PR_REQUEST_CMC = "CMC";
Expand Down Expand Up @@ -109,8 +110,9 @@ public static void main(String[] s) {
"-d<dir to cert8.db, key3.db> " +
"-n<nickname> " +
"-i<issuerName> " +
"-s<serialName> " +
"-s<serialNumber> " +
"-m<reason to revoke> " +
"-t<shared secret> " +
"-p<password to db> " +
"-h<tokenname> " +
"-c<comment> ");
Expand All @@ -135,6 +137,8 @@ public static void main(String[] s) {
mValue = cleanArgs(s[i].substring(2));
} else if (s[i].startsWith("-p")) {
pValue = cleanArgs(s[i].substring(2));
} else if (s[i].startsWith("-t")) {
tValue = cleanArgs(s[i].substring(2));
} else if (s[i].startsWith("-h")) {
hValue = cleanArgs(s[i].substring(2));
} else if (s[i].startsWith("-c")) {
Expand All @@ -143,8 +147,6 @@ public static void main(String[] s) {

}
// optional parameters
if (cValue == null)
cValue = "";
if (hValue == null)
hValue = "";

Expand All @@ -160,7 +162,7 @@ else if (dValue.length() == 0 || nValue.length() == 0 || iValue.length() == 0 ||
"-d<dir to cert8.db, key3.db> " +
"-n<nickname> " +
"-i<issuerName> " +
"-s<serialName> " +
"-s<serialNumber> " +
"-m<reason to revoke> " +
"-p<password to db> " +
"-h<tokenname> " +
Expand Down Expand Up @@ -191,9 +193,9 @@ else if (dValue.length() == 0 || nValue.length() == 0 || iValue.length() == 0 ||

token.login(pass);
X509Certificate signerCert = getCertificate(cm, hValue, nValue);
String outBlob = createRevokeReq(hValue, signerCert, cm);
ContentInfo fullEnrollmentRequest = createRevokeReq(hValue, signerCert, cm);

printCMCRevokeRequest(outBlob);
printCMCRevokeRequest(fullEnrollmentRequest);
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
Expand All @@ -209,29 +211,48 @@ else if (dValue.length() == 0 || nValue.length() == 0 || iValue.length() == 0 ||
*
* @param asciiBASE64Blob the ascii string of the request
*/
static void printCMCRevokeRequest(String asciiBASE64Blob) {
static void printCMCRevokeRequest(ContentInfo fullEnrollmentReq) {
String method = "printCMCRevokeRequest: ";

// (6) Finally, print the actual CMCSigning blob to the
ByteArrayOutputStream os = new ByteArrayOutputStream();
ByteArrayOutputStream bs = new ByteArrayOutputStream();
PrintStream ps = new PrintStream(bs);

if (fullEnrollmentReq == null) {
System.out.println(method + "param fullEnrollmentRequest is null");
System.exit(1);
}
// format is PR_REQUEST_CMC
try {
fullEnrollmentReq.encode(os);
} catch (IOException e) {
System.out.println("CMCSigning: I/O error " +
"encountered during write():\n" +
e);
System.exit(1);
}
//ps.print(Utils.base64encode(os.toByteArray()));
// no line breaks for ease of copy/paste for CA acceptance
System.out.println(RFC7468_HEADER);
ps.print(Utils.base64encodeSingleLine(os.toByteArray()));
////fullEnrollmentReq.print(ps); // no header/trailer

String asciiBASE64Blob = bs.toString();
System.out.println(asciiBASE64Blob + "\n" + RFC7468_TRAILER);

// (6) Finally, print the actual CMCSigning binary blob to the
// specified output file
FileOutputStream outputBlob = null;

try {
outputBlob = new FileOutputStream("CMCRevoke.out");
fullEnrollmentReq.encode(outputBlob);
} catch (IOException e) {
System.out.println("CMCSigning: unable to open file CMCRevoke.out for writing:\n" + e);
return;
}

System.out.println(RFC7468_HEADER);
System.out.println(asciiBASE64Blob + RFC7468_TRAILER);
try {
asciiBASE64Blob = RFC7468_HEADER + "\n" + asciiBASE64Blob + RFC7468_TRAILER;
outputBlob.write(asciiBASE64Blob.getBytes());
} catch (IOException e) {
System.out.println("CMCSigning: I/O error " +
"encountered during write():\n" +
e);
}
System.out.println("\nCMC revocation binary blob written to CMCRevoke.out\n");

try {
outputBlob.close();
Expand Down Expand Up @@ -280,12 +301,11 @@ public static X509Certificate getCertificate(CryptoManager manager, String token
* @param manager the crypto manger.
* @return the CMC revocation request encoded in base64
*/
static String createRevokeReq(String tokenname, X509Certificate signerCert, CryptoManager manager) {
static ContentInfo createRevokeReq(String tokenname, X509Certificate signerCert, CryptoManager manager) {

java.security.PrivateKey privKey = null;
SignerIdentifier si = null;
ContentInfo fullEnrollmentReq = null;
String asciiBASE64Blob = null;

try {

Expand All @@ -305,8 +325,8 @@ static String createRevokeReq(String tokenname, X509Certificate signerCert, Cryp

if (privKey == null) {
System.out.println("CMCRevoke::createRevokeReq() - " +
"privKey is null!");
return "";
"privKey is null!");
return null;
}

int bpid = 1;
Expand All @@ -319,65 +339,64 @@ static String createRevokeReq(String tokenname, X509Certificate signerCert, Cryp
byte[] dig;

try {
MessageDigest SHA1Digest = MessageDigest.getInstance("SHA1");
MessageDigest SHA2Digest = MessageDigest.getInstance("SHA256");

dig = SHA1Digest.digest(salt.getBytes());
dig = SHA2Digest.digest(salt.getBytes());
} catch (NoSuchAlgorithmException ex) {
dig = salt.getBytes();
}
String sn = Utils.base64encode(dig);

TaggedAttribute senderNonce =
new TaggedAttribute(new INTEGER(bpid++), OBJECT_IDENTIFIER.id_cmc_senderNonce,
new OCTET_STRING(sn.getBytes()));
TaggedAttribute senderNonce = new TaggedAttribute(new INTEGER(bpid++), OBJECT_IDENTIFIER.id_cmc_senderNonce,
new OCTET_STRING(sn.getBytes()));

controlSeq.addElement(senderNonce);

Name subjectName = new Name();

subjectName.addCommonName(iValue);
org.mozilla.jss.pkix.cmmf.RevRequest lRevokeRequest =
new org.mozilla.jss.pkix.cmmf.RevRequest(new ANY((new X500Name(iValue)).getEncoded()),
new INTEGER(sValue),
//org.mozilla.jss.pkix.cmmf.RevRequest.unspecified,
new ENUMERATED((new Integer(mValue)).longValue()),
null,
new OCTET_STRING(pValue.getBytes()),
new UTF8String(cValue.toCharArray()));
org.mozilla.jss.pkix.cmc.RevokeRequest lRevokeRequest = new org.mozilla.jss.pkix.cmc.RevokeRequest(
new ANY((new X500Name(iValue)).getEncoded()),
new INTEGER(sValue),
//org.mozilla.jss.pkix.cmc.RevokeRequest.unspecified,
new ENUMERATED((new Integer(mValue)).longValue()),
null,
(tValue != null) ? new OCTET_STRING(tValue.getBytes()) : null,
(cValue != null) ? new UTF8String(cValue.toCharArray()) : null);
//byte[] encoded = ASN1Util.encode(lRevokeRequest);
//org.mozilla.jss.asn1.ASN1Template template = new org.mozilla.jss.pkix.cmmf.RevRequest.Template();
//org.mozilla.jss.pkix.cmmf.RevRequest revRequest = (org.mozilla.jss.pkix.cmmf.RevRequest)
//org.mozilla.jss.asn1.ASN1Template template = new org.mozilla.jss.pkix.cmc.RevokeRequest.Template();
//org.mozilla.jss.pkix.cmc.RevokeRequest revRequest = (org.mozilla.jss.pkix.cmc.RevokeRequest)
// template.decode(new java.io.ByteArrayInputStream(
// encoded));

ByteArrayOutputStream os = new ByteArrayOutputStream();
//lRevokeRequest.encode(os); // khai
TaggedAttribute revokeRequestTag =
new TaggedAttribute(new INTEGER(bpid++), OBJECT_IDENTIFIER.id_cmc_revokeRequest,
lRevokeRequest);
TaggedAttribute revokeRequestTag = new TaggedAttribute(new INTEGER(bpid++),
OBJECT_IDENTIFIER.id_cmc_revokeRequest,
lRevokeRequest);

controlSeq.addElement(revokeRequestTag);
PKIData pkidata = new PKIData(controlSeq, new SEQUENCE(), new SEQUENCE(), new SEQUENCE());

EncapsulatedContentInfo ci = new EncapsulatedContentInfo(OBJECT_IDENTIFIER.id_cct_PKIData, pkidata);
// SHA1 is the default digest Alg for now.
DigestAlgorithm digestAlg = null;
SignatureAlgorithm signAlg = null;
org.mozilla.jss.crypto.PrivateKey.Type signingKeyType = ((org.mozilla.jss.crypto.PrivateKey) privKey).getType();
org.mozilla.jss.crypto.PrivateKey.Type signingKeyType = ((org.mozilla.jss.crypto.PrivateKey) privKey)
.getType();
if (signingKeyType.equals(org.mozilla.jss.crypto.PrivateKey.Type.RSA)) {
signAlg = SignatureAlgorithm.RSASignatureWithSHA1Digest;
signAlg = SignatureAlgorithm.RSASignatureWithSHA256Digest;
} else if (signingKeyType.equals(org.mozilla.jss.crypto.PrivateKey.Type.EC)) {
signAlg = SignatureAlgorithm.ECSignatureWithSHA1Digest;
} else if (signingKeyType.equals(org.mozilla.jss.crypto.PrivateKey.Type.DSA)) {
signAlg = SignatureAlgorithm.DSASignatureWithSHA1Digest;
signAlg = SignatureAlgorithm.ECSignatureWithSHA256Digest;
} else {
System.out.println("Algorithm not supported:" +
signingKeyType);
return null;
}

MessageDigest SHADigest = null;
byte[] digest = null;

try {
SHADigest = MessageDigest.getInstance("SHA1");
digestAlg = DigestAlgorithm.SHA1;
SHADigest = MessageDigest.getInstance("SHA256");
digestAlg = DigestAlgorithm.SHA256;

ByteArrayOutputStream ostream = new ByteArrayOutputStream();

Expand Down Expand Up @@ -411,21 +430,11 @@ static String createRevokeReq(String tokenname, X509Certificate signerCert, Cryp

fullEnrollmentReq = new ContentInfo(req);

ByteArrayOutputStream bs = new ByteArrayOutputStream();
PrintStream ps = new PrintStream(bs);

if (fullEnrollmentReq != null) {
// format is PR_REQUEST_CMC
fullEnrollmentReq.encode(os);
ps.print(Utils.base64encode(os.toByteArray()));
////fullEnrollmentReq.print(ps); // no header/trailer
}

asciiBASE64Blob = bs.toString();
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
return asciiBASE64Blob;

return fullEnrollmentReq;
}
}
19 changes: 14 additions & 5 deletions base/server/cms/src/com/netscape/cms/authentication/CMCAuth.java
Expand Up @@ -237,6 +237,9 @@ public void init(String name, String implName, IConfigStore config)
*/
public IAuthToken authenticate(IAuthCredentials authCred) throws EMissingCredential, EInvalidCredentials,
EBaseException {
String method = "CMCAuth: authenticate: ";
String msg = "";

String auditMessage = null;
String auditSubjectID = auditSubjectID();
String auditReqType = ILogger.UNIDENTIFIED;
Expand All @@ -261,7 +264,7 @@ public IAuthToken authenticate(IAuthCredentials authCred) throws EMissingCredent
}
String cmc = (String) returnVal;
if (cmc == null) {
CMS.debug("CMCAuth: Authentication failed. Missing CMC.");
CMS.debug(method + "Authentication failed. Missing CMC.");

// store a message in the signed audit log file
auditMessage = CMS.getLogMessage(
Expand All @@ -279,8 +282,9 @@ public IAuthToken authenticate(IAuthCredentials authCred) throws EMissingCredent
}

if (cmc.equals("")) {
log(ILogger.LL_FAILURE,
"cmc : attempted login with empty CMC.");
msg = "attempted login with empty CMC";
CMS.debug(method + msg);
log(ILogger.LL_FAILURE, method + msg);

// store a message in the signed audit log file
auditMessage = CMS.getLogMessage(
Expand Down Expand Up @@ -331,6 +335,7 @@ public IAuthToken authenticate(IAuthCredentials authCred) throws EMissingCredent
if (!cmcReq.getContentType().equals(
org.mozilla.jss.pkix.cms.ContentInfo.SIGNED_DATA) ||
!cmcReq.hasContent()) {
CMS.debug(method + "malformed cmc: either not ContentInfo.SIGNED_DATA or cmcReq has no content");
// store a message in the signed audit log file
auditMessage = CMS.getLogMessage(
AuditEvent.CMC_SIGNED_REQUEST_SIG_VERIFY,
Expand Down Expand Up @@ -358,13 +363,13 @@ public IAuthToken authenticate(IAuthCredentials authCred) throws EMissingCredent
if (checkSignerInfo) {
IAuthToken agentToken = verifySignerInfo(authToken, cmcFullReq);
if (agentToken == null) {
CMS.debug("CMCAuth: authenticate() agentToken null");
CMS.debug(method + "agentToken null");
throw new EBaseException("CMCAuth: agent verifySignerInfo failure");
}
userid = agentToken.getInString("userid");
uid = agentToken.getInString("cn");
} else {
CMS.debug("CMCAuth: authenticate() signerInfo verification bypassed");
CMS.debug(method + "signerInfo verification bypassed");
}
// reset value of auditSignerInfo
if (uid != null) {
Expand All @@ -377,6 +382,8 @@ public IAuthToken authenticate(IAuthCredentials authCred) throws EMissingCredent

if (!id.equals(OBJECT_IDENTIFIER.id_cct_PKIData) ||
!ci.hasContent()) {
msg = "request EncapsulatedContentInfo content type not OBJECT_IDENTIFIER.id_cct_PKIData";
CMS.debug( method + msg);
// store a message in the signed audit log file
auditMessage = CMS.getLogMessage(
AuditEvent.CMC_SIGNED_REQUEST_SIG_VERIFY,
Expand Down Expand Up @@ -406,6 +413,7 @@ public IAuthToken authenticate(IAuthCredentials authCred) throws EMissingCredent

if (numReqs == 0) {
// revocation request
CMS.debug(method + "numReqs 0, assume revocation request");

// reset value of auditReqType
auditReqType = SIGNED_AUDIT_REVOCATION_REQUEST_TYPE;
Expand Down Expand Up @@ -476,6 +484,7 @@ public IAuthToken authenticate(IAuthCredentials authCred) throws EMissingCredent
}
} else {
// enrollment request
CMS.debug(method + "numReqs not 0, assume enrollment request");

// reset value of auditReqType
auditReqType = SIGNED_AUDIT_ENROLLMENT_REQUEST_TYPE;
Expand Down

0 comments on commit 698192f

Please sign in to comment.