View Javadoc

1   package net.sf.bse;
2   
3   /*
4    * Copyright (c) 2002-2003 BSE project contributors 
5    * (http://bse.sourceforge.net/)
6    * 
7    * Permission is hereby granted, free of charge, to any person obtaining a copy
8    * of this software and associated documentation files (the "Software"), to deal
9    * in the Software without restriction, including without limitation the rights
10   * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11   * copies of the Software, and to permit persons to whom the Software is
12   * furnished to do so, subject to the following conditions:
13   * 
14   * The above copyright notice and this permission notice shall be included in
15   * all copies or substantial portions of the Software.
16   * 
17   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
20   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23   * THE SOFTWARE.
24   */
25  
26  import java.io.BufferedOutputStream;
27  import java.io.ByteArrayInputStream;
28  import java.io.ByteArrayOutputStream;
29  import java.io.File;
30  import java.io.FileOutputStream;
31  import java.io.IOException;
32  import java.io.OutputStream;
33  import java.io.PrintStream;
34  import java.security.KeyFactory;
35  import java.security.MessageDigest;
36  import java.security.PrivateKey;
37  import java.security.Signature;
38  import java.security.spec.PKCS8EncodedKeySpec;
39  import java.util.ArrayList;
40  import java.util.Hashtable;
41  import java.util.Map;
42  import java.util.StringTokenizer;
43  
44  import org.bouncycastle.asn1.DERBitString;
45  import org.bouncycastle.asn1.DERConstructedSequence;
46  import org.bouncycastle.asn1.DERInteger;
47  import org.bouncycastle.asn1.DERObjectIdentifier;
48  import org.bouncycastle.asn1.DEROutputStream;
49  import org.bouncycastle.asn1.DERTaggedObject;
50  import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier;
51  import org.bouncycastle.asn1.x509.GeneralName;
52  import org.bouncycastle.asn1.x509.X509Name;
53  import org.bouncycastle.jce.provider.X509CertificateObject;
54  
55  /***
56   * Command to sign an Xlet.
57   *
58   * @author Bill Foote (bill.foote@sun.com)
59   * @author Aleksi Peebles (aleksi.peebles@infocast.fi)
60   * @version $Revision: 1.3 $ $Date: 2004/05/06 09:55:18 $
61   */
62  public class SignXlet extends Command
63  {
64      public SignXlet(Map args)
65      {
66          super(args);
67      }
68      
69      public void usageMessage(PrintStream out)
70      {
71          out.println(
72  "Command:  xlet\n\n" +
73  
74  "    Signs an MHP Xlet\n\n" +
75  
76  "    Arguments:\n\n" +
77  
78  "        certs:  Full names of all certificate files in the certificate\n" +
79  "                chain, separated by the OS path separator.\n" +
80  "                The file names must be in the correct ascending order:\n" +
81  "                signing certificate first and root certificate last.\n" +
82  "        key:    Full name of file containing signing private key\n" +
83  "        src:    Base directory to copy Xlet files from\n" +
84  "        dest:   Destination. If this is equal to src, the files will be\n" +
85  "                added/modified in this directory. Otherwise,\n" +
86  "                a directory with this name will be created and\n" +
87  "                if the directory already exists and the optional rm\n" +
88  "                argument is not set to \"true\" the command will fail.\n\n" +
89  
90  "    Plus, optionally:\n\n" +
91  
92  "        files:  Full names of all files to be signed, separated by the\n" +
93  "                OS path separator. All other files will not be signed.\n" +
94  "                If this argument is left out all files will be signed.\n" +
95  "        rm:     If set to \"true\", the dest directory will be deleted\n" +
96  "                in the case that it already exists. If set to \"false\"\n" +
97  "                (or anything else) or left out, the command will fail if\n" +
98  "                the dest directory already exists.\n");
99  
100     }
101     
102     public String[] getRequiredArgs()
103     {
104         return new String[] { "certs:", "key:", "src:", "dest:" };
105     }
106     
107     public String[] getOptionalArgs()
108     {
109         return new String[] { "files:", "rm:" };
110     }    
111     
112     private X509CertificateObject readCert(String file) throws Exception
113     {
114         System.out.println("Reading certificate from " + file);
115         ByteArrayInputStream bis = 
116             new ByteArrayInputStream(readBytesFromFile(file));
117         return readX509(bis);
118     }
119     
120     private PrivateKey readPrivateKey(String file) throws Exception
121     {
122         System.out.println("Reading private key from " + file);
123         byte[] encoded = readBytesFromFile(file);
124         PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(encoded);
125         KeyFactory fact = KeyFactory.getInstance("RSA", "BC");
126         PrivateKey key = fact.generatePrivate(spec);
127         return key;
128     }
129         
130     private void rmMinusR(File f)
131     {
132         if (f.isFile())
133         {
134             f.delete();
135         } 
136         else if (f.isDirectory())
137         {
138             String[] contents = f.list();
139             for (int i = 0; i < contents.length; i++)
140             {
141                 rmMinusR(new File(f, contents[i]));
142             }
143             f.delete();
144         }
145     }
146 
147     /***
148      * Copies the contents of the <code>src</code> directory to the 
149      * <code>dest</code> directory and creates a hashfile in all copied 
150      * directories. A separate digest is calculated for every file listed in 
151      * <code>signFiles</code> (see MHP 1.0.2 spec 12.4.1.4).
152      */
153     private byte[] multiDigestCopyAndHash(String path, File src, File dest, 
154         Hashtable signFiles) throws Exception
155     {
156         if (!src.isDirectory())
157         {
158             throw new IOException(src + " isn't a readable directory");
159         }
160         if (dest != null && !dest.mkdirs())
161         {
162             throw new IOException("Couldn't create directory " + dest);
163         }
164         
165         // Start constructing hashfile. See MHP 1.0.2 spec 12.4.1.1.
166         
167         ByteArrayOutputStream hashFile = new ByteArrayOutputStream();
168         
169         String[] contents = src.list();
170         
171         // digest_count, 16 bit uimsbf
172         // Is the same as file count.
173         hashFile.write(contents.length >> 8);
174         hashFile.write(contents.length & 0xff);
175         
176         for (int i = 0; i < contents.length; i++)
177         {                        
178             File srcF = new File(src, contents[i]);
179             File destF = dest == null ? null : new File(dest, contents[i]);
180             
181             System.out.println("Calculating digest for file " + srcF);
182             
183             int digestType = 0; // 0 = Non authenticated
184             if (signFiles.get(buildPath(path, contents[i])) != null ||
185                 signFiles.size() == 0 || srcF.isDirectory())
186             {
187                 digestType = 1; // 1 = MD5, 8 bit uimsbf
188             }
189             hashFile.write(digestType);
190 
191             // name_count, 16 bit uimsbf
192             // just one file in digest
193             hashFile.write(0);
194             hashFile.write(1);
195             
196             if (contents[i].length() > 0xff)
197             {
198                 throw new IOException("Filename too long:  " + contents[i]);
199             }
200             // name_length
201             hashFile.write(contents[i].length());
202             
203             for (int j = 0; j < contents[i].length(); j++)
204             {
205                 // name_byte
206                 hashFile.write(contents[i].charAt(j));
207             }            
208             
209             // Copy file to destination directory and calculate digest
210             
211             MessageDigest md5 = MessageDigest.getInstance("MD5", "BC");
212             md5.reset();
213 
214             if (srcF.isDirectory())
215             {
216                 byte[] ba = multiDigestCopyAndHash(
217                     buildPath(path, contents[i]), srcF, destF, signFiles);
218                 md5.update(ba);
219                 hashFile.write(md5.digest());
220             } 
221             else if (srcF.isFile())
222             {
223                 byte[] ba = readBytesFromFile(srcF.getAbsolutePath());
224                 if (dest != null) {
225                     FileOutputStream fos = new FileOutputStream(destF);
226                     fos.write(ba);
227                     fos.close();
228                 }
229                 if (digestType > 0)
230                 {
231                     md5.update(ba);
232                     hashFile.write(md5.digest());
233                 }
234             } 
235             else
236             {
237                 System.err.println("Ignoring " + srcF);
238             }
239         }
240                                                 
241         hashFile.close();
242                 
243         byte[] hashContents = hashFile.toByteArray();
244         
245         File hashF = new File(dest == null ? src : dest, "dvb.hashfile");
246         FileOutputStream fos = new FileOutputStream(hashF);
247         fos.write(hashContents);
248         fos.close();
249         return hashContents;
250     }    
251 
252     private String buildPath(String path, String file) {
253       if (path == null || path.length() == 0) {
254         return file;
255       }
256       return path + File.separator + file;
257     }
258 
259     private void writeCertificate(OutputStream os, X509CertificateObject cert)
260         throws Exception
261     {
262         byte[] encoded = cert.getEncoded();
263         
264         // Length, 24 bit uimsbf
265         os.write(encoded.length >> 16);
266         os.write((encoded.length >> 8) & 0xff);
267         os.write(encoded.length & 0xff);
268         
269         os.write(encoded);
270     }
271     
272     public void run() throws Exception
273     {
274         Hashtable signFiles = new Hashtable();
275         if (getArg("files:") != null)
276         {
277             StringTokenizer st = new StringTokenizer(getArg("files:"), 
278                 File.pathSeparator);
279             while (st.hasMoreTokens())
280             {
281                 // The digest algorithm value isn't used at the moment, 
282                 // but maybe in future versions...
283                 signFiles.put(st.nextToken(), "MD5");
284             }
285         }
286         
287         System.out.println("Signing Xlet " + getArg("src:"));
288         
289         File src = new File(getArg("src:"));
290         File dest = new File(getArg("dest:"));
291         if (dest.getAbsolutePath().equals(src.getAbsolutePath())) {
292           dest = null;
293         }
294         
295         if (dest != null && dest.exists())
296         {
297             if (getArg("rm:") != null && 
298                 getArg("rm:").equalsIgnoreCase("true"))
299             {
300                 System.out.println("Removing " + dest);
301                 rmMinusR(dest);
302             }
303             else
304             {
305                 System.out.println(
306                     "Command failed: destination directory already exists.\n");
307                 return;
308             }
309         }
310         
311         // Certificate file. See MHP 1.0.2 spec 12.4.3.
312         
313         System.out.println("Generating the certificate file");                
314 
315         ArrayList certFiles = new ArrayList();
316         StringTokenizer st = 
317             new StringTokenizer(getArg("certs:"), File.pathSeparator);
318         while (st.hasMoreTokens())
319         {
320             certFiles.add(st.nextToken());
321         }
322         
323         File certFile = new File(src, "dvb.certificates.1");
324         OutputStream os = 
325             new BufferedOutputStream(new FileOutputStream(certFile));
326 
327         // certificate_count, 16 bit uimsbf
328         os.write(certFiles.size() >> 8);
329         os.write(certFiles.size() & 0xff);
330 
331         X509CertificateObject leafCert = readCert((String)certFiles.get(0));
332         writeCertificate(os, leafCert);
333         for (int i = 1; i < certFiles.size(); i++)
334         {
335             writeCertificate(os, readCert((String)certFiles.get(i)));
336         }
337 
338         os.close();
339         
340         System.out.println("Computing hashes and copying tree");
341         
342         byte[] hashFile = multiDigestCopyAndHash(null, src, dest, signFiles);
343         
344         System.out.println("Generating signature file");
345 
346         // Begin constructing signature sequence.
347         // See MHP 1.0.2 spec 12.4.2.
348 
349         PrivateKey key = readPrivateKey(getArg("key:"));
350         Signature engine = Signature.getInstance("MD5WITHRSA", "BC");        
351         engine.initSign(key);
352         engine.update(hashFile);
353         byte[] signature = engine.sign();
354         DERConstructedSequence sigSeq = new DERConstructedSequence();
355 
356         // Begin constructing certificateIdentifier
357         // (AuthorityKeyIdentifier) sequence
358 
359         DERConstructedSequence ci = new DERConstructedSequence();
360 
361         // keyIdentifier left out from sequence, not required in MHP
362 
363         // authorityCertIssuer
364         X509Name nm = (X509Name)leafCert.getIssuerDN();
365         GeneralName gn = new GeneralName(nm);
366         ci.addObject(new DERTaggedObject(1, gn));
367 
368         // authorityCertSerialNumber
369         DERInteger ser = new DERInteger(leafCert.getSerialNumber());
370         ci.addObject(new DERTaggedObject(2, ser));
371 
372         // cerfiticateIdentifier sequence ready, add to signature sequence
373         AuthorityKeyIdentifier aki = new AuthorityKeyIdentifier(ci);
374         sigSeq.addObject(aki);
375 
376         // Add hashSignatureAlgorithm to signature sequence
377         sigSeq.addObject(new DERObjectIdentifier("1.2.840.113549.2.5"));
378 
379         // Add signatureValue to signature sequence
380         sigSeq.addObject(new DERBitString(signature));
381 
382         // Signature sequence ready, write to file
383         File f = new File(dest == null ? src : dest, "dvb.signaturefile.1");
384         os = new BufferedOutputStream(new FileOutputStream(f));
385         DEROutputStream dos = new DEROutputStream(os);
386         dos.writeObject(sigSeq);
387         dos.close();
388         
389         // Delete the generated certificate file from the source directory
390         if (dest != null && !certFile.delete()) {
391           System.err.println(
392             "Could not delete certificate file in source directory: "
393               + certFile.getName());
394         } 
395         
396         System.out.println();
397         System.out.println("Done!");
398         System.out.println();
399     }
400 }