4ced4b6491e36e4a592bf1d5ec6e992e6dc83b82
[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                 Element e = doc.getDocumentElement();
157                 if (ns != null && name != null && !org.opensaml.XML.isElementNamed(e, ns, name)) {
158                         System.err.println("error: root element did not match ns and name parameters");
159                         System.exit(1);
160                 } else if (!org.opensaml.XML.isElementNamed(e, XML.SHIB_NS, "SiteGroup")
161                                 && !org.opensaml.XML.isElementNamed(e, XML.SHIB_NS, "Trust")
162                                 && !org.opensaml.XML.isElementNamed(e, XML.TRUST_NS, "Trust")
163                                 && !org.opensaml.XML.isElementNamed(e, XML.SAML2META_NS, "EntityDescriptor")
164                                 && !org.opensaml.XML.isElementNamed(e, XML.SAML2META_NS, "EntitiesDescriptor")) {
165                         System.err.println("error: root element must be SiteGroup, Trust, EntitiesDescriptor, or EntityDescriptor");
166                         System.exit(1);
167                 }
168
169                 if (id != null) {
170                         e = doc.getElementById(id);
171                         if (e == null) {
172                                 System.err.println("error: no element with ID (" + id + ") found in document");
173                                 System.exit(1);
174                         }
175                 }
176
177                 if (sign != null && sign.booleanValue()) {
178                         // Remove any existing signature.
179                         Element old = org.opensaml.XML.getFirstChildElement(e, org.opensaml.XML.XMLSIG_NS, "Signature");
180                         if (old != null) e.removeChild(old);
181
182                         // Create new signature.
183                         XMLSignature sig = new XMLSignature(doc, "", XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1,
184                                         Canonicalizer.ALGO_ID_C14N_EXCL_WITH_COMMENTS);
185                         Transforms transforms = new Transforms(doc);
186                         transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE);
187                         transforms.addTransform(Transforms.TRANSFORM_C14N_EXCL_WITH_COMMENTS);
188                         sig.addDocument((id == null) ? ("") : ("#" + id), transforms,
189                                         org.apache.xml.security.utils.Constants.ALGO_ID_DIGEST_SHA1);
190
191                         // Add any X.509 certificates provided.
192                         if (chain != null && chain.length > 0) {
193                                 X509Data x509 = new X509Data(doc);
194                                 for (int i = 0; i < chain.length; i++) {
195                                         if (chain[i] instanceof X509Certificate) x509.addCertificate((X509Certificate) chain[i]);
196                                 }
197                                 KeyInfo keyinfo = new KeyInfo(doc);
198                                 keyinfo.add(x509);
199                                 sig.getElement().appendChild(keyinfo.getElement());
200                         }
201
202                         if (XML.SAML2META_NS.equals(e.getNamespaceURI())) e.insertBefore(sig.getElement(), e.getFirstChild());
203                         else e.appendChild(sig.getElement());
204                         sig.sign(privateKey);
205                 } else {
206                         // Check the root element's signature or the particular one specified.
207                         Element sigElement = org.opensaml.XML.getLastChildElement(e, org.opensaml.XML.XMLSIG_NS, "Signature");
208                         boolean v = (noverify == null || !noverify.booleanValue());
209                         if (v) {
210                                 if (sigElement == null) {
211                                         System.err.println("error: file is not signed");
212                                         System.exit(1);
213                                 }
214                                 if (!verifySignature(doc, sigElement, cert)) {
215                                         System.err.println("error: signature did not verify");
216                                         System.exit(1);
217                                 }
218                         } else if (sigElement != null) {
219                                 System.err.println("verification of signer disabled, make sure you trust the source of this file!");
220                                 if (!verifySignature(doc, sigElement, cert)) {
221                                         System.err.println("error: signature did not verify");
222                                         System.exit(1);
223                                 }
224                         } else {
225                                 System.err.println("verification disabled, and file is unsigned!");
226                         }
227
228                         // Check all the signatures.
229                         NodeList nlist = e.getElementsByTagNameNS(org.opensaml.XML.XMLSIG_NS, "Signature");
230                         for (int i = 0; i < nlist.getLength(); i++) {
231                                 if (!verifySignature(doc, (Element) nlist.item(i), cert)) {
232                                         System.err.println("error: signature did not verify");
233                                         System.exit(1);
234                                 }
235                         }
236                 }
237
238                 Canonicalizer c = Canonicalizer.getInstance(Canonicalizer.ALGO_ID_C14N_EXCL_WITH_COMMENTS);
239                 if (outfile != null && outfile.length() > 0) {
240                         OutputStream out = new FileOutputStream(outfile);
241                         out.write(c.canonicalizeSubtree(doc));
242                         out.close();
243                 } else {
244                         // For some reason, using write(byte[]) doesn't work.
245                         System.out.print(new String(c.canonicalizeSubtree(doc)));
246                 }
247         }
248
249         private static boolean verifySignature(Document doc, Element sigNode, X509Certificate cert) throws Exception {
250
251                 XMLSignature sig = new XMLSignature(sigNode, "");
252
253                 // Validate the signature content by checking for specific Transforms.
254                 boolean valid = false;
255                 SignedInfo si = sig.getSignedInfo();
256                 if (si.getLength() == 1) {
257                         Reference ref = si.item(0);
258                         if (ref.getURI() == null || ref.getURI().equals("")
259                                         || ref.getURI().equals("#" + ((Element) sigNode.getParentNode()).getAttributeNS(null, "ID"))) {
260                                 Transforms trans = ref.getTransforms();
261                                 for (int i = 0; i < trans.getLength(); i++) {
262                                         if (trans.item(i).getURI().equals(Transforms.TRANSFORM_ENVELOPED_SIGNATURE)) valid = true;
263                                         else if (!trans.item(i).getURI().equals(Transforms.TRANSFORM_C14N_EXCL_OMIT_COMMENTS)
264                                                         && !trans.item(i).getURI().equals(Transforms.TRANSFORM_C14N_EXCL_WITH_COMMENTS)) {
265                                                 valid = false;
266                                                 break;
267                                         }
268                                 }
269                         }
270                 }
271
272                 if (!valid) {
273                         System.err.println("error: signature profile was invalid");
274                         return false;
275                 }
276
277                 if (cert != null) return sig.checkSignatureValue(cert);
278                 else return sig.checkSignatureValue(sig.getKeyInfo().getPublicKey());
279         }
280
281         private static void printUsage(PrintStream out) {
282
283                 out.println("usage: java edu.internet2.middleware.shibboleth.utils.MetadataTool");
284                 out.println();
285                 out.println("when signing:   -i <uri> -s -k <keystore> -a <alias> -p <pass> [-o <outfile>]");
286                 out.println("when updating:  -i <uri> [-k <keystore> -a <alias> OR -N ] [-o <outfile>]");
287                 out.println("  -i,--in              input file or url");
288                 out.println("  -k,--keystore        pathname of Java keystore file");
289                 out.println("  -a,--alias           alias of signing or verification key");
290                 out.println("  -p,--password        keystore/key password");
291                 out.println("  -o,--outfile         write signed copy to this file instead of stdout");
292                 out.println("  -s,--sign            sign the input file and write out a signed version");
293                 out.println("  -N,--noverify        allows update of file without signature check");
294                 out.println("  -h,--help            print this message");
295                 out.println("  -x,--ns              XML namespace of root element");
296                 out.println("  -n,--name            name of root element");
297                 out.println("  -I,--id              ID attribute value of element to sign");
298                 out.println("  -d, --debug          run in debug mode");
299                 out.println();
300                 System.exit(1);
301         }
302
303         private static void configureLogging(boolean debugEnabled) {
304
305                 ConsoleAppender rootAppender = new ConsoleAppender();
306                 rootAppender.setWriter(new PrintWriter(System.err));
307                 rootAppender.setName("stdout");
308                 Logger.getRootLogger().addAppender(rootAppender);
309
310                 if (debugEnabled) {
311                         Logger.getRootLogger().setLevel(Level.DEBUG);
312                         rootAppender.setLayout(new PatternLayout("%-5p %-41X{serviceId} %d{ISO8601} (%c:%L) - %m%n"));
313                 } else {
314                         Logger.getRootLogger().setLevel(Level.INFO);
315                         Logger.getLogger("edu.internet2.middleware.shibboleth.aa.attrresolv").setLevel(Level.WARN);
316                         rootAppender.setLayout(new PatternLayout(PatternLayout.TTCC_CONVERSION_PATTERN));
317                 }
318                 Logger.getLogger("org.apache.xml.security").setLevel(Level.OFF);
319         }
320 }