2 * The Shibboleth License, Version 1.
4 * University Corporation for Advanced Internet Development, Inc.
8 * Redistribution and use in source and binary forms, with or without
9 * modification, are permitted provided that the following conditions are met:
11 * Redistributions of source code must retain the above copyright notice, this
12 * list of conditions and the following disclaimer.
14 * Redistributions in binary form must reproduce the above copyright notice,
15 * this list of conditions and the following disclaimer in the documentation
16 * and/or other materials provided with the distribution, if any, must include
17 * the following acknowledgment: "This product includes software developed by
18 * the University Corporation for Advanced Internet Development
19 * <http://www.ucaid.edu>Internet2 Project. Alternately, this acknowledegement
20 * may appear in the software itself, if and wherever such third-party
21 * acknowledgments normally appear.
23 * Neither the name of Shibboleth nor the names of its contributors, nor
24 * Internet2, nor the University Corporation for Advanced Internet Development,
25 * Inc., nor UCAID may be used to endorse or promote products derived from this
26 * software without specific prior written permission. For written permission,
27 * please contact shibboleth@shibboleth.org
29 * Products derived from this software may not be called Shibboleth, Internet2,
30 * UCAID, or the University Corporation for Advanced Internet Development, nor
31 * may Shibboleth appear in their name, without prior written permission of the
32 * University Corporation for Advanced Internet Development.
35 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
36 * AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
37 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
38 * PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE DISCLAIMED AND THE ENTIRE RISK
39 * OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE.
40 * IN NO EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY
41 * CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC. BE LIABLE FOR ANY DIRECT,
42 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
43 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
44 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
45 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
46 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
47 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
50 package edu.internet2.middleware.shibboleth.utils;
52 import jargs.gnu.CmdLineParser;
55 import java.security.*;
56 import java.security.cert.Certificate;
57 import java.security.cert.*;
59 import org.apache.log4j.ConsoleAppender;
60 import org.apache.log4j.Level;
61 import org.apache.log4j.Logger;
62 import org.apache.log4j.PatternLayout;
63 import org.apache.xml.security.c14n.*;
64 import org.apache.xml.security.keys.KeyInfo;
65 import org.apache.xml.security.keys.content.X509Data;
66 import org.apache.xml.security.signature.*;
67 import org.apache.xml.security.transforms.*;
70 import edu.internet2.middleware.shibboleth.common.XML;
71 import edu.internet2.middleware.shibboleth.xml.Parser;
74 * Signs/verifies/maintains Shibboleth metadata files
76 * @author Scott Cantor
77 * @created June 11, 2002
79 public class MetadataTool
82 * Signs/verifies/maintains Shibboleth metadata files
84 * @param argv The command line arguments
85 * @exception Exception One of about fifty different kinds of possible errors
87 public static void main(String args[]) throws Exception {
88 // Process the command line.
89 CmdLineParser parser = new CmdLineParser();
90 CmdLineParser.Option helpOption = parser.addBooleanOption('h', "help");
91 CmdLineParser.Option signOption = parser.addBooleanOption('s', "sign");
92 CmdLineParser.Option noverifyOption = parser.addBooleanOption('N', "noverify");
93 CmdLineParser.Option inOption = parser.addStringOption('i', "in");
94 CmdLineParser.Option outOption = parser.addStringOption('o', "out");
95 CmdLineParser.Option keystoreOption = parser.addStringOption('k', "keystore");
96 CmdLineParser.Option aliasOption = parser.addStringOption('a', "alias");
97 CmdLineParser.Option pwOption = parser.addStringOption('p', "password");
98 CmdLineParser.Option nsOption = parser.addStringOption('x', "ns");
99 CmdLineParser.Option nameOption = parser.addStringOption('n', "name");
100 CmdLineParser.Option idOption = parser.addStringOption('I', "id");
101 CmdLineParser.Option debugOption = parser.addBooleanOption('d', "debug");
103 Boolean debugEnabled = ((Boolean) parser.getOptionValue(debugOption));
104 boolean debug = false;
105 if (debugEnabled != null) {
106 debug = debugEnabled.booleanValue();
108 configureLogging(debug);
113 catch (CmdLineParser.OptionException e) {
114 System.err.println(e.getMessage());
116 Thread.sleep(100); //silliness to get error to print first
118 catch (InterruptedException ie) {
121 printUsage(System.out);
125 Boolean helpEnabled = (Boolean)parser.getOptionValue(helpOption);
126 if (helpEnabled != null && helpEnabled.booleanValue()) {
127 printUsage(System.out);
131 Boolean sign = (Boolean)parser.getOptionValue(signOption);
132 Boolean noverify = (Boolean)parser.getOptionValue(noverifyOption);
133 String keystore = (String)parser.getOptionValue(keystoreOption);
134 String pw = (String)parser.getOptionValue(pwOption);
135 String alias = (String)parser.getOptionValue(aliasOption);
136 String infile = (String)parser.getOptionValue(inOption);
137 String outfile = (String)parser.getOptionValue(outOption);
138 String ns = (String)parser.getOptionValue(nsOption);
139 String name = (String)parser.getOptionValue(nameOption);
140 String id = (String)parser.getOptionValue(idOption);
142 if (infile == null || infile.length() == 0) {
143 printUsage(System.out);
147 if (keystore != null && keystore.length() > 0) {
148 if (alias == null || alias.length() == 0) {
149 printUsage(System.out);
154 PrivateKey privateKey = null;
155 Certificate chain[] = null;
156 X509Certificate cert = null;
158 if (sign != null && sign.booleanValue()) {
159 if (keystore == null || keystore.length() == 0 || pw == null || pw.length() == 0) {
160 printUsage(System.out);
163 KeyStore ks = KeyStore.getInstance("JKS");
164 FileInputStream fis = new FileInputStream(keystore);
165 ks.load(fis, pw.toCharArray());
166 privateKey = (PrivateKey)ks.getKey(alias, pw.toCharArray());
167 chain = ks.getCertificateChain(alias);
168 if (privateKey == null || chain == null) {
169 System.err.println("error: couldn't load key or certificate chain from keystore");
173 else if (keystore != null && keystore.length() > 0){
174 KeyStore ks = KeyStore.getInstance("JKS");
175 FileInputStream fis = new FileInputStream(keystore);
177 cert = (X509Certificate)ks.getCertificate(alias);
179 System.err.println("error: couldn't load certificate from keystore");
183 else if (noverify == null || !noverify.booleanValue()) {
184 printUsage(System.out);
189 // Parse file and verify root element.
190 Document doc = Parser.loadDom(infile,true);
191 Element e = doc.getDocumentElement();
192 if (ns != null && name != null && !org.opensaml.XML.isElementNamed(e,ns,name)) {
193 System.err.println("error: root element did not match ns and name parameters");
196 else if (!org.opensaml.XML.isElementNamed(e,XML.SHIB_NS,"SiteGroup") &&
197 !org.opensaml.XML.isElementNamed(e,XML.SHIB_NS,"Trust") &&
198 !org.opensaml.XML.isElementNamed(e,XML.TRUST_NS,"Trust") &&
199 !org.opensaml.XML.isElementNamed(e,XML.SAML2META_NS,"EntityDescriptor") &&
200 !org.opensaml.XML.isElementNamed(e,XML.SAML2META_NS,"EntitiesDescriptor")) {
201 System.err.println("error: root element must be SiteGroup, Trust, EntitiesDescriptor, or EntityDescriptor");
206 e = doc.getElementById(id);
208 System.err.println("error: no element with ID (" + id + ") found in document");
213 if (sign != null && sign.booleanValue()) {
214 // Remove any existing signature.
215 Element old = org.opensaml.XML.getFirstChildElement(e, org.opensaml.XML.XMLSIG_NS, "Signature");
219 // Create new signature.
220 XMLSignature sig = new XMLSignature(doc, "", XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1, Canonicalizer.ALGO_ID_C14N_EXCL_WITH_COMMENTS);
221 Transforms transforms = new Transforms(doc);
222 transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE);
223 transforms.addTransform(Transforms.TRANSFORM_C14N_EXCL_WITH_COMMENTS);
225 (id == null) ? ("") : ("#" + id),
227 org.apache.xml.security.utils.Constants.ALGO_ID_DIGEST_SHA1
230 // Add any X.509 certificates provided.
231 if (chain!=null && chain.length > 0) {
232 X509Data x509 = new X509Data(doc);
233 for (int i=0; i < chain.length; i++) {
234 if (chain[i] instanceof X509Certificate)
235 x509.addCertificate((X509Certificate)chain[i]);
237 KeyInfo keyinfo = new KeyInfo(doc);
239 sig.getElement().appendChild(keyinfo.getElement());
242 if (XML.SAML2META_NS.equals(e.getNamespaceURI()))
243 e.insertBefore(sig.getElement(),e.getFirstChild());
245 e.appendChild(sig.getElement());
246 sig.sign(privateKey);
249 // Check the root element's signature or the particular one specified.
250 Element sigElement = org.opensaml.XML.getLastChildElement(e, org.opensaml.XML.XMLSIG_NS, "Signature");
251 boolean v = (noverify == null || !noverify.booleanValue());
253 if (sigElement == null) {
254 System.err.println("error: file is not signed");
257 if (!verifySignature(doc, sigElement, cert)) {
258 System.err.println("error: signature did not verify");
262 else if (sigElement != null) {
263 System.err.println("verification of signer disabled, make sure you trust the source of this file!");
264 if (!verifySignature(doc, sigElement, cert)) {
265 System.err.println("error: signature did not verify");
270 System.err.println("verification disabled, and file is unsigned!");
273 // Check all the signatures.
274 NodeList nlist = e.getElementsByTagNameNS(org.opensaml.XML.XMLSIG_NS, "Signature");
275 for (int i=0; i < nlist.getLength(); i++) {
276 if (!verifySignature(doc, (Element)nlist.item(i), cert)) {
277 System.err.println("error: signature did not verify");
283 Canonicalizer c = Canonicalizer.getInstance(Canonicalizer.ALGO_ID_C14N_EXCL_WITH_COMMENTS);
284 if (outfile != null && outfile.length() > 0) {
285 OutputStream out = new FileOutputStream(outfile);
286 out.write(c.canonicalizeSubtree(doc));
290 // For some reason, using write(byte[]) doesn't work.
291 System.out.print(new String(c.canonicalizeSubtree(doc)));
295 private static boolean verifySignature(Document doc, Element sigNode, X509Certificate cert) throws Exception
297 XMLSignature sig = new XMLSignature(sigNode, "");
299 // Validate the signature content by checking for specific Transforms.
301 SignedInfo si=sig.getSignedInfo();
302 if (si.getLength()==1) {
303 Reference ref = si.item(0);
304 if (ref.getURI() == null || ref.getURI().equals("") ||
305 ref.getURI().equals("#" + ((Element)sigNode.getParentNode()).getAttributeNS(null,"ID"))) {
306 Transforms trans = ref.getTransforms();
307 for (int i=0; i < trans.getLength(); i++) {
308 if (trans.item(i).getURI().equals(Transforms.TRANSFORM_ENVELOPED_SIGNATURE))
310 else if (!trans.item(i).getURI().equals(Transforms.TRANSFORM_C14N_EXCL_OMIT_COMMENTS) &&
311 !trans.item(i).getURI().equals(Transforms.TRANSFORM_C14N_EXCL_WITH_COMMENTS)) {
320 System.err.println("error: signature profile was invalid");
325 return sig.checkSignatureValue(cert);
327 return sig.checkSignatureValue(sig.getKeyInfo().getPublicKey());
330 private static void printUsage(PrintStream out)
332 out.println("usage: java edu.internet2.middleware.shibboleth.utils.MetadataTool");
334 out.println("when signing: -i <uri> -s -k <keystore> -a <alias> -p <pass> [-o <outfile>]");
335 out.println("when updating: -i <uri> [-k <keystore> -a <alias> OR -N ] [-o <outfile>]");
336 out.println(" -i,--in input file or url");
337 out.println(" -k,--keystore pathname of Java keystore file");
338 out.println(" -a,--alias alias of signing or verification key");
339 out.println(" -p,--password keystore/key password");
340 out.println(" -o,--outfile write signed copy to this file instead of stdout");
341 out.println(" -s,--sign sign the input file and write out a signed version");
342 out.println(" -N,--noverify allows update of file without signature check");
343 out.println(" -h,--help print this message");
344 out.println(" -x,--ns XML namespace of root element");
345 out.println(" -n,--name name of root element");
346 out.println(" -I,--id ID attribute value of element to sign");
347 out.println(" -d, --debug run in debug mode");
352 private static void configureLogging(boolean debugEnabled)
354 ConsoleAppender rootAppender = new ConsoleAppender();
355 rootAppender.setWriter(new PrintWriter(System.err));
356 rootAppender.setName("stdout");
357 Logger.getRootLogger().addAppender(rootAppender);
360 Logger.getRootLogger().setLevel(Level.DEBUG);
361 rootAppender.setLayout(new PatternLayout("%-5p %-41X{serviceId} %d{ISO8601} (%c:%L) - %m%n"));
363 Logger.getRootLogger().setLevel(Level.INFO);
364 Logger.getLogger("edu.internet2.middleware.shibboleth.aa.attrresolv").setLevel(Level.WARN);
365 rootAppender.setLayout(new PatternLayout(PatternLayout.TTCC_CONVERSION_PATTERN));
367 Logger.getLogger("org.apache.xml.security").setLevel(Level.OFF);