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