2 * Copyright [2005] [University Corporation for Advanced Internet Development, Inc.]
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package edu.internet2.middleware.shibboleth.utils;
19 import jargs.gnu.CmdLineParser;
23 import java.security.*;
24 import java.security.cert.Certificate;
25 import java.security.cert.*;
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.*;
38 import edu.internet2.middleware.shibboleth.common.XML;
39 import edu.internet2.middleware.shibboleth.xml.Parser;
42 * Signs/verifies/maintains Shibboleth metadata files
44 * @author Scott Cantor
45 * @created June 11, 2002
47 public class MetadataTool {
50 * Signs/verifies/maintains Shibboleth metadata files
53 * The command line arguments
54 * @exception Exception
55 * One of about fifty different kinds of possible errors
57 public static void main(String args[]) throws Exception {
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");
74 Boolean debugEnabled = ((Boolean) parser.getOptionValue(debugOption));
75 boolean debug = false;
76 if (debugEnabled != null) {
77 debug = debugEnabled.booleanValue();
79 configureLogging(debug);
83 } catch (CmdLineParser.OptionException e) {
84 System.err.println(e.getMessage());
86 Thread.sleep(100); // silliness to get error to print first
87 } catch (InterruptedException ie) {
90 printUsage(System.out);
94 Boolean helpEnabled = (Boolean) parser.getOptionValue(helpOption);
95 if (helpEnabled != null && helpEnabled.booleanValue()) {
96 printUsage(System.out);
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);
111 if (infile == null || infile.length() == 0) {
112 printUsage(System.out);
116 if (keystore != null && keystore.length() > 0) {
117 if (alias == null || alias.length() == 0) {
118 printUsage(System.out);
123 PrivateKey privateKey = null;
124 Certificate chain[] = null;
125 X509Certificate cert = null;
127 if (sign != null && sign.booleanValue()) {
128 if (keystore == null || keystore.length() == 0 || pw == null || pw.length() == 0) {
129 printUsage(System.out);
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");
141 } else if (keystore != null && keystore.length() > 0) {
142 KeyStore ks = KeyStore.getInstance("JKS");
143 FileInputStream fis = new FileInputStream(keystore);
145 cert = (X509Certificate) ks.getCertificate(alias);
147 System.err.println("error: couldn't load certificate from keystore");
150 } else if (noverify == null || !noverify.booleanValue()) {
151 printUsage(System.out);
155 // Parse file and verify root element.
156 Document doc = Parser.loadDom(new URL(new URL("file:"),infile), true);
158 System.out.println("error: unable to read in file (" + infile + ")");
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");
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");
175 e = doc.getElementById(id);
177 System.err.println("error: no element with ID (" + id + ") found in document");
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);
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);
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]);
202 KeyInfo keyinfo = new KeyInfo(doc);
204 sig.getElement().appendChild(keyinfo.getElement());
207 if (XML.SAML2META_NS.equals(e.getNamespaceURI())) e.insertBefore(sig.getElement(), e.getFirstChild());
208 else e.appendChild(sig.getElement());
209 sig.sign(privateKey);
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());
215 if (sigElement == null) {
216 System.err.println("error: file is not signed");
219 if (!verifySignature(doc, sigElement, cert)) {
220 System.err.println("error: signature did not verify");
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");
230 System.err.println("verification disabled, and file is unsigned!");
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");
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));
249 // For some reason, using write(byte[]) doesn't work.
250 System.out.print(new String(c.canonicalizeSubtree(doc)));
254 private static boolean verifySignature(Document doc, Element sigNode, X509Certificate cert) throws Exception {
256 XMLSignature sig = new XMLSignature(sigNode, "");
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)) {
278 System.err.println("error: signature profile was invalid");
282 if (cert != null) return sig.checkSignatureValue(cert);
283 else return sig.checkSignatureValue(sig.getKeyInfo().getPublicKey());
286 private static void printUsage(PrintStream out) {
288 out.println("usage: java edu.internet2.middleware.shibboleth.utils.MetadataTool");
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");
308 private static void configureLogging(boolean debugEnabled) {
310 ConsoleAppender rootAppender = new ConsoleAppender();
311 rootAppender.setWriter(new PrintWriter(System.err));
312 rootAppender.setName("stdout");
313 Logger.getRootLogger().addAppender(rootAppender);
316 Logger.getRootLogger().setLevel(Level.DEBUG);
317 rootAppender.setLayout(new PatternLayout("%-5p %-41X{serviceId} %d{ISO8601} (%c:%L) - %m%n"));
319 Logger.getRootLogger().setLevel(Level.INFO);
320 Logger.getLogger("edu.internet2.middleware.shibboleth.aa.attrresolv").setLevel(Level.WARN);
321 rootAppender.setLayout(new PatternLayout(PatternLayout.TTCC_CONVERSION_PATTERN));
323 Logger.getLogger("org.apache.xml.security").setLevel(Level.OFF);