/* ==========================================
* Laverca Project
* https://sourceforge.net/projects/laverca/
* ==========================================
* Copyright 2015 Laverca Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fi.laverca;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1Object;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Set;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.pkcs.ContentInfo;
import org.bouncycastle.asn1.pkcs.IssuerAndSerialNumber;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.pkcs.SignedData;
import org.bouncycastle.asn1.pkcs.SignerInfo;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.X509Name;
import org.bouncycastle.util.encoders.Base64;
import fi.laverca.mss.MssException;
import fi.laverca.util.X509Util;
/**
* A CMS signature wrapper.
*/
public class CmsSignature implements Signature {
private static final Log log = LogFactory.getLog(CmsSignature.class);
private SignedData _sd;
private byte[] rawSignature;
/**
*
* @param bytes In general, you get this from an MSS_SignatureResp.getSignature() call.
* @throws IllegalArgumentException if bytes is null or the amount of signer certificates found is not equal to one
*/
public CmsSignature(byte[] bytes) throws IllegalArgumentException {
if(bytes == null) {
throw new IllegalArgumentException("Can't construct a PKCS7 SignedData element from null input.");
}
this.rawSignature = bytes;
this._sd = bytesToPkcs7SignedData(bytes);
if(this._sd.getSignerInfos() == null || this._sd.getSignerInfos().size() != 1) {
throw new IllegalArgumentException("This only works with exactly one SignerInfo.");
}
}
/**
* Look up the certificate of the signer of this signature.
*
Note that this only looks up the first signer. In MSSP signatures,
* there is only one, but in a general Pkcs7 case, there can be several.
*
* @return X509 signer certificate
* @throws MssException if the amount of signer certificates found is not equal to one
*/
@Override
public X509Certificate getSignerCert() throws MssException {
List allSignerCerts = getSignerCerts(this._sd);
int certsFound = allSignerCerts.size();
if (certsFound < 1) {
throw new MssException("Signer cert not found.");
} else if (certsFound > 1) {
throw new MssException("Expected a single signer cert but found " + certsFound + ".");
}
return allSignerCerts.get(0);
}
/**
* Convenience method. Equivalent to calling getSignerCert and
* then parsing out the CN from the certificate's Subject field.
* @return Signer CN or null if there's a problem.
*/
@Override
public String getSignerCn() {
try {
X509Certificate signerCert = this.getSignerCert();
String dn = signerCert.getSubjectX500Principal().getName();
String cn = null;
try {
LdapName ldapDn = new LdapName(dn);
List rdns = ldapDn.getRdns();
for(Rdn r : rdns) {
if("CN".equals(r.getType())) {
cn = r.getValue().toString();
}
}
} catch(InvalidNameException e) {
log.warn("Invalid name", e);
}
return cn;
} catch(Throwable t) {
log.error("Failed to get signer CN: " + t.getMessage());
return null;
}
}
@Override
public byte[] getRawSignature() {
return this.rawSignature;
}
@Override
public String getBase64Signature() {
return Base64.toBase64String(this.getRawSignature());
}
/**
* Get the CMS SignatureValue as byte[]
* @return CMS SignatureValue as byte[]
*/
public byte[] getSignatureValue() {
return readSignatureValue(this.getSignerInfo());
}
/**
* Get the SignerInfo object if found
* @return SignerInfo or null
*/
public SignerInfo getSignerInfo() {
for (SignerInfo si : readSignerInfos(this._sd)) {
return si;
}
return null;
}
/**
* Get the certificates
* @return All certificates or an empty list
*/
public List getCertificates() {
return readCerts(this._sd);
}
/**
* Convert a byte array to a PKCS7 SignedData object
* @param bytes byte array
* @return PKCS7 SignedData object
*/
public static SignedData bytesToPkcs7SignedData(byte[] bytes) {
if(bytes == null) {
throw new IllegalArgumentException("null bytes");
}
ASN1InputStream ais = new ASN1InputStream(bytes);
ASN1Object asn1 = null;
try {
asn1 = ais.readObject();
} catch(IOException ioe) {
throw new IllegalArgumentException("not a pkcs7 signature");
} finally {
try {
ais.close();
} catch (IOException e) {
// Ignore
}
}
ContentInfo ci = ContentInfo.getInstance(asn1);
ASN1ObjectIdentifier typeId = ci.getContentType();
if( ! typeId.equals(PKCSObjectIdentifiers.signedData)) {
throw new IllegalArgumentException("not a pkcs7 signature");
}
return SignedData.getInstance(ci.getContent());
}
/**
* Read the certificates used to sign a PKCS7 SignedData.
*
* @param sd PKCS7 SignedData
* @return List of X509 certificates
* @throws MssException if no certificate or signer info is found from the data
*/
public static List getSignerCerts(final SignedData sd) throws MssException {
// 0. Setup.
// 1. Read PKCS7.Certificates to get all possible certs.
// 2. Read PKCS7.SignerInfo to get all signers.
// 3. Look up matching certificates.
// 4. Return the list.
// 0. Setup.
if(sd == null) {
throw new IllegalArgumentException("null input");
}
List signerCerts = new ArrayList();
// 1. Read PKCS7.Certificates to get all possible certs.
log.debug("Read all certs");
List certs = readCerts(sd);
if (certs.isEmpty()) {
throw new MssException("PKCS7 SignedData certificates not found");
}
// 2. Read PKCS7.SignerInfo to get all signers.
log.debug("Read SignerInfo");
List signerInfos = readSignerInfos(sd);
if (signerInfos.isEmpty()) {
throw new MssException("PKCS7 SignedData signerInfo not found");
}
// 3. Verify that signerInfo cert details match the cert on hand
log.debug("Matching cert and SignerInfo details");
for (SignerInfo si : signerInfos) {
for (X509Certificate c : certs) {
String siIssuer = readIssuer(si);
String siSerial = readSerial(si);
String cIssuer = c.getIssuerDN().toString();
String cSerial = c.getSerialNumber().toString();
if (dnsEqual(siIssuer, cIssuer) && siSerial.equals(cSerial)) {
signerCerts.add(c);
log.debug("Cert does match signerInfo");
log.debug("SignerInfo issuer:serial = " + siIssuer + ":" + siSerial);
log.debug("Certificates issuer:serial = " + cIssuer + ":" + cSerial);
} else {
log.debug("Cert does not match signerInfo");
log.debug("SignerInfo issuer:serial = " + siIssuer + ":" + siSerial);
log.debug("Certificates issuer:serial = " + cIssuer + ":" + cSerial);
}
}
}
// 4. Return the list.
log.debug("Returning " + signerCerts.size() + " certs");
return signerCerts;
}
/**
* Read all certificates from a SignedData
* @param sd data
* @return all X509 certificates or an empty list
*/
public static List readCerts(final SignedData sd) {
if (sd == null) {
return Collections.emptyList();
}
List certs = new ArrayList();
ASN1Set certSet = sd.getCertificates();
Enumeration> en = certSet.getObjects();
while(en.hasMoreElements()) {
Object o = en.nextElement();
try {
byte[] certDer = ((DERSequence)o).getEncoded();
X509Certificate cert = X509Util.DERtoX509Certificate(certDer);
certs.add(cert);
} catch (IOException e) {
log.debug("Failed to read cert", e);
}
}
return certs;
}
/**
* Read SignerInfo elements from a SignedData
* @param sd data
* @return SignerInfo element list or null
*/
public static List readSignerInfos(final SignedData sd) {
if (sd == null) {
return null;
}
List signerInfos = new ArrayList();
ASN1Set siSet = sd.getSignerInfos();
Enumeration> e = siSet.getObjects();
while(e.hasMoreElements()) {
Object o = e.nextElement();
try {
SignerInfo si = SignerInfo.getInstance(o);
signerInfos.add(si);
} catch (RuntimeException ex) {
log.trace("SignerInfo " + o + " not found");
}
}
return signerInfos;
}
/**
* Read the Serial element from a SignedInfo object
* @param si data
* @return Serial as String
*/
public static String readSerial(final SignerInfo si) {
if (si == null) {
return null;
}
final IssuerAndSerialNumber ias = si.getIssuerAndSerialNumber();
final ASN1Integer serialDER = ias.getCertificateSerialNumber();
return serialDER.getPositiveValue().toString();
}
/**
* Read the Issuer from a SignedInfo object
* @param si data
* @return Issuer as String
*/
public static String readIssuer(final SignerInfo si) {
if (si == null) {
return null;
}
final IssuerAndSerialNumber ias = si.getIssuerAndSerialNumber();
final X500Name issuerName = ias.getName();
return issuerName.toString();
}
/**
* Read the SignatureValue from a SignedInfo
* @param si data
* @return SignatureValue as byte[]
*/
public static byte[] readSignatureValue(final SignerInfo si) {
if (si == null) {
return null;
}
return si.getEncryptedDigest().getOctets();
}
/**
* Return true if two Distinguished Names are equal, ignoring
* delimiters and order of elements.
*
* @param dn1 First Distinguished name
* @param dn2 Second Distinguished name
* @return true if DNs are equal, false otherwise
*/
@SuppressWarnings("deprecation")
public static boolean dnsEqual(String dn1, String dn2) {
if (dn1 == null || dn2 == null) {
return false;
}
X509Name n1 = new X509Name(dn1);
X509Name n2 = new X509Name(dn2);
return n1.equals(n2, false);
}
}