Bump logging to WARN
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / utils / MetadataTool.java
1 /*
2  * Copyright [2005] [University Corporation for Advanced Internet Development, Inc.]
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package edu.internet2.middleware.shibboleth.utils;
18
19 import jargs.gnu.CmdLineParser;
20
21 import java.io.*;
22 import java.net.URL;
23 import java.security.*;
24 import java.security.cert.Certificate;
25 import java.security.cert.*;
26
27 import org.apache.log4j.ConsoleAppender;
28 import org.apache.log4j.Level;
29 import org.apache.log4j.Logger;
30 import org.apache.log4j.PatternLayout;
31 import org.apache.xml.security.c14n.*;
32 import org.apache.xml.security.keys.KeyInfo;
33 import org.apache.xml.security.keys.content.X509Data;
34 import org.apache.xml.security.signature.*;
35 import org.apache.xml.security.transforms.*;
36 import org.w3c.dom.*;
37
38 import edu.internet2.middleware.shibboleth.common.XML;
39 import edu.internet2.middleware.shibboleth.xml.Parser;
40
41 /**
42  * Signs/verifies/maintains Shibboleth metadata files
43  * 
44  * @author Scott Cantor
45  * @created June 11, 2002
46  */
47 public class MetadataTool {
48
49         /**
50          * Signs/verifies/maintains Shibboleth metadata files
51          * 
52          * @param argv
53          *            The command line arguments
54          * @exception Exception
55          *                One of about fifty different kinds of possible errors
56          */
57         public static void main(String args[]) throws Exception {
58
59                 // Process the command line.
60                 CmdLineParser parser = new CmdLineParser();
61                 CmdLineParser.Option helpOption = parser.addBooleanOption('h', "help");
62                 CmdLineParser.Option signOption = parser.addBooleanOption('s', "sign");
63                 CmdLineParser.Option noverifyOption = parser.addBooleanOption('N', "noverify");
64                 CmdLineParser.Option inOption = parser.addStringOption('i', "in");
65                 CmdLineParser.Option outOption = parser.addStringOption('o', "out");
66                 CmdLineParser.Option keystoreOption = parser.addStringOption('k', "keystore");
67                 CmdLineParser.Option aliasOption = parser.addStringOption('a', "alias");
68                 CmdLineParser.Option pwOption = parser.addStringOption('p', "password");
69                 CmdLineParser.Option nsOption = parser.addStringOption('x', "ns");
70                 CmdLineParser.Option nameOption = parser.addStringOption('n', "name");
71                 CmdLineParser.Option idOption = parser.addStringOption('I', "id");
72                 CmdLineParser.Option debugOption = parser.addBooleanOption('d', "debug");
73
74                 Boolean debugEnabled = ((Boolean) parser.getOptionValue(debugOption));
75                 boolean debug = false;
76                 if (debugEnabled != null) {
77                         debug = debugEnabled.booleanValue();
78                 }
79                 configureLogging(debug);
80
81                 try {
82                         parser.parse(args);
83                 } catch (CmdLineParser.OptionException e) {
84                         System.err.println(e.getMessage());
85                         try {
86                                 Thread.sleep(100); // silliness to get error to print first
87                         } catch (InterruptedException ie) {
88                                 // doesn't matter
89                         }
90                         printUsage(System.out);
91                         System.exit(-1);
92                 }
93
94                 Boolean helpEnabled = (Boolean) parser.getOptionValue(helpOption);
95                 if (helpEnabled != null && helpEnabled.booleanValue()) {
96                         printUsage(System.out);
97                         System.exit(0);
98                 }
99
100                 Boolean sign = (Boolean) parser.getOptionValue(signOption);
101                 Boolean noverify = (Boolean) parser.getOptionValue(noverifyOption);
102                 String keystore = (String) parser.getOptionValue(keystoreOption);
103                 String pw = (String) parser.getOptionValue(pwOption);
104                 String alias = (String) parser.getOptionValue(aliasOption);
105                 String infile = (String) parser.getOptionValue(inOption);
106                 String outfile = (String) parser.getOptionValue(outOption);
107                 String ns = (String) parser.getOptionValue(nsOption);
108                 String name = (String) parser.getOptionValue(nameOption);
109                 String id = (String) parser.getOptionValue(idOption);
110
111                 if (infile == null || infile.length() == 0) {
112                         printUsage(System.out);
113                         System.exit(-1);
114                 }
115
116                 if (keystore != null && keystore.length() > 0) {
117                         if (alias == null || alias.length() == 0) {
118                                 printUsage(System.out);
119                                 System.exit(-1);
120                         }
121                 }
122
123                 PrivateKey privateKey = null;
124                 Certificate chain[] = null;
125                 X509Certificate cert = null;
126
127                 if (sign != null && sign.booleanValue()) {
128                         if (keystore == null || keystore.length() == 0 || pw == null || pw.length() == 0) {
129                                 printUsage(System.out);
130                                 System.exit(-1);
131                         }
132                         KeyStore ks = KeyStore.getInstance("JKS");
133                         FileInputStream fis = new FileInputStream(keystore);
134                         ks.load(fis, pw.toCharArray());
135                         privateKey = (PrivateKey) ks.getKey(alias, pw.toCharArray());
136                         chain = ks.getCertificateChain(alias);
137                         if (privateKey == null || chain == null) {
138                                 System.err.println("error: couldn't load key or certificate chain from keystore");
139                                 System.exit(1);
140                         }
141                 } else if (keystore != null && keystore.length() > 0) {
142                         KeyStore ks = KeyStore.getInstance("JKS");
143                         FileInputStream fis = new FileInputStream(keystore);
144                         ks.load(fis, null);
145                         cert = (X509Certificate) ks.getCertificate(alias);
146                         if (cert == null) {
147                                 System.err.println("error: couldn't load certificate from keystore");
148                                 System.exit(1);
149                         }
150                 } else if (noverify == null || !noverify.booleanValue()) {
151                         printUsage(System.out);
152                         System.exit(-1);
153                 }
154
155                 // Parse file and verify root element.
156                 Document doc = Parser.loadDom(new URL(new URL("file:"),infile), true);
157                 if (doc == null) {
158                         System.out.println("error: unable to read in file (" + infile + ")");
159                         System.exit(-1);
160                 }
161                 Element e = doc.getDocumentElement();
162                 if (ns != null && name != null && !org.opensaml.XML.isElementNamed(e, ns, name)) {
163                         System.err.println("error: root element did not match ns and name parameters");
164                         System.exit(1);
165                 } else if (!org.opensaml.XML.isElementNamed(e, XML.SHIB_NS, "SiteGroup")
166                                 && !org.opensaml.XML.isElementNamed(e, XML.SHIB_NS, "Trust")
167                                 && !org.opensaml.XML.isElementNamed(e, XML.TRUST_NS, "Trust")
168                                 && !org.opensaml.XML.isElementNamed(e, XML.SAML2META_NS, "EntityDescriptor")
169                                 && !org.opensaml.XML.isElementNamed(e, XML.SAML2META_NS, "EntitiesDescriptor")) {
170                         System.err.println("error: root element must be SiteGroup, Trust, EntitiesDescriptor, or EntityDescriptor");
171                         System.exit(1);
172                 }
173
174                 if (id != null) {
175                         e = doc.getElementById(id);
176                         if (e == null) {
177                                 System.err.println("error: no element with ID (" + id + ") found in document");
178                                 System.exit(1);
179                         }
180                 }
181
182                 if (sign != null && sign.booleanValue()) {
183                         // Remove any existing signature.
184                         Element old = org.opensaml.XML.getFirstChildElement(e, org.opensaml.XML.XMLSIG_NS, "Signature");
185                         if (old != null) e.removeChild(old);
186
187                         // Create new signature.
188                         XMLSignature sig = new XMLSignature(doc, "", XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1,
189                                         Canonicalizer.ALGO_ID_C14N_EXCL_WITH_COMMENTS);
190                         Transforms transforms = new Transforms(doc);
191                         transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE);
192                         transforms.addTransform(Transforms.TRANSFORM_C14N_EXCL_WITH_COMMENTS);
193                         sig.addDocument((id == null) ? ("") : ("#" + id), transforms,
194                                         org.apache.xml.security.utils.Constants.ALGO_ID_DIGEST_SHA1);
195
196                         // Add any X.509 certificates provided.
197                         if (chain != null && chain.length > 0) {
198                                 X509Data x509 = new X509Data(doc);
199                                 for (int i = 0; i < chain.length; i++) {
200                                         if (chain[i] instanceof X509Certificate) x509.addCertificate((X509Certificate) chain[i]);
201                                 }
202                                 KeyInfo keyinfo = new KeyInfo(doc);
203                                 keyinfo.add(x509);
204                                 sig.getElement().appendChild(keyinfo.getElement());
205                         }
206
207                         if (XML.SAML2META_NS.equals(e.getNamespaceURI())) e.insertBefore(sig.getElement(), e.getFirstChild());
208                         else e.appendChild(sig.getElement());
209                         sig.sign(privateKey);
210                 } else {
211                         // Check the root element's signature or the particular one specified.
212                         Element sigElement = org.opensaml.XML.getLastChildElement(e, org.opensaml.XML.XMLSIG_NS, "Signature");
213                         boolean v = (noverify == null || !noverify.booleanValue());
214                         if (v) {
215                                 if (sigElement == null) {
216                                         System.err.println("error: file is not signed");
217                                         System.exit(1);
218                                 }
219                                 if (!verifySignature(doc, sigElement, cert)) {
220                                         System.err.println("error: signature did not verify");
221                                         System.exit(1);
222                                 }
223                         } else if (sigElement != null) {
224                                 System.err.println("verification of signer disabled, make sure you trust the source of this file!");
225                                 if (!verifySignature(doc, sigElement, cert)) {
226                                         System.err.println("error: signature did not verify");
227                                         System.exit(1);
228                                 }
229                         } else {
230                                 System.err.println("verification disabled, and file is unsigned!");
231                         }
232
233                         // Check all the signatures.
234                         NodeList nlist = e.getElementsByTagNameNS(org.opensaml.XML.XMLSIG_NS, "Signature");
235                         for (int i = 0; i < nlist.getLength(); i++) {
236                                 if (!verifySignature(doc, (Element) nlist.item(i), cert)) {
237                                         System.err.println("error: signature did not verify");
238                                         System.exit(1);
239                                 }
240                         }
241                 }
242
243                 Canonicalizer c = Canonicalizer.getInstance(Canonicalizer.ALGO_ID_C14N_EXCL_WITH_COMMENTS);
244                 if (outfile != null && outfile.length() > 0) {
245                         OutputStream out = new FileOutputStream(outfile);
246                         out.write(c.canonicalizeSubtree(doc));
247                         out.close();
248                 } else {
249                         // For some reason, using write(byte[]) doesn't work.
250                         System.out.print(new String(c.canonicalizeSubtree(doc)));
251                 }
252         }
253
254         private static boolean verifySignature(Document doc, Element sigNode, X509Certificate cert) throws Exception {
255
256                 XMLSignature sig = new XMLSignature(sigNode, "");
257
258                 // Validate the signature content by checking for specific Transforms.
259                 boolean valid = false;
260                 SignedInfo si = sig.getSignedInfo();
261                 if (si.getLength() == 1) {
262                         Reference ref = si.item(0);
263                         if (ref.getURI() == null || ref.getURI().equals("")
264                                         || ref.getURI().equals("#" + ((Element) sigNode.getParentNode()).getAttributeNS(null, "ID"))) {
265                                 Transforms trans = ref.getTransforms();
266                                 for (int i = 0; i < trans.getLength(); i++) {
267                                         if (trans.item(i).getURI().equals(Transforms.TRANSFORM_ENVELOPED_SIGNATURE)) valid = true;
268                                         else if (!trans.item(i).getURI().equals(Transforms.TRANSFORM_C14N_EXCL_OMIT_COMMENTS)
269                                                         && !trans.item(i).getURI().equals(Transforms.TRANSFORM_C14N_EXCL_WITH_COMMENTS)) {
270                                                 valid = false;
271                                                 break;
272                                         }
273                                 }
274                         }
275                 }
276
277                 if (!valid) {
278                         System.err.println("error: signature profile was invalid");
279                         return false;
280                 }
281
282                 if (cert != null) return sig.checkSignatureValue(cert);
283                 else return sig.checkSignatureValue(sig.getKeyInfo().getPublicKey());
284         }
285
286         private static void printUsage(PrintStream out) {
287
288                 out.println("usage: java edu.internet2.middleware.shibboleth.utils.MetadataTool");
289                 out.println();
290                 out.println("when signing:   -i <uri> -s -k <keystore> -a <alias> -p <pass> [-o <outfile>]");
291                 out.println("when updating:  -i <uri> [-k <keystore> -a <alias> OR -N ] [-o <outfile>]");
292                 out.println("  -i,--in              input file or url");
293                 out.println("  -k,--keystore        pathname of Java keystore file");
294                 out.println("  -a,--alias           alias of signing or verification key");
295                 out.println("  -p,--password        keystore/key password");
296                 out.println("  -o,--outfile         write signed copy to this file instead of stdout");
297                 out.println("  -s,--sign            sign the input file and write out a signed version");
298                 out.println("  -N,--noverify        allows update of file without signature check");
299                 out.println("  -h,--help            print this message");
300                 out.println("  -x,--ns              XML namespace of root element");
301                 out.println("  -n,--name            name of root element");
302                 out.println("  -I,--id              ID attribute value of element to sign");
303                 out.println("  -d, --debug          run in debug mode");
304                 out.println();
305                 System.exit(1);
306         }
307
308         private static void configureLogging(boolean debugEnabled) {
309
310                 ConsoleAppender rootAppender = new ConsoleAppender();
311                 rootAppender.setWriter(new PrintWriter(System.err));
312                 rootAppender.setName("stdout");
313                 Logger.getRootLogger().addAppender(rootAppender);
314
315                 if (debugEnabled) {
316                         Logger.getRootLogger().setLevel(Level.DEBUG);
317                         rootAppender.setLayout(new PatternLayout("%-5p %-41X{serviceId} %d{ISO8601} (%c:%L) - %m%n"));
318                 } else {
319                         Logger.getRootLogger().setLevel(Level.WARN);
320                         rootAppender.setLayout(new PatternLayout(PatternLayout.TTCC_CONVERSION_PATTERN));
321                 }
322                 Logger.getLogger("org.apache.xml.security").setLevel(Level.OFF);
323         }
324 }