/* * SignedDoc.java * PROJECT: JDigiDoc * DESCRIPTION: Digi Doc functions for creating * and reading signed documents. * AUTHOR: Veiko Sinivee, S|E|B IT Partner Estonia *================================================== * Copyright (C) AS Sertifitseerimiskeskus * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * This library 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. * GNU Lesser General Public Licence is available at * http://www.gnu.org/copyleft/lesser.html *================================================== */ package es.uji.security.crypto.openxades.digidoc; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Serializable; import java.net.URL; import java.security.KeyStore; import java.security.MessageDigest; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.ArrayList; import javax.crypto.Cipher; import org.apache.log4j.Logger; import es.uji.security.crypto.config.ConfigManager; /** * Represents an instance of signed doc in DIGIDOC format. Contains one or more DataFile -s and zero * or more Signature -s. * * @author Veiko Sinivee * @version 1.0 */ public class SignedDoc implements Serializable { /** digidoc format */ private String m_format; /** format version */ private String m_version; /** DataFile objects */ private ArrayList m_dataFiles; /** Signature objects */ private ArrayList m_signatures; /** the only supported formats are SK-XML and DIGIDOC-XML */ public static final String FORMAT_SK_XML = "SK-XML"; public static final String FORMAT_DIGIDOC_XML = "DIGIDOC-XML"; /** supported versions are 1.0 and 1.1 */ public static final String VERSION_1_0 = "1.0"; public static final String VERSION_1_1 = "1.1"; public static final String VERSION_1_2 = "1.2"; public static final String VERSION_1_3 = "1.3"; public static final String VERSION_1_4 = "1.4"; /** the only supported algorithm is SHA1 */ public static final String SHA1_DIGEST_ALGORITHM = "http://www.w3.org/2000/09/xmldsig#sha1"; /** SHA1 digest data is allways 20 bytes */ public static final int SHA1_DIGEST_LENGTH = 20; /** the only supported canonicalization method is 20010315 */ public static final String CANONICALIZATION_METHOD_20010315 = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"; /** the only supported signature method is RSA-SHA1 */ public static final String RSA_SHA1_SIGNATURE_METHOD = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; /** the only supported transform is digidoc detatched transform */ public static final String DIGIDOC_DETATCHED_TRANSFORM = "http://www.sk.ee/2002/10/digidoc#detatched-document-signature"; /** XML-DSIG namespace */ public static String xmlns_xmldsig = "http://www.w3.org/2000/09/xmldsig#"; /** ETSI namespace */ public static String xmlns_etsi = "http://uri.etsi.org/01903/v1.1.1#"; /** DigiDoc namespace */ public static String xmlns_digidoc = "http://www.sk.ee/DigiDoc/v1.3.0#"; /** program & library name */ public static final String LIB_NAME = "JDigiDoc"; /** program & library version */ public static final String LIB_VERSION = "2.3.7"; private static Logger log = Logger.getLogger(SignedDoc.class); private static ConfigManager conf = ConfigManager.getInstance(); /** * Creates new SignedDoc Initializes everything to null */ public SignedDoc() { m_format = null; m_version = null; m_dataFiles = null; m_signatures = null; } /** * Creates new SignedDoc * * @param format * file format name * @param version * file version number * @throws DigiDocException * for validation errors */ public SignedDoc(String format, String version) throws DigiDocException { setFormat(format); setVersion(version); m_dataFiles = null; m_signatures = null; } /** * Accessor for format attribute * * @return value of format attribute */ public String getFormat() { return m_format; } /** * Mutator for format attribute * * @param str * new value for format attribute * @throws DigiDocException * for validation errors */ public void setFormat(String str) throws DigiDocException { DigiDocException ex = validateFormat(str); if (ex != null) throw ex; m_format = str; } /** * Helper method to validate a format * * @param str * input data * @return exception or null for ok */ private DigiDocException validateFormat(String str) { DigiDocException ex = null; if (str == null || (!str.equals(FORMAT_SK_XML) && !str.equals(FORMAT_DIGIDOC_XML)) || (str.equals(FORMAT_SK_XML) && m_version != null && !m_version .equals(VERSION_1_0)) || (str.equals(FORMAT_DIGIDOC_XML) && m_version != null && !m_version.equals(VERSION_1_1) && !m_version.equals(VERSION_1_2) && !m_version .equals(VERSION_1_3))) ex = new DigiDocException(DigiDocException.ERR_DIGIDOC_FORMAT, "Currently supports only SK-XML and DIGIDOC-XML formats", null); return ex; } /** * Accessor for version attribute * * @return value of version attribute */ public String getVersion() { return m_version; } /** * Mutator for version attribute * * @param str * new value for version attribute * @throws DigiDocException * for validation errors */ public void setVersion(String str) throws DigiDocException { DigiDocException ex = validateVersion(str); if (ex != null) throw ex; m_version = str; } /** * Helper method to validate a version * * @param str * input data * @return exception or null for ok */ private DigiDocException validateVersion(String str) { DigiDocException ex = null; if (str == null || (!str.equals(VERSION_1_0) && !str.equals(VERSION_1_1) && !str.equals(VERSION_1_2) && !str.equals(VERSION_1_3) && !str .equals(VERSION_1_4)) || (str.equals(VERSION_1_0) && m_format != null && !m_format.equals(FORMAT_SK_XML)) || ((str.equals(VERSION_1_1) || str.equals(VERSION_1_2) || str.equals(VERSION_1_3) || str .equals(VERSION_1_4)) && m_format != null && !m_format.equals(FORMAT_DIGIDOC_XML))) ex = new DigiDocException(DigiDocException.ERR_DIGIDOC_VERSION, "Currently supports only versions 1.0, 1.1, 1.2, 1.3 and 1.4", null); return ex; } /** * return the count of DataFile objects * * @return count of DataFile objects */ public int countDataFiles() { return ((m_dataFiles == null) ? 0 : m_dataFiles.size()); } /** * return a new available DataFile id * * @retusn new DataFile id */ public String getNewDataFileId() { int nDf = 0; String id = "D" + nDf; boolean bExists = false; do { bExists = false; for (int d = 0; d < countDataFiles(); d++) { DataFile df = getDataFile(d); if (df.getId().equals(id)) { nDf++; id = "D" + nDf; bExists = true; continue; } } } while (bExists); return id; } /** * Adds a new DataFile to signed doc * * @param inputFile * input file name * @param mime * files mime type * @param contentType * DataFile's content type * @return new DataFile object */ public DataFile addDataFile(File inputFile, String mime, String contentType) throws DigiDocException { DataFile df = new DataFile(getNewDataFileId(), contentType, inputFile.getAbsolutePath(), mime, this); addDataFile(df); return df; } /** * Writes the SignedDoc to an output file and automatically calculates DataFile sizes and * digests * * @param outputFile * output file name * @throws DigiDocException * for all errors */ public void writeToFile(File outputFile) throws DigiDocException { // TODO read DataFile elements from old file try { // System.out.println("Write to file: " + outputFile.getAbsoluteFile()); FileOutputStream fos = new FileOutputStream(outputFile); writeToStream(fos); fos.close(); // System.out.println("Write complete!"); } catch (DigiDocException ex) { throw ex; // already handled } catch (Exception ex) { DigiDocException.handleException(ex, DigiDocException.ERR_READ_FILE); } } /** * Writes the SignedDoc to an output file and automatically calculates DataFile sizes and * digests * * @param outputFile * output file name * @throws DigiDocException * for all errors */ public void writeToStream(OutputStream os) throws DigiDocException { // TODO read DataFile elements from old file try { os.write(xmlHeader().getBytes()); for (int i = 0; i < countDataFiles(); i++) { DataFile df = getDataFile(i); df.writeToFile(os); os.write("\n".getBytes()); } for (int i = 0; i < countSignatures(); i++) { Signature sig = getSignature(i); os.write(sig.toXML()); os.write("\n".getBytes()); } os.write(xmlTrailer().getBytes()); } catch (DigiDocException ex) { throw ex; // already handled } catch (Exception ex) { DigiDocException.handleException(ex, DigiDocException.ERR_WRITE_FILE); } } /** * Adds a new DataFile object * * @param attr * DataFile object to add */ public void addDataFile(DataFile df) throws DigiDocException { if (countSignatures() > 0) throw new DigiDocException(DigiDocException.ERR_SIGATURES_EXIST, "Cannot add DataFiles when signatures exist!", null); if (m_dataFiles == null) m_dataFiles = new ArrayList(); if (df.getId() == null) df.setId(getNewDataFileId()); m_dataFiles.add(df); } /** * return the desired DataFile object * * @param idx * index of the DataFile object * @return desired DataFile object */ public DataFile getDataFile(int idx) { return (DataFile) m_dataFiles.get(idx); } /** * return the latest DataFile object * * @return desired DataFile object */ public DataFile getLastDataFile() { return (DataFile) m_dataFiles.get(m_dataFiles.size() - 1); } /** * Removes the datafile with the given index * * @param idx * index of the data file */ public void removeDataFile(int idx) throws DigiDocException { if (countSignatures() > 0) throw new DigiDocException(DigiDocException.ERR_SIGATURES_EXIST, "Cannot remove DataFiles when signatures exist!", null); m_dataFiles.remove(idx); } /** * return the count of Signature objects * * @return count of Signature objects */ public int countSignatures() { return ((m_signatures == null) ? 0 : m_signatures.size()); } /** * return a new available Signature id * * @return new Signature id */ public String getNewSignatureId() { int nS = 0; String id = "S" + nS; boolean bExists = false; do { bExists = false; for (int i = 0; i < countSignatures(); i++) { Signature sig = getSignature(i); if (sig.getId().equals(id)) { nS++; id = "S" + nS; bExists = true; continue; } } } while (bExists); return id; } /** * Adds a new uncomplete signature to signed doc * * @param cert * signers certificate * @param claimedRoles * signers claimed roles * @param adr * signers address * @return new Signature object */ public Signature prepareSignature(X509Certificate cert, String[] claimedRoles, SignatureProductionPlace adr) throws DigiDocException { Signature sig = new Signature(this); sig.setId(getNewSignatureId()); // create SignedInfo block SignedInfo si = new SignedInfo(sig, RSA_SHA1_SIGNATURE_METHOD, CANONICALIZATION_METHOD_20010315); // add DataFile references for (int i = 0; i < countDataFiles(); i++) { DataFile df = getDataFile(i); Reference ref = new Reference(si, df); si.addReference(ref); } // create key info KeyInfo ki = new KeyInfo(cert); sig.setKeyInfo(ki); ki.setSignature(sig); CertValue cval = new CertValue(); cval.setType(CertValue.CERTVAL_TYPE_SIGNER); cval.setCert(cert); sig.addCertValue(cval); CertID cid = new CertID(sig, cert, CertID.CERTID_TYPE_SIGNER); sig.addCertID(cid); // create signed properties SignedProperties sp = new SignedProperties(sig, cert, claimedRoles, adr); Reference ref = new Reference(si, sp); si.addReference(ref); sig.setSignedInfo(si); sig.setSignedProperties(sp); addSignature(sig); return sig; } /** * Adds a new Signature object * * @param attr * Signature object to add */ public void addSignature(Signature sig) { if (m_signatures == null) m_signatures = new ArrayList(); m_signatures.add(sig); } /** * return the desired Signature object * * @param idx * index of the Signature object * @return desired Signature object */ public Signature getSignature(int idx) { return (Signature) m_signatures.get(idx); } /** * Removes the desired Signature object * * @param idx * index of the Signature object */ public void removeSignature(int idx) { m_signatures.remove(idx); } /** * return the latest Signature object * * @return desired Signature object */ public Signature getLastSignature() { return (Signature) m_signatures.get(m_signatures.size() - 1); } /** * Deletes last signature */ public void removeLastSiganture() { if (m_signatures.size() > 0) m_signatures.remove(m_signatures.size() - 1); } /** * Helper method to validate the whole SignedDoc object * * @param bStrong * flag that specifies if Id atribute value is to be rigorously checked (according to * digidoc format) or only as required by XML-DSIG * @return a possibly empty list of DigiDocException objects */ public ArrayList validate(boolean bStrong) { ArrayList errs = new ArrayList(); DigiDocException ex = validateFormat(m_format); if (ex != null) errs.add(ex); ex = validateVersion(m_version); if (ex != null) errs.add(ex); for (int i = 0; i < countDataFiles(); i++) { DataFile df = getDataFile(i); ArrayList e = df.validate(bStrong); if (!e.isEmpty()) errs.addAll(e); } for (int i = 0; i < countSignatures(); i++) { Signature sig = getSignature(i); ArrayList e = sig.validate(); if (!e.isEmpty()) errs.addAll(e); } return errs; } /** * Helper method to verify the whole SignedDoc object. Use this method to verify all signatures * * @param checkDate * Date on which to check the signature validity * @param demandConfirmation * true if you demand OCSP confirmation from every signature * @return a possibly empty list of DigiDocException objects */ public ArrayList verify(boolean checkDate, boolean demandConfirmation) { ArrayList errs = validate(false); for (int i = 0; i < countSignatures(); i++) { Signature sig = getSignature(i); ArrayList e = sig.verify(this, checkDate, demandConfirmation); if (!e.isEmpty()) errs.addAll(e); } if (countSignatures() == 0) { errs.add(new DigiDocException(DigiDocException.ERR_NOT_SIGNED, "This document is not signed!", null)); } return errs; } /** * Helper method to verify the whole SignedDoc object. Use this method to verify all signatures * * @param checkDate * Date on which to check the signature validity * @param bUseOcsp * true if you demand OCSP confirmation from every signature. False if you want to * check against CRL. * @return a possibly empty list of DigiDocException objects */ public ArrayList verifyOcspOrCrl(boolean checkDate, boolean bUseOcsp) { ArrayList errs = validate(false); for (int i = 0; i < countSignatures(); i++) { Signature sig = getSignature(i); ArrayList e = sig.verifyOcspOrCrl(this, checkDate, bUseOcsp); if (!e.isEmpty()) errs.addAll(e); } if (countSignatures() == 0) { errs.add(new DigiDocException(DigiDocException.ERR_NOT_SIGNED, "This document is not signed!", null)); } return errs; } /** * Helper method to create the xml header * * @return xml header */ private String xmlHeader() { StringBuffer sb = new StringBuffer("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); sb.append("<SignedDoc format=\""); sb.append(m_format); sb.append("\" version=\""); sb.append(m_version); sb.append("\""); // namespace if (m_version.equals(VERSION_1_3)) { sb.append(" xmlns=\""); sb.append(xmlns_digidoc); sb.append("\""); } sb.append(">\n"); return sb.toString(); } /** * Helper method to create the xml trailer * * @return xml trailer */ private String xmlTrailer() { return "\n</SignedDoc>"; } /** * Converts the SignedDoc to XML form * * @return XML representation of SignedDoc */ public String toXML() throws DigiDocException { // System.out.println("TO-XML:"); StringBuffer sb = new StringBuffer(xmlHeader()); // System.out.println("DFS: " + countDataFiles()); for (int i = 0; i < countDataFiles(); i++) { DataFile df = getDataFile(i); String str = df.toString(); // System.out.println("DF String: " + df.toString() + "DF: " + df.getId() + " size: " + // str.length()); sb.append(str); sb.append("\n"); } // System.out.println("SIGS: " + countSignatures()); for (int i = 0; i < countSignatures(); i++) { Signature sig = getSignature(i); String str = sig.toString(); // System.out.println("SIG: " + sig.getId() + " size: " + str.length()); sb.append(str); sb.append("\n"); } sb.append(xmlTrailer()); // System.out.println("Doc size: " + sb.toString().length()); return sb.toString(); } /** * return the stringified form of SignedDoc * * @return SignedDoc string representation */ public String toString() { String str = null; try { str = toXML(); } catch (Exception ex) { } return str; } /** * Computes an SHA1 digest * * @param data * input data * @return SHA1 digest */ public static byte[] digest(byte[] data) throws DigiDocException { byte[] dig = null; try { MessageDigest sha = MessageDigest.getInstance("SHA-1"); sha.update(data); dig = sha.digest(); } catch (Exception ex) { DigiDocException.handleException(ex, DigiDocException.ERR_CALCULATE_DIGEST); } return dig; } /** * Verifies the siganture * * @param digest * input data digest * @param signature * signature value * @param cert * certificate to be used on verify * @return true if signature verifies */ public static boolean verify(byte[] digest, byte[] signature, X509Certificate cert) throws DigiDocException { boolean rc = false; try { // VS - for some reason this JDK internal method sometimes failes // System.out.println("Verify digest: " + bin2hex(digest) + // " signature: " + Base64Util.encode(signature, 0)); /* * // check keystore... java.security.Signature sig = * java.security.Signature.getInstance("SHA1withRSA"); * sig.initVerify((java.security.interfaces.RSAPublicKey)cert.getPublicKey()); * sig.update(digest); rc = sig.verify(signature); */ Cipher cryptoEngine = Cipher.getInstance(conf.getProperty( "DIGIDOC_VERIFY_ALGORITHM"), "BC"); cryptoEngine.init(Cipher.DECRYPT_MODE, cert); byte[] decryptedDigestValue = cryptoEngine.doFinal(signature); byte[] cdigest = new byte[digest.length]; System.arraycopy(decryptedDigestValue, decryptedDigestValue.length - digest.length, cdigest, 0, digest.length); // System.out.println("Decrypted digest: \'" + bin2hex(cdigest) + "\'"); // now compare the digests rc = compareDigests(digest, cdigest); // System.out.println("Result: " + rc); if (!rc) throw new DigiDocException(DigiDocException.ERR_VERIFY, "Invalid signature value!", null); } catch (DigiDocException ex) { throw ex; // pass it on, but check other exceptions } catch (Exception ex) { // System.out.println("Exception: " + ex); DigiDocException.handleException(ex, DigiDocException.ERR_VERIFY); } return rc; } /** * return certificate owners first name * * @return certificate owners first name or null */ public static String getSubjectFirstName(X509Certificate cert) { String name = null; String dn = cert.getSubjectDN().getName(); int idx1 = dn.indexOf("CN="); if (idx1 != -1) { while (idx1 < dn.length() && dn.charAt(idx1) != ',') idx1++; idx1++; int idx2 = idx1; while (idx2 < dn.length() && dn.charAt(idx2) != ',' && dn.charAt(idx2) != '/') idx2++; name = dn.substring(idx1, idx2); } return name; } /** * return certificate owners last name * * @return certificate owners last name or null */ public static String getSubjectLastName(X509Certificate cert) { String name = null; String dn = cert.getSubjectDN().getName(); int idx1 = dn.indexOf("CN="); if (idx1 != -1) { idx1 += 2; while (idx1 < dn.length() && !Character.isLetter(dn.charAt(idx1))) idx1++; int idx2 = idx1; while (idx2 < dn.length() && dn.charAt(idx2) != ',' && dn.charAt(idx2) != '/') idx2++; name = dn.substring(idx1, idx2); } return name; } /** * return certificate owners personal code * * @return certificate owners personal code or null */ public static String getSubjectPersonalCode(X509Certificate cert) { String code = null; String dn = cert.getSubjectDN().getName(); int idx1 = dn.indexOf("CN="); // System.out.println("DN: " + dn); if (idx1 != -1) { while (idx1 < dn.length() && !Character.isDigit(dn.charAt(idx1))) idx1++; int idx2 = idx1; while (idx2 < dn.length() && Character.isDigit(dn.charAt(idx2))) idx2++; code = dn.substring(idx1, idx2); } // System.out.println("Code: " + code); return code; } /** * return CN part of DN * * @return CN part of DN or null */ public static String getCommonName(String dn) { String name = null; if (dn != null) { int idx1 = dn.indexOf("CN="); if (idx1 != -1) { idx1 += 2; while (idx1 < dn.length() && !Character.isLetter(dn.charAt(idx1))) idx1++; int idx2 = idx1; while (idx2 < dn.length() && dn.charAt(idx2) != ',' && dn.charAt(idx2) != '/') idx2++; name = dn.substring(idx1, idx2); } } return name; } /** * Reads X509 certificate from a data stream * * @param data * input data in Base64 form * @return X509Certificate object * @throws EFormException * for all errors */ public static X509Certificate readCertificate(byte[] data) throws DigiDocException { X509Certificate cert = null; try { // ByteArrayInputStream certStream = new ByteArrayInputStream(Base64Util.decode(data)); ByteArrayInputStream certStream = new ByteArrayInputStream(data); CertificateFactory cf = CertificateFactory.getInstance("X.509"); cert = (X509Certificate) cf.generateCertificate(certStream); certStream.close(); } catch (Exception ex) { DigiDocException.handleException(ex, DigiDocException.ERR_READ_CERT); } return cert; } /** * Reads in data file * * @param inFile * input file */ public static byte[] readFile(File inFile) throws IOException, FileNotFoundException { byte[] data = null; FileInputStream is = new FileInputStream(inFile); DataInputStream dis = new DataInputStream(is); data = new byte[dis.available()]; dis.readFully(data); dis.close(); is.close(); return data; } /** * Reads the cert from a file * * @param certFile * certificates file name * @return certificate object */ public static X509Certificate readCertificate(File certFile) throws DigiDocException { X509Certificate cert = null; try { FileInputStream fis = new FileInputStream(certFile); CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); cert = (X509Certificate) certificateFactory.generateCertificate(fis); fis.close(); // byte[] data = readFile(certFile); // cert = readCertificate(data); } catch (Exception ex) { DigiDocException.handleException(ex, DigiDocException.ERR_READ_FILE); } return cert; } /** * Helper method for comparing digest values * * @param dig1 * first digest value * @param dig2 * second digest value * @return true if they are equal */ public static boolean compareDigests(byte[] dig1, byte[] dig2) { boolean ok = (dig1 != null) && (dig2 != null) && (dig1.length == dig2.length); for (int i = 0; ok && (i < dig1.length); i++) if (dig1[i] != dig2[i]) ok = false; return ok; } /** * Converts a hex string to byte array * * @param hexString * input data * @return byte array */ public static byte[] hex2bin(String hexString) { // System.out.println("hex2bin: " + hexString); ByteArrayOutputStream bos = new ByteArrayOutputStream(); try { for (int i = 0; (hexString != null) && (i < hexString.length()); i += 2) { String tmp = hexString.substring(i, i + 2); // System.out.println("tmp: " + tmp); Integer x = new Integer(Integer.parseInt(tmp, 16)); // System.out.println("x: " + x); bos.write(x.byteValue()); } } catch (Exception ex) { System.err.println("Error converting hex string: " + ex); } return bos.toByteArray(); } /** * Converts a byte array to hex string * * @param arr * byte array input data * @return hex string */ public static String bin2hex(byte[] arr) { StringBuffer sb = new StringBuffer(); for (int i = 0; i < arr.length; i++) { String str = Integer.toHexString((int) arr[i]); if (str.length() == 2) sb.append(str); if (str.length() < 2) { sb.append("0"); sb.append(str); } if (str.length() > 2) sb.append(str.substring(str.length() - 2)); } return sb.toString(); } }