Added signing of subelements, profile checking, and tighter verification.
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / utils / MetadataTool.java
1 /* 
2  * The Shibboleth License, Version 1. 
3  * Copyright (c) 2002 
4  * University Corporation for Advanced Internet Development, Inc. 
5  * All rights reserved
6  * 
7  * 
8  * Redistribution and use in source and binary forms, with or without 
9  * modification, are permitted provided that the following conditions are met:
10  * 
11  * Redistributions of source code must retain the above copyright notice, this 
12  * list of conditions and the following disclaimer.
13  * 
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.
22  * 
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
28  * 
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.
33  * 
34  * 
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.
48  */
49
50 package edu.internet2.middleware.shibboleth.utils;
51
52 import jargs.gnu.CmdLineParser;
53
54 import java.io.*;
55 import java.security.*;
56 import java.security.cert.Certificate;
57 import java.security.cert.*;
58
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.*;
68 import org.w3c.dom.*;
69
70 import edu.internet2.middleware.shibboleth.common.XML;
71 import edu.internet2.middleware.shibboleth.xml.Parser;
72
73 /**
74  *  Signs/verifies/maintains Shibboleth metadata files
75  *
76  * @author     Scott Cantor
77  * @created    June 11, 2002
78  */
79 public class MetadataTool
80 {
81     /**
82      *  Signs/verifies/maintains Shibboleth metadata files
83      *
84      * @param  argv           The command line arguments
85      * @exception  Exception  One of about fifty different kinds of possible errors
86      */
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");
102                 
103                 Boolean debugEnabled = ((Boolean) parser.getOptionValue(debugOption));
104                 boolean debug = false;
105                 if (debugEnabled != null) {
106                         debug = debugEnabled.booleanValue();
107                 }
108         configureLogging(debug);
109         
110         try {
111             parser.parse(args);
112         }
113         catch (CmdLineParser.OptionException e) {
114             System.err.println(e.getMessage());
115             try {
116                 Thread.sleep(100); //silliness to get error to print first
117             }
118             catch (InterruptedException ie) {
119                 //doesn't matter
120             }
121             printUsage(System.out);
122             System.exit(-1);
123         }
124
125         Boolean helpEnabled = (Boolean)parser.getOptionValue(helpOption);
126         if (helpEnabled != null && helpEnabled.booleanValue()) {
127             printUsage(System.out);
128             System.exit(0);
129         }
130         
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);
141
142         if (infile == null || infile.length() == 0) {
143             printUsage(System.out);
144             System.exit(-1);
145         }
146         
147         if (keystore != null && keystore.length() > 0) {
148             if (alias == null || alias.length() == 0) {
149                 printUsage(System.out);
150                 System.exit(-1);
151             }
152         }
153
154         PrivateKey privateKey = null;
155         Certificate chain[] = null;
156         X509Certificate cert = null;
157         
158         if (sign != null && sign.booleanValue()) {
159             if (keystore == null || keystore.length() == 0 || pw == null || pw.length() == 0) {
160                 printUsage(System.out);
161                 System.exit(-1);
162             }
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");
170                 System.exit(1);
171             }
172         }
173         else if (keystore != null && keystore.length() > 0){
174             KeyStore ks = KeyStore.getInstance("JKS");
175             FileInputStream fis = new FileInputStream(keystore);
176             ks.load(fis, null);
177             cert = (X509Certificate)ks.getCertificate(alias);
178             if (cert == null) {
179                 System.err.println("error: couldn't load certificate from keystore");
180                 System.exit(1);
181             }
182         }
183         else if (noverify == null || !noverify.booleanValue()) {
184             printUsage(System.out);
185             System.exit(-1);
186         }
187         
188         
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");
194             System.exit(1);
195         }
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");
202             System.exit(1);
203         }
204         
205         if (id != null) {
206             e = doc.getElementById(id);
207             if (e == null) {
208                 System.err.println("error: no element with ID (" + id + ") found in document");
209                 System.exit(1);
210             }
211         }
212
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");
216             if (old != null)
217                 e.removeChild(old);
218
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);
224             sig.addDocument(
225                 (id == null) ? ("") : ("#" + id),
226                 transforms,
227                 org.apache.xml.security.utils.Constants.ALGO_ID_DIGEST_SHA1
228                 );
229
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]);
236                 }
237                 KeyInfo keyinfo = new KeyInfo(doc);
238                 keyinfo.add(x509);
239                 sig.getElement().appendChild(keyinfo.getElement());
240             }
241
242             if (XML.SAML2META_NS.equals(e.getNamespaceURI()))
243                 e.insertBefore(sig.getElement(),e.getFirstChild());
244             else
245                 e.appendChild(sig.getElement());
246             sig.sign(privateKey);
247         }
248         else {
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());
252             if (v) {
253                 if (sigElement == null) {
254                     System.err.println("error: file is not signed");
255                     System.exit(1);
256                 }
257                 if (!verifySignature(doc, sigElement, cert)) {
258                     System.err.println("error: signature did not verify");
259                     System.exit(1);
260                 }
261             }
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");
266                     System.exit(1);
267                 }
268             }
269             else {
270                 System.err.println("verification disabled, and file is unsigned!");
271             }
272             
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");
278                     System.exit(1);
279                 }
280             }
281         }
282         
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));
287             out.close();
288         }
289         else {
290             // For some reason, using write(byte[]) doesn't work.
291                 System.out.print(new String(c.canonicalizeSubtree(doc)));
292         }
293     }
294     
295     private static boolean verifySignature(Document doc, Element sigNode, X509Certificate cert) throws Exception
296     {
297         XMLSignature sig = new XMLSignature(sigNode, "");
298
299         // Validate the signature content by checking for specific Transforms.
300         boolean valid=false;
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))
309                         valid = true;
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)) {
312                         valid = false;
313                         break;
314                     }
315                 }
316             }
317         }
318         
319         if (!valid) {
320             System.err.println("error: signature profile was invalid");
321             return false;
322         }
323         
324         if (cert != null)
325             return sig.checkSignatureValue(cert);
326         else
327             return sig.checkSignatureValue(sig.getKeyInfo().getPublicKey());
328     }
329
330     private static void printUsage(PrintStream out)
331     {
332         out.println("usage: java edu.internet2.middleware.shibboleth.utils.MetadataTool");
333         out.println();
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");
348         out.println();
349         System.exit(1);
350     }
351     
352         private static void configureLogging(boolean debugEnabled) 
353         {
354                 ConsoleAppender rootAppender = new ConsoleAppender();
355                 rootAppender.setWriter(new PrintWriter(System.err));
356                 rootAppender.setName("stdout");
357                 Logger.getRootLogger().addAppender(rootAppender);
358
359                 if (debugEnabled) {
360                         Logger.getRootLogger().setLevel(Level.DEBUG);
361                         rootAppender.setLayout(new PatternLayout("%-5p %-41X{serviceId} %d{ISO8601} (%c:%L) - %m%n")); 
362                 } else {
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)); 
366                 }
367                 Logger.getLogger("org.apache.xml.security").setLevel(Level.OFF);
368         }
369 }
370