package org.keysupport.bc.scvp;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.Security;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.bouncycastle.asn1.ASN1GeneralizedTime;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.ASN1SequenceParser;
import org.bouncycastle.asn1.ASN1SetParser;
import org.bouncycastle.asn1.ASN1StreamParser;
import org.bouncycastle.asn1.ASN1TaggedObjectParser;
import org.bouncycastle.asn1.ASN1UTCTime;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.cms.Attribute;
import org.bouncycastle.asn1.cms.Attributes;
import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
import org.bouncycastle.asn1.cms.ContentInfoParser;
import org.bouncycastle.asn1.cms.IssuerAndSerialNumber;
import org.bouncycastle.asn1.cms.SignerIdentifier;
import org.bouncycastle.asn1.cms.SignerInfo;
import org.bouncycastle.asn1.util.ASN1Dump;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.Certificate;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.Arrays;
import org.keysupport.bc.scvp.asn1.ResponseTypes;
import org.keysupport.bc.scvp.asn1.RevocationInfoTypes;
import org.keysupport.bc.scvp.asn1.ServerPolicyRequest;
import org.keysupport.bc.scvp.asn1.ValPolRequest;
import org.keysupport.crypto.CipherEngine;
import org.keysupport.crypto.DigestEngine;
import org.keysupport.util.DataUtil;
public class ServerPolicyClient {
private static final Logger log = Logger.getLogger(ServerPolicyClient.class.getPackage().getName());
private Provider jceProvider = null;
private byte[] fullRequest = null;
private byte[] fullResponse = null;
public ServerPolicyClient(Provider jceProvider) {
this.jceProvider = jceProvider;
}
public static void usage() {
System.out.println("usage: java -jar SCVPAPI.jar <scvp_url>");
}
public static void main(String args[]) throws SCVPException {
/*
* We are going to override the platform logger for
* this example and throw all messages to the console.
*/
log.setUseParentHandlers(false);
ConsoleHandler handler = new ConsoleHandler();
log.setLevel(Level.ALL);
handler.setLevel(Level.ALL);
log.addHandler(handler);
String scvpUrl = null;
if (args.length <= 0) {
usage();
return;
} else {
scvpUrl = args[0];
}
Provider jceProvider = new BouncyCastleProvider();
Security.addProvider(jceProvider);
ServerPolicyClient client = new ServerPolicyClient(jceProvider);
client.serverPolicyQuery(scvpUrl);
}
public ASN1OctetString generateNonce(int nonceSize) {
SecureRandom random = null;
byte[] nonce = null;
nonce = new byte[nonceSize];
random = new SecureRandom();
random.nextBytes(nonce);
return new DEROctetString(nonce);
}
public void serverPolicyQuery(String scvpServer) throws SCVPException {
ValPolRequest policyRequest = new ValPolRequest(generateNonce(16));
ServerPolicyRequest encapReq = new ServerPolicyRequest(policyRequest);
log.log(Level.FINE, "ValPolRequest:\n" + ASN1Dump.dumpAsString(encapReq, true));
byte[] rawReq;
try {
rawReq = encapReq.toASN1Primitive().getEncoded();
} catch (IOException e) {
throw new SCVPException("Problem with SCVP Policy Request", e);
}
this.fullRequest = rawReq;
/*
* Send the request to the SCVP service...
*/
byte[] resp = sendSCVPRequestPOST(scvpServer, rawReq);
this.fullResponse = resp;
ASN1SequenceParser cmsSeqPar = null;
ContentInfoParser contentInfoParser = null;
ASN1ObjectIdentifier contentType = null;
if (resp != null) {
ASN1StreamParser streamParser = new ASN1StreamParser(resp);
Object object;
try {
object = streamParser.readObject();
} catch (IOException e) {
throw new SCVPException("Problem parsing response from server",
e);
}
if (object instanceof ASN1SequenceParser) {
cmsSeqPar = (ASN1SequenceParser) object;
try {
contentInfoParser = new ContentInfoParser(cmsSeqPar);
} catch (IOException e) {
throw new SCVPException("Problem parsing CMS ContentInfo",
e);
}
contentType = contentInfoParser.getContentType();
if (CMSObjectIdentifiers.signedData.equals(contentType)) {
try {
object = streamParser.readObject();
} catch (IOException e) {
throw new SCVPException(
"Problem parsing response from server", e);
}
if (object instanceof ASN1SequenceParser) {
/*
* Now that we confirmed this is CMS Signed data we are
* going to start parsing what we know without checking
* (not a good long term solution)
*/
ASN1SequenceParser cmsSdPar = (ASN1SequenceParser) object;
// version CMSVersion
ASN1Integer sdv;
try {
sdv = (ASN1Integer) cmsSdPar.readObject();
} catch (IOException e) {
throw new SCVPException(
"Problem parsing CMS Version", e);
}
ASN1SetParser dASetPar;
AlgorithmIdentifier algId;
try {
dASetPar = (ASN1SetParser) cmsSdPar.readObject();
algId = AlgorithmIdentifier.getInstance(dASetPar
.readObject());
} catch (IOException e) {
throw new SCVPException(
"Problem parsing digest algorithm identifier",
e);
}
ASN1SequenceParser eCInfoPar;
ASN1ObjectIdentifier eContentType;
ASN1TaggedObjectParser eContent;
ASN1OctetString valPolResponse;
try {
eCInfoPar = (ASN1SequenceParser) cmsSdPar
.readObject();
eContentType = (ASN1ObjectIdentifier) eCInfoPar
.readObject();
eContent = (ASN1TaggedObjectParser) eCInfoPar
.readObject();
valPolResponse = (ASN1OctetString) eContent
.getObjectParser(0, true).toASN1Primitive();
} catch (IOException e) {
throw new SCVPException(
"Problem parsing EncapsulatedContentInfo",
e);
}
/*
* Digest the object bytes for signature validation
*/
byte[] vPRespBytes = valPolResponse.getOctets();
byte[] digest = null;
/*
* Only support SHA-1/SHA-256/SHA-384. Die on validation
* otherwise.
*/
if (algId.getAlgorithm().equals(CipherEngine.SHA384)) {
/*
* SHA-384
*/
digest = DigestEngine.sHA384Sum(vPRespBytes,
jceProvider.getName());
} else if (algId.getAlgorithm().equals(
CipherEngine.SHA256)) {
/*
* SHA-256
*/
digest = DigestEngine.sHA256Sum(vPRespBytes,
jceProvider.getName());
} else if (algId.getAlgorithm().equals(
CipherEngine.SHA1)) {
/*
* SHA-1
*/
digest = DigestEngine.sHA1Sum(vPRespBytes,
jceProvider.getName());
} else {
throw new SCVPException(
"Unexpected Digest Algorithm: "
+ algId.getAlgorithm().getId());
}
ASN1TaggedObjectParser certSet;
Certificate cvSigner;
try {
certSet = (ASN1TaggedObjectParser) cmsSdPar
.readObject();
cvSigner = Certificate
.getInstance(certSet.getObjectParser(0,
true).toASN1Primitive());
} catch (IOException e) {
throw new SCVPException(
"Error parsing SCVP Signer in CMS", e);
}
ASN1SetParser sInfosPar;
SignerInfo sInfo;
try {
sInfosPar = (ASN1SetParser) cmsSdPar.readObject();
sInfo = SignerInfo.getInstance(sInfosPar
.readObject().toASN1Primitive());
} catch (IOException e) {
throw new SCVPException("Error parsing SignerInfo",
e);
}
SignerIdentifier sID = sInfo.getSID();
IssuerAndSerialNumber iSn = IssuerAndSerialNumber
.getInstance(sID);
if (iSn.equals(new IssuerAndSerialNumber(cvSigner))) {
/*
* To get here the signerInfo references the
* included signer and we will proceed to parse the
* SignerInfo, which includes the digest of (and
* reference to) a CVResponse, and the encrypted
* value (signature). Parse and validate the
* signature...
*/
AlgorithmIdentifier sIAlgId = sInfo
.getDigestAlgorithm();
Attributes sIAA = Attributes.getInstance(sInfo
.getAuthenticatedAttributes());
Attribute siContentType = null;
Attribute siSigningTime = null;
Attribute siMessageDigest = null;
for (Attribute a : sIAA.getAttributes()) {
if (a.getAttrType().equals(
new ASN1ObjectIdentifier(
"1.2.840.113549.1.9.3"))) {
siContentType = a;
}
if (a.getAttrType().equals(
new ASN1ObjectIdentifier(
"1.2.840.113549.1.9.5"))) {
siSigningTime = a;
}
if (a.getAttrType().equals(
new ASN1ObjectIdentifier(
"1.2.840.113549.1.9.4"))) {
siMessageDigest = a;
}
}
/*
* Make sure the SignerInfo has all that we expect,
* and lets validate the data.
*
* -ContentType: Make sure it is an SCVP Response
* -SigningTime: We use a nonce, ensure it was
* signed within the past minute -MessageDigest:
* This must match the digest of the CVResponse
*/
if (siContentType != null && siSigningTime != null
&& siMessageDigest != null) {
ASN1ObjectIdentifier siCT = (ASN1ObjectIdentifier) siContentType
.getAttrValues().getObjectAt(0);
if (siCT.equals(new ASN1ObjectIdentifier(
"1.2.840.113549.1.9.16.1.13"))) {
} else {
throw new SCVPException(
"Unexpected Content Type: "
+ siCT.getId());
}
Calendar currentTime = Calendar.getInstance();
ASN1UTCTime claimSignTime = (ASN1UTCTime) siSigningTime
.getAttrValues().getObjectAt(0);
Calendar signingTime = new GregorianCalendar();
try {
signingTime.setTime(claimSignTime
.getAdjustedDate());
} catch (ParseException e) {
throw new SCVPException(
"Error parsing SigningTime", e);
}
Calendar minBefore = new GregorianCalendar();
Calendar minAfter = new GregorianCalendar();
minBefore.add(Calendar.MINUTE, -1);
minAfter.add(Calendar.MINUTE, 1);
if (!(currentTime.before(minBefore) || currentTime
.after(minAfter))) {
} else {
throw new SCVPException(
"Unacceptable Signing Time: "
+ claimSignTime
.getAdjustedTime());
}
ASN1OctetString claimDigestOS = (ASN1OctetString) siMessageDigest
.getAttrValues().getObjectAt(0);
byte[] claimDigest = claimDigestOS.getOctets();
if (Arrays.areEqual(digest, claimDigest)) {
} else {
throw new SCVPException(
"SignerInfo Message Digest ("
+ DataUtil
.byteArrayToString(claimDigest)
+ ") does is not equal to actual digest ("
+ DataUtil
.byteArrayToString(digest)
+ ")");
}
} else {
throw new SCVPException(
"SignerInfo does not include requred Authenticated attributes");
}
AlgorithmIdentifier sigAlg = sInfo
.getDigestEncryptionAlgorithm();
byte[] sigBits = sInfo.getEncryptedDigest()
.getOctets();
String sigAlgName = CipherEngine
.getSigningAlgorithm(
sIAlgId.getAlgorithm(),
sigAlg.getAlgorithm());
Signature signature = null;
try {
signature = Signature.getInstance(sigAlgName,
jceProvider.getName());
} catch (NoSuchAlgorithmException
| NoSuchProviderException e) {
throw new SCVPException(
"Problem verifing signature", e);
}
InputStream in;
try {
in = new ByteArrayInputStream(
cvSigner.getEncoded());
} catch (IOException e) {
throw new SCVPException(
"Error parsing SCVP Signer Certificate",
e);
}
CertificateFactory cf;
X509Certificate cvSignerCert;
try {
cf = CertificateFactory.getInstance("X.509",
jceProvider.getName());
cvSignerCert = (X509Certificate) cf
.generateCertificate(in);
signature.initVerify(cvSignerCert);
} catch (InvalidKeyException e) {
throw new SCVPException(
"Problem parsing SCVP Signer public key",
e);
} catch (CertificateException e) {
throw new SCVPException(
"Problem parsing SCVP Signing certificate",
e);
} catch (NoSuchProviderException e) {
throw new SCVPException(
"Problem with JCE Provider", e);
}
try {
signature.update(sIAA.getEncoded());
} catch (SignatureException | IOException e) {
throw new SCVPException(
"Problem with SCVP Signature validation",
e);
}
boolean sigMatch = false;
try {
sigMatch = signature.verify(sigBits);
} catch (SignatureException e) {
throw new SCVPException(
"Invalid SCVP Signature: Signature Validation Failed",
e);
}
if (sigMatch) {
/*
* TODO: Validate that we trust the SCVP Signer
* certificate:
*
* To elaborate, while this code does validate the signature
* of the SCVP response, it does not verify the signer
* certificate is one that we "trust". Further, a large
* fault-tolerant SCVP service MAY have multiple SCVP signers.
* To specify explicit trust in those signers as a command
* line option, or as inputs to this code is counter-intuitive,
* as SCVP is intended to ease the burden of managing trust lists.
*
* So for this implementation, the SCVP signing certificate MUST chain
* to one specific trust anchor. There MUST be a policy on the SCVP
* service that supports validation of all SCVP signers encountered
* to that trust anchor. It is up to the implementor how often
* the SCVP signer is validated, vs. reliance on a cached CVResponse
* of the prior validation.
*
*/
/*
* Begin parsing ValPolResponse
*/
ASN1StreamParser vpRespOs = new ASN1StreamParser(
vPRespBytes);
ASN1SequenceParser vpResp;
ASN1Integer vpResponseVersion = null;
ASN1Integer maxCVRequestVersion = null;
ASN1Integer maxVPRequestVersion = null;
ASN1Integer serverConfigurationID = null;
ASN1GeneralizedTime thisUpdate = null;
ASN1GeneralizedTime nextUpdate = null;
ASN1Sequence supportedChecks = null;
ASN1Sequence supportedWantBacks = null;
ASN1Sequence validationPolicies = null;
ASN1Sequence validationAlgs = null;
ASN1Sequence authPolicies = null;
ResponseTypes responseTypes = null;
ASN1Sequence defaultPolicyValues = null;
RevocationInfoTypes revocationInfoTypes = null;
ASN1Sequence signatureGeneration = null;
ASN1Sequence signatureVerification = null;
ASN1Sequence hashAlgorithms = null;
ASN1Sequence serverPublicKeys = null;
ASN1Integer clockSkew = null;
ASN1OctetString requestNonce = null;
try {
vpResp = (ASN1SequenceParser) vpRespOs
.readObject();
vpResponseVersion = ASN1Integer
.getInstance(vpResp.readObject());
log.log(Level.FINE, "vpResponseVersion:\n" + ASN1Dump.dumpAsString(vpResponseVersion, true));
maxCVRequestVersion = ASN1Integer
.getInstance(vpResp.readObject());
log.log(Level.FINE, "maxCVRequestVersion:\n" + ASN1Dump.dumpAsString(maxCVRequestVersion, true));
maxVPRequestVersion = ASN1Integer
.getInstance(vpResp.readObject());
log.log(Level.FINE, "maxVPRequestVersion:\n" + ASN1Dump.dumpAsString(maxVPRequestVersion, true));
serverConfigurationID = ASN1Integer
.getInstance(vpResp.readObject());
log.log(Level.FINE, "serverConfigurationID:\n" + ASN1Dump.dumpAsString(serverConfigurationID, true));
thisUpdate = ASN1GeneralizedTime
.getInstance(vpResp.readObject());
log.log(Level.FINE, "thisUpdate:\n" + ASN1Dump.dumpAsString(thisUpdate, true));
nextUpdate = ASN1GeneralizedTime
.getInstance(vpResp.readObject());
log.log(Level.FINE, "nextUpdate:\n" + ASN1Dump.dumpAsString(nextUpdate, true));
supportedChecks = ASN1Sequence
.getInstance(vpResp.readObject());
log.log(Level.FINE, "supportedChecks:\n" + ASN1Dump.dumpAsString(supportedChecks, true));
supportedWantBacks = ASN1Sequence
.getInstance(vpResp.readObject());
log.log(Level.FINE, "supportedWantBacks:\n" + ASN1Dump.dumpAsString(supportedWantBacks, true));
validationPolicies = ASN1Sequence
.getInstance(vpResp.readObject());
log.log(Level.FINE, "validationPolicies:\n" + ASN1Dump.dumpAsString(validationPolicies, true));
validationAlgs = ASN1Sequence
.getInstance(vpResp.readObject());
log.log(Level.FINE, "validationAlgs:\n" + ASN1Dump.dumpAsString(validationAlgs, true));
authPolicies = ASN1Sequence
.getInstance(vpResp.readObject());
log.log(Level.FINE, "authPolicies:\n" + ASN1Dump.dumpAsString(authPolicies, true));
responseTypes = ResponseTypes
.getInstance(vpResp.readObject());
log.log(Level.FINE, "responseTypes:\n" + ASN1Dump.dumpAsString(responseTypes, true));
defaultPolicyValues = ASN1Sequence
.getInstance(vpResp.readObject());
log.log(Level.FINE, "defaultPolicyValues:\n" + ASN1Dump.dumpAsString(defaultPolicyValues, true));
revocationInfoTypes = RevocationInfoTypes
.getInstance(vpResp.readObject());
log.log(Level.FINE, "revocationInfoTypes:\n" + ASN1Dump.dumpAsString(revocationInfoTypes, true));
signatureGeneration = ASN1Sequence
.getInstance(vpResp.readObject());
log.log(Level.FINE, "signatureGeneration:\n" + ASN1Dump.dumpAsString(signatureGeneration, true));
signatureVerification = ASN1Sequence
.getInstance(vpResp.readObject());
log.log(Level.FINE, "signatureVerification:\n" + ASN1Dump.dumpAsString(signatureVerification, true));
hashAlgorithms = ASN1Sequence
.getInstance(vpResp.readObject());
log.log(Level.FINE, "hashAlgorithms:\n" + ASN1Dump.dumpAsString(hashAlgorithms, true));
/*
* The next defined object is optional, and not
* explicitly tagged. Luckily, it is either a
* SEQUENCE (optional object) or an INTEGER
* (the following mandatory object).
*/
Object nextObj = vpResp.readObject();
if (nextObj instanceof ASN1Sequence) {
/*
* Process
*/
serverPublicKeys = ASN1Sequence
.getInstance(vpResp.readObject());
log.log(Level.FINE, "serverPublicKeys:\n" + ASN1Dump.dumpAsString(serverPublicKeys, true));
/*
* Pull the next object,
* which is mandatory.
*/
nextObj = vpResp.readObject();
}
clockSkew = ASN1Integer
.getInstance(nextObj);
log.log(Level.FINE, "clockSkew:\n" + ASN1Dump.dumpAsString(clockSkew, true));
/*
* The following (and final)
* object is also optional
*/
nextObj = vpResp.readObject();
if (null != nextObj) {
requestNonce = ASN1OctetString
.getInstance(nextObj);
log.log(Level.FINE, "requestNonce:\n" + ASN1Dump.dumpAsString(requestNonce, true));
}
/*
* Finished parsing ValPolResponse
*/
} catch (IOException e) {
throw new SCVPException(
"Error parsing CVResponse", e);
}
}
}
}
}
}
}
}
/*
* This is not my preferable path... TODO: Replace transport with Apache
* HTTP client.
*/
public static byte[] sendSCVPRequestPOST(String postURL, byte[] req)
throws SCVPException {
byte[] resp = null;
try {
URL url = new URL(postURL);
URLConnection con = url.openConnection();
con.setReadTimeout(10000);
con.setConnectTimeout(10000);
con.setAllowUserInteraction(false);
con.setUseCaches(false);
con.setDoOutput(true);
con.setDoInput(true);
con.setRequestProperty("Content-Type",
"application/scvp-vp-request");
con.setRequestProperty("Accept",
"application/scvp-vp-response");
OutputStream os = con.getOutputStream();
os.write(req);
os.close();
/*
* Lets make sure we are receiving an SCVP response...
*/
if (con.getContentType().equalsIgnoreCase(
"application/scvp-vp-response")) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] chunk = new byte[4096];
int bytesRead;
InputStream stream = con.getInputStream();
while ((bytesRead = stream.read(chunk)) > 0) {
baos.write(chunk, 0, bytesRead);
}
resp = baos.toByteArray();
} else {
throw new SCVPException(
"Response from the server is not a CMS message");
}
} catch (IOException e) {
throw new SCVPException("Problem communicating with SCVP server", e);
}
return resp;
}
/**
* @return the fullRequest
*/
public byte[] getFullRequest() {
return fullRequest;
}
/**
* @return the fullResponse
*/
public byte[] getFullResponse() {
return fullResponse;
}
}