package org.basex.query.util.crypto;
import static org.basex.query.util.Err.*;
import static org.basex.util.Token.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Vector;
import javax.xml.crypto.MarshalException;
import javax.xml.crypto.XMLStructure;
import javax.xml.crypto.dom.DOMStructure;
import javax.xml.crypto.dsig.CanonicalizationMethod;
import javax.xml.crypto.dsig.DigestMethod;
import javax.xml.crypto.dsig.Reference;
import javax.xml.crypto.dsig.SignatureMethod;
import javax.xml.crypto.dsig.SignedInfo;
import javax.xml.crypto.dsig.Transform;
import javax.xml.crypto.dsig.XMLObject;
import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.crypto.dsig.XMLSignatureException;
import javax.xml.crypto.dsig.XMLSignatureFactory;
import javax.xml.crypto.dsig.dom.DOMSignContext;
import javax.xml.crypto.dsig.dom.DOMValidateContext;
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;
import javax.xml.crypto.dsig.keyinfo.KeyValue;
import javax.xml.crypto.dsig.keyinfo.X509Data;
import javax.xml.crypto.dsig.keyinfo.X509IssuerSerial;
import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec;
import javax.xml.crypto.dsig.spec.TransformParameterSpec;
import javax.xml.crypto.dsig.spec.XPathFilterParameterSpec;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.basex.build.MemBuilder;
import org.basex.build.Parser;
import org.basex.core.Prop;
import org.basex.data.Data;
import org.basex.io.IO;
import org.basex.io.serial.Serializer;
import org.basex.io.serial.SerializerProp;
import org.basex.query.QueryException;
import org.basex.query.item.ANode;
import org.basex.query.item.Bln;
import org.basex.query.item.DBNode;
import org.basex.query.item.Item;
import org.basex.util.InputInfo;
import org.basex.util.hash.TokenMap;
import org.basex.util.hash.TokenSet;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* This class generates and validates digital signatures for XML data.
*
* @author BaseX Team 2005-12, BSD License
* @author Lukas Kircher
*/
public final class DigitalSignature {
/** Canonicalization algorithms. */
private static final TokenMap CANONICALIZATIONS = new TokenMap();
/** Signature digest algorithms. */
private static final TokenMap DIGESTS = new TokenMap();
/** Signature algorithms. */
private static final TokenMap SIGNATURES = new TokenMap();
/** Signature types. */
private static final TokenSet TYPES = new TokenSet();
/** Default canonicalization algorithm. */
private static final byte[] DEFC = token("inclusive-with-comments");
/** Default digest algorithm. */
private static final byte[] DEFD = token("sha1");
/** Default signature algorithm. */
private static final byte[] DEFS = token("rsa_sha1");
/** Default signature type enveloped. */
private static final byte[] DEFT = token("enveloped");
/** Signature type enveloping. */
private static final byte[] ENVT = token("enveloping");
// /** Signature type detached. */
// private static final byte[] DETT = token("detached");
// initializations
static {
CANONICALIZATIONS.add(DEFC, token(
CanonicalizationMethod.INCLUSIVE_WITH_COMMENTS));
CANONICALIZATIONS.add(token("exclusive-with-comments"), token(
CanonicalizationMethod.EXCLUSIVE_WITH_COMMENTS));
CANONICALIZATIONS.add(token("inclusive"), token(
CanonicalizationMethod.INCLUSIVE));
CANONICALIZATIONS.add(token("exclusive"), token(
CanonicalizationMethod.EXCLUSIVE));
DIGESTS.add(DEFD, token(DigestMethod.SHA1));
DIGESTS.add(token("sha256"), token(DigestMethod.SHA256));
DIGESTS.add(token("sha512"), token(DigestMethod.SHA512));
SIGNATURES.add(DEFS, token(SignatureMethod.RSA_SHA1));
SIGNATURES.add(token("dsa_sha1"), token(SignatureMethod.DSA_SHA1));
TYPES.add(DEFT);
TYPES.add(ENVT);
// TYPES.add(DETT);
}
/** Input info. */
private final InputInfo input;
/**
* Constructor.
*
* @param ii input info
*/
public DigitalSignature(final InputInfo ii) {
input = ii;
}
/**
* Generates a signature.
* @param node node to be signed
* @param c canonicalization algorithm
* @param d digest algorithm
* @param sig signature algorithm
* @param ns signature element namespace prefix
* @param t signature type (enveloped, enveloping, detached)
* @param expr XPath expression which specifies node to be signed
* @param ce certificate which contains keystore information for
* signing the node, may be null
*
* @return signed node
* @throws QueryException query exception
*/
public ANode generateSignature(final ANode node, final byte[] c,
final byte[] d, final byte[] sig, final byte[] ns, final byte[] t,
final byte[] expr, final ANode ce) throws QueryException {
// checking input variables
byte[] b = c;
if(b.length == 0) b = DEFC;
b = CANONICALIZATIONS.get(lc(b));
if(b == null) CRYPTOCANINV.thrw(input, c);
final String canonicalization = string(b);
b = d;
if(b.length == 0) b = DEFD;
b = DIGESTS.get(lc(b));
if(b == null) CRYPTODIGINV.thrw(input, d);
final String digest = string(b);
b = sig;
if(b.length == 0) b = DEFS;
final byte[] tsig = b;
b = SIGNATURES.get(lc(b));
if(b == null) CRYPTOSIGINV.thrw(input, sig);
final String signature = string(b);
final String keytype = string(tsig).substring(0, 3);
b = t;
if(b.length == 0) b = DEFT;
final int ti = TYPES.id(lc(b));
if(ti == 0) CRYPTOSIGTYPINV.thrw(input, t);
final byte[] type = b;
ANode signedNode = null;
try {
final XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
final PrivateKey pk;
final PublicKey puk;
final KeyInfo ki;
// dealing with given certificate details to initialize the keystore
if(ce != null) {
String ksTY = null;
String ksPW = null;
String kAlias = null;
String pkPW = null;
String ksURI = null;
final Document ceDOM = toDOMNode(ce);
if(!ceDOM.getDocumentElement().getNodeName().
equals("digital-certificate"))
CRYPTOINVNM.thrw(input, ceDOM);
final NodeList ceChildren = ceDOM.getDocumentElement().getChildNodes();
final int s = ceChildren.getLength();
int ci = 0;
// iterate child axis to retrieve keystore setup
while(ci < s) {
final Node cn = ceChildren.item(ci++);
final String name = cn.getNodeName();
if(name.equals("keystore-type"))
ksTY = cn.getTextContent();
else if(name.equals("keystore-password"))
ksPW = cn.getTextContent();
else if(name.equals("key-alias"))
kAlias = cn.getTextContent();
else if(name.equals("private-key-password"))
pkPW = cn.getTextContent();
else if(name.equals("keystore-uri"))
ksURI = cn.getTextContent();
}
// initialize the keystore
final KeyStore ks = KeyStore.getInstance(ksTY);
if(ks == null) CRYPTOKSNULL.thrw(input, ks);
ks.load(new FileInputStream(ksURI), ksPW.toCharArray());
pk = (PrivateKey) ks.getKey(kAlias, pkPW.toCharArray());
final X509Certificate x509ce = (X509Certificate)
ks.getCertificate(kAlias);
if(x509ce == null) CRYPTOALINV.thrw(input, kAlias);
puk = x509ce.getPublicKey();
final KeyInfoFactory kifactory = fac.getKeyInfoFactory();
final KeyValue keyValue = kifactory.newKeyValue(puk);
final Vector<XMLStructure> kiCont = new Vector<XMLStructure>();
kiCont.add(keyValue);
final List<Object> x509Content = new ArrayList<Object>();
final X509IssuerSerial issuer = kifactory.newX509IssuerSerial(x509ce.
getIssuerX500Principal().getName(),
x509ce.getSerialNumber());
x509Content.add(x509ce.getSubjectX500Principal().getName());
x509Content.add(issuer);
x509Content.add(x509ce);
final X509Data x509Data = kifactory.newX509Data(x509Content);
kiCont.add(x509Data);
ki = kifactory.newKeyInfo(kiCont);
// auto-generate keys if no certificate is provided
} else {
final KeyPairGenerator gen =
KeyPairGenerator.getInstance(keytype);
gen.initialize(512);
final KeyPair kp = gen.generateKeyPair();
final KeyInfoFactory kif = fac.getKeyInfoFactory();
final KeyValue kv = kif.newKeyValue(kp.getPublic());
ki = kif.newKeyInfo(Collections.singletonList(kv));
pk = kp.getPrivate();
}
final Document inputNode = toDOMNode(node);
final List<Transform> tfList;
// validating a given XPath expression to get nodes to be signed
if(expr.length > 0) {
final XPathFactory xpf = XPathFactory.newInstance();
final XPathExpression xExpr = xpf.newXPath().compile(string(expr));
final NodeList xRes = (NodeList) xExpr.evaluate(inputNode,
XPathConstants.NODESET);
if(xRes.getLength() < 1)
CRYPTOXPINV.thrw(input, expr);
tfList = new ArrayList<Transform>(2);
tfList.add(fac.newTransform(Transform.XPATH,
new XPathFilterParameterSpec(string(expr))));
tfList.add(fac.newTransform(Transform.ENVELOPED,
(TransformParameterSpec) null));
} else {
tfList = Collections.singletonList(fac.newTransform(
Transform.ENVELOPED, (TransformParameterSpec) null));
}
// creating reference element
final Reference ref = fac.newReference("",
fac.newDigestMethod(digest, null), tfList, null, null);
// creating signed info element
final SignedInfo si = fac.newSignedInfo(fac.newCanonicalizationMethod(
canonicalization, (C14NMethodParameterSpec) null),
fac.newSignatureMethod(signature, null),
Collections.singletonList(ref));
// prepare document signature
final DOMSignContext signContext;
final XMLSignature xmlSig;
// enveloped signature
if(eq(type, DEFT)) {
signContext = new DOMSignContext(pk, inputNode.getDocumentElement());
xmlSig = fac.newXMLSignature(si, ki);
// detached signature
// } else if(eq(type, DETT)) {
// final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
// dbf.setNamespaceAware(true);
// inputNode = dbf.newDocumentBuilder().newDocument();
// signContext = new DOMSignContext(pk, inputNode);
// xmlSig = fac.newXMLSignature(si, ki);
// enveloping signature
} else {
final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
final XMLStructure cont = new DOMStructure(
inputNode.getDocumentElement());
final XMLObject obj = fac.newXMLObject(Collections.singletonList(cont),
"", null, null);
xmlSig = fac.newXMLSignature(si, ki, Collections.singletonList(obj),
null, null);
signContext = new DOMSignContext(pk, inputNode);
}
// set Signature element namespace prefix, if given
if(ns.length > 0)
signContext.setDefaultNamespacePrefix(new String(ns));
// actually sign the document
xmlSig.sign(signContext);
signedNode = toDBNode(inputNode);
} catch(final XPathExpressionException e) {
CRYPTOXPINV.thrw(input, e);
} catch(final SAXException e) {
CRYPTOIOEXC.thrw(input, e);
} catch(final IOException e) {
CRYPTOIOEXC.thrw(input, e);
} catch(final ParserConfigurationException e) {
CRYPTOIOEXC.thrw(input, e);
} catch(final KeyStoreException e) {
CRYPTOKSEXC.thrw(input, e);
} catch(final MarshalException e) {
CRYPTOSIGEXC.thrw(input, e);
} catch(final XMLSignatureException e) {
CRYPTOSIGEXC.thrw(input, e);
} catch(final NoSuchAlgorithmException e) {
CRYPTOALGEXC.thrw(input, e);
} catch(final CertificateException e) {
CRYPTOALGEXC.thrw(input, e);
} catch(final UnrecoverableKeyException e) {
CRYPTONOKEY.thrw(input, e);
} catch(final KeyException e) {
CRYPTONOKEY.thrw(input, e);
} catch(final InvalidAlgorithmParameterException e) {
CRYPTOALGEXC.thrw(input, e);
}
return signedNode;
}
/**
* Validates a signature.
* @param node input node
*
* @return true if signature valid
* @throws QueryException query exception
*/
public Item validateSignature(final ANode node) throws QueryException {
boolean coreVal = false;
try {
final Document doc = toDOMNode(node);
final DOMValidateContext valContext =
new DOMValidateContext(new MyKeySelector(), doc);
final NodeList signl = doc.getElementsByTagNameNS(XMLSignature.XMLNS,
"Signature");
if(signl.getLength() < 1)
CRYPTONOSIG.thrw(input, node);
valContext.setNode(signl.item(0));
final XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
final XMLSignature signature = fac.unmarshalXMLSignature(valContext);
coreVal = signature.validate(valContext);
return Bln.get(coreVal);
} catch(final XMLSignatureException e) {
CRYPTOIOEXC.thrw(input, e);
} catch(final SAXException e) {
CRYPTOIOEXC.thrw(input, e);
} catch(final ParserConfigurationException e) {
CRYPTOIOEXC.thrw(input, e);
} catch(final IOException e) {
CRYPTOIOEXC.thrw(input, e);
} catch(final MarshalException e) {
CRYPTOSIGEXC.thrw(input, e);
}
return Bln.get(coreVal);
}
/**
* Creates a BaseX database node from the given DOM node.
* @param n DOM node
* @return database node
* @throws QueryException query exception
*/
private ANode toDBNode(final Node n) throws QueryException {
final String xmlString;
DBNode dbn = null;
try {
final TransformerFactory transfac = TransformerFactory.newInstance();
final Transformer trans = transfac.newTransformer();
//create string from xml tree
final StringWriter sw = new StringWriter();
final StreamResult result = new StreamResult(sw);
final DOMSource source = new DOMSource(n);
trans.transform(source, result);
xmlString = sw.toString();
final Parser parser = Parser.xmlParser(IO.get(xmlString), new Prop());
final MemBuilder builder = new MemBuilder("", parser, new Prop());
final Data mem = builder.build();
dbn = new DBNode(mem, 1);
} catch(final IOException e) {
CRYPTOIOEXC.thrw(input, e);
} catch(final TransformerException e) {
CRYPTOIOEXC.thrw(input, e);
}
return dbn;
}
/**
* Serializes the given XML node to a byte array.
* @param n node to be serialized
* @return byte array containing XML
* @throws IOException exception
*/
private static byte[] nodeToBytes(final ANode n)
throws IOException {
final ByteArrayOutputStream b = new ByteArrayOutputStream();
final Serializer s = Serializer.get(b,
new SerializerProp("format=no"));
n.serialize(s);
s.close();
return b.toByteArray();
}
/**
* Creates a DOM node for the given input node.
* @param n node
* @return DOM node representation of input node
* @throws SAXException exception
* @throws IOException exception
* @throws ParserConfigurationException exception
*/
private static Document toDOMNode(final ANode n)
throws SAXException, IOException, ParserConfigurationException {
final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
return dbf.newDocumentBuilder().parse(
new ByteArrayInputStream(nodeToBytes(n)));
}
}