/*
* Commons eID Project.
* Copyright (C) 2008-2013 FedICT.
* Copyright (C) 2009 e-Contract.be BVBA.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License version
* 3.0 as published by the Free Software Foundation.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, see
* http://www.gnu.org/licenses/.
*/
package be.fedict.commons.eid.consumer;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
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.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.x509.DigestInfo;
import be.fedict.commons.eid.consumer.tlv.TlvParser;
/**
* Utility class for various eID related integrity checks.
*
* @author Frank Cornelis
*
*/
public class BeIDIntegrity {
private final static Log LOG = LogFactory.getLog(BeIDIntegrity.class);
private final CertificateFactory certificateFactory;
/**
* Default constructor.
*/
public BeIDIntegrity() {
try {
this.certificateFactory = CertificateFactory.getInstance("X.509");
} catch (final CertificateException cex) {
throw new RuntimeException("X.509 algo", cex);
}
}
/**
* Loads a DER-encoded X509 certificate from a byte array.
*
* @param encodedCertificate
* @return
*/
public X509Certificate loadCertificate(final byte[] encodedCertificate) {
X509Certificate certificate;
try {
certificate = (X509Certificate) this.certificateFactory
.generateCertificate(new ByteArrayInputStream(
encodedCertificate));
} catch (final CertificateException cex) {
throw new RuntimeException("X509 decoding error: "
+ cex.getMessage(), cex);
}
return certificate;
}
/**
* Gives back a parsed identity file after integrity verification.
*
* @param identityFile
* @param identitySignatureFile
* @param rrnCertificate
* @return
*/
public Identity getVerifiedIdentity(final byte[] identityFile,
final byte[] identitySignatureFile,
final X509Certificate rrnCertificate) {
final Identity identity = this.getVerifiedIdentity(identityFile,
identitySignatureFile, null, rrnCertificate);
return identity;
}
/**
* Gives back a parsed identity file after integrity verification including
* the eID photo.
*
* @param identityFile
* @param identitySignatureFile
* @param photo
* @param rrnCertificate
* @return
*/
public Identity getVerifiedIdentity(final byte[] identityFile,
final byte[] identitySignatureFile, final byte[] photo,
final X509Certificate rrnCertificate) {
final PublicKey publicKey = rrnCertificate.getPublicKey();
boolean result;
try {
result = verifySignature(rrnCertificate.getSigAlgName(),
identitySignatureFile, publicKey, identityFile);
} catch (final Exception ex) {
throw new SecurityException(
"identity signature verification error: " + ex.getMessage(),
ex);
}
if (false == result) {
throw new SecurityException("signature integrity error");
}
final Identity identity = TlvParser.parse(identityFile, Identity.class);
if (null != photo) {
final byte[] expectedPhotoDigest = identity.getPhotoDigest();
final byte[] actualPhotoDigest = digest(
getDigestAlgo(expectedPhotoDigest.length), photo);
if (false == Arrays.equals(expectedPhotoDigest, actualPhotoDigest)) {
throw new SecurityException("photo digest mismatch");
}
}
return identity;
}
/**
* Gives back a parsed address file after integrity verification.
*
* @param addressFile
* @param identitySignatureFile
* @param addressSignatureFile
* @param rrnCertificate
* @return
*/
public Address getVerifiedAddress(final byte[] addressFile,
final byte[] identitySignatureFile,
final byte[] addressSignatureFile,
final X509Certificate rrnCertificate) {
final byte[] trimmedAddressFile = trimRight(addressFile);
final PublicKey publicKey = rrnCertificate.getPublicKey();
boolean result;
try {
result = verifySignature(rrnCertificate.getSigAlgName(),
addressSignatureFile, publicKey, trimmedAddressFile,
identitySignatureFile);
} catch (final Exception ex) {
throw new SecurityException(
"address signature verification error: " + ex.getMessage(),
ex);
}
if (false == result) {
throw new SecurityException("address integrity error");
}
final Address address = TlvParser.parse(addressFile, Address.class);
return address;
}
/**
* Verifies a SHA1withRSA signature.
*
* @param signatureData
* @param publicKey
* @param data
* @return
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
* @throws SignatureException
*/
public boolean verifySignature(final byte[] signatureData,
final PublicKey publicKey, final byte[]... data)
throws InvalidKeyException, NoSuchAlgorithmException,
SignatureException {
return this.verifySignature("SHA1withRSA", signatureData, publicKey,
data);
}
/**
* Verifies a signature.
*
* @param signatureAlgo
* @param signatureData
* @param publicKey
* @param data
* @return
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
* @throws SignatureException
*/
public boolean verifySignature(final String signatureAlgo,
final byte[] signatureData, final PublicKey publicKey,
final byte[]... data) throws NoSuchAlgorithmException,
InvalidKeyException, SignatureException {
Signature signature;
signature = Signature.getInstance(signatureAlgo);
signature.initVerify(publicKey);
for (byte[] dataItem : data) {
signature.update(dataItem);
}
final boolean result = signature.verify(signatureData);
return result;
}
private byte[] digest(final String algoName, final byte[] data) {
MessageDigest messageDigest;
try {
messageDigest = MessageDigest.getInstance(algoName);
} catch (final NoSuchAlgorithmException nsaex) {
throw new RuntimeException(algoName);
}
final byte[] digestValue = messageDigest.digest(data);
return digestValue;
}
private byte[] trimRight(final byte[] addressFile) {
int idx;
for (idx = 0; idx < addressFile.length; idx++) {
if (0 == addressFile[idx]) {
break;
}
}
final byte[] result = new byte[idx];
System.arraycopy(addressFile, 0, result, 0, idx);
return result;
}
/**
* Verifies an authentication signature.
*
* @param toBeSigned
* @param signatureValue
* @param authnCertificate
* @return
*/
public boolean verifyAuthnSignature(final byte[] toBeSigned,
final byte[] signatureValue, final X509Certificate authnCertificate) {
final PublicKey publicKey = authnCertificate.getPublicKey();
boolean result;
try {
result = this
.verifySignature(signatureValue, publicKey, toBeSigned);
} catch (final InvalidKeyException ikex) {
LOG.warn("invalid key: " + ikex.getMessage(), ikex);
return false;
} catch (final NoSuchAlgorithmException nsaex) {
LOG.warn("no such algo: " + nsaex.getMessage(), nsaex);
return false;
} catch (final SignatureException sigex) {
LOG.warn("signature error: " + sigex.getMessage(), sigex);
return false;
}
return result;
}
/**
* Verifies a non-repudiation signature.
*
* @param expectedDigestValue
* @param signatureValue
* @param certificate
* @return
*/
public boolean verifyNonRepSignature(final byte[] expectedDigestValue,
final byte[] signatureValue, final X509Certificate certificate) {
try {
return __verifyNonRepSignature(expectedDigestValue, signatureValue,
certificate);
} catch (final InvalidKeyException ikex) {
LOG.warn("invalid key: " + ikex.getMessage(), ikex);
return false;
} catch (final NoSuchAlgorithmException nsaex) {
LOG.warn("no such algo: " + nsaex.getMessage(), nsaex);
return false;
} catch (final NoSuchPaddingException nspex) {
LOG.warn("no such padding: " + nspex.getMessage(), nspex);
return false;
} catch (final BadPaddingException bpex) {
LOG.warn("bad padding: " + bpex.getMessage(), bpex);
return false;
} catch (final IOException ioex) {
LOG.warn("IO error: " + ioex.getMessage(), ioex);
return false;
} catch (final IllegalBlockSizeException ibex) {
LOG.warn("illegal block size: " + ibex.getMessage(), ibex);
return false;
}
}
private boolean __verifyNonRepSignature(final byte[] expectedDigestValue,
final byte[] signatureValue, final X509Certificate certificate)
throws NoSuchAlgorithmException, NoSuchPaddingException,
InvalidKeyException, IllegalBlockSizeException,
BadPaddingException, IOException {
final PublicKey publicKey = certificate.getPublicKey();
final Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, publicKey);
final byte[] actualSignatureDigestInfoValue = cipher
.doFinal(signatureValue);
final ASN1InputStream asnInputStream = new ASN1InputStream(
actualSignatureDigestInfoValue);
final DigestInfo actualSignatureDigestInfo = new DigestInfo(
(ASN1Sequence) asnInputStream.readObject());
asnInputStream.close();
final byte[] actualDigestValue = actualSignatureDigestInfo.getDigest();
return Arrays.equals(expectedDigestValue, actualDigestValue);
}
private String getDigestAlgo(final int hashSize) throws SecurityException {
switch (hashSize) {
case 20 :
return "SHA-1";
case 28 :
return "SHA-224";
case 32 :
return "SHA-256";
case 48 :
return "SHA-384";
case 64 :
return "SHA-512";
}
throw new SecurityException(
"Failed to find guess algorithm for hash size of " + hashSize
+ " bytes");
}
}