security/nss/cmd/smimetools/smime

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rwxr-xr-x

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 #!/usr/local/bin/perl
     3 # This Source Code Form is subject to the terms of the Mozilla Public
     4 # License, v. 2.0. If a copy of the MPL was not distributed with this
     5 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
     7 #
     8 # smime.pl - frontend for S/MIME message generation and parsing
     9 #
    11 use Getopt::Std;
    13 @boundarychars = ( "0" .. "9", "A" .. "F" );
    15 # path to cmsutil
    16 $cmsutilpath = "cmsutil";
    18 #
    19 # Thanks to Gisle Aas <gisle@aas.no> for the base64 functions
    20 # originally taken from MIME-Base64-2.11 at www.cpan.org
    21 #
    22 sub encode_base64($)
    23 {
    24     my $res = "";
    25     pos($_[0]) = 0;                          # ensure start at the beginning
    26     while ($_[0] =~ /(.{1,45})/gs) {
    27 	$res .= substr(pack('u', $1), 1);    # get rid of length byte after packing
    28 	chop($res);
    29     }
    30     $res =~ tr|` -_|AA-Za-z0-9+/|;
    31     # fix padding at the end
    32     my $padding = (3 - length($_[0]) % 3) % 3;
    33     $res =~ s/.{$padding}$/'=' x $padding/e if $padding;
    34     # break encoded string into lines of no more than 76 characters each
    35     $res =~ s/(.{1,76})/$1\n/g;
    36     $res;
    37 }
    39 sub decode_base64($)
    40 {
    41     local($^W) = 0; # unpack("u",...) gives bogus warning in 5.00[123]
    43     my $str = shift;
    44     my $res = "";
    46     $str =~ tr|A-Za-z0-9+=/||cd;            # remove non-base64 chars
    47     if (length($str) % 4) {
    48 	require Carp;
    49 	Carp::carp("Length of base64 data not a multiple of 4")
    50     }
    51     $str =~ s/=+$//;                        # remove padding
    52     $str =~ tr|A-Za-z0-9+/| -_|;            # convert to uuencoded format
    53     while ($str =~ /(.{1,60})/gs) {
    54 	my $len = chr(32 + length($1)*3/4); # compute length byte
    55 	$res .= unpack("u", $len . $1 );    # uudecode
    56     }
    57     $res;
    58 }
    60 #
    61 # parse headers into a hash
    62 #
    63 # %headers = parseheaders($headertext);
    64 #
    65 sub parseheaders($)
    66 {
    67     my ($headerdata) = @_;
    68     my $hdr;
    69     my %hdrhash;
    70     my $hdrname;
    71     my $hdrvalue;
    72     my @hdrvalues;
    73     my $subhdrname;
    74     my $subhdrvalue;
    76     # the expression in split() correctly handles continuation lines
    77     foreach $hdr (split(/\n(?=\S)/, $headerdata)) {
    78 	$hdr =~ s/\r*\n\s+/ /g;	# collapse continuation lines
    79 	($hdrname, $hdrvalue) = $hdr =~ m/^(\S+):\s+(.*)$/;
    81 	# ignore non-headers (or should we die horribly?)
    82 	next unless (defined($hdrname));
    83 	$hdrname =~ tr/A-Z/a-z/;			# lowercase the header name
    84 	@hdrvalues = split(/\s*;\s*/, $hdrvalue);	# split header values (XXXX quoting)
    86 	# there is guaranteed to be at least one value
    87 	$hdrvalue = shift @hdrvalues;
    88 	if ($hdrvalue =~ /^\s*\"(.*)\"\s*$/) {		# strip quotes if there
    89 	    $hdrvalue = $1;
    90 	}
    92 	$hdrhash{$hdrname}{MAIN} = $hdrvalue;
    93 	# print "XXX $hdrname = $hdrvalue\n";
    95 	# deal with additional name-value pairs
    96 	foreach $hdrvalue (@hdrvalues) {
    97 	    ($subhdrname, $subhdrvalue) = $hdrvalue =~ m/^(\S+)\s*=\s*(.*)$/;
    98 	    # ignore non-name-value pairs (or should we die?)
    99 	    next unless (defined($subhdrname));
   100 	    $subhdrname =~ tr/A-Z/a-z/;
   101 	    if ($subhdrvalue =~ /^\s*\"(.*)\"\s*$/) {	# strip quotes if there
   102 		$subhdrvalue = $1;
   103 	    }
   104 	    $hdrhash{$hdrname}{$subhdrname} = $subhdrvalue;
   105 	}
   107     }
   108     return %hdrhash;
   109 }
   111 #
   112 # encryptentity($entity, $options) - encrypt an S/MIME entity,
   113 #                                    creating a new application/pkcs7-smime entity
   114 #
   115 # entity  - string containing entire S/MIME entity to encrypt
   116 # options - options for cmsutil
   117 #
   118 # this will generate and return a new application/pkcs7-smime entity containing
   119 # the enveloped input entity.
   120 #
   121 sub encryptentity($$)
   122 {
   123     my ($entity, $cmsutiloptions) = @_;
   124     my $out = "";
   125     my $boundary;
   127     $tmpencfile = "/tmp/encryptentity.$$";
   129     #
   130     # generate a random boundary string
   131     #
   132     $boundary = "------------ms" . join("", @boundarychars[map{rand @boundarychars }( 1 .. 24 )]);
   134     #
   135     # tell cmsutil to generate a enveloped CMS message using our data
   136     #
   137     open(CMS, "|$cmsutilpath -E $cmsutiloptions -o $tmpencfile") or die "ERROR: cannot pipe to cmsutil";
   138     print CMS $entity;
   139     unless (close(CMS)) {
   140 	print STDERR "ERROR: encryption failed.\n";
   141 	unlink($tmpsigfile);
   142 	exit 1;
   143     }
   145     $out  = "Content-Type: application/pkcs7-mime; smime-type=enveloped-data; name=smime.p7m\n";
   146     $out .= "Content-Transfer-Encoding: base64\n";
   147     $out .= "Content-Disposition: attachment; filename=smime.p7m\n";
   148     $out .= "\n";			# end of entity header
   150     open (ENC, $tmpencfile) or die "ERROR: cannot find newly generated encrypted content";
   151     local($/) = undef;			# slurp whole file
   152     $out .= encode_base64(<ENC>), "\n";	# entity body is base64-encoded CMS message
   153     close(ENC);
   155     unlink($tmpencfile);
   157     $out;
   158 }
   160 #
   161 # signentity($entity, $options) - sign an S/MIME entity
   162 #
   163 # entity  - string containing entire S/MIME entity to sign
   164 # options - options for cmsutil
   165 #
   166 # this will generate and return a new multipart/signed entity consisting
   167 # of the canonicalized original content, plus a signature block.
   168 #
   169 sub signentity($$)
   170 {
   171     my ($entity, $cmsutiloptions) = @_;
   172     my $out = "";
   173     my $boundary;
   175     $tmpsigfile = "/tmp/signentity.$$";
   177     #
   178     # generate a random boundary string
   179     #
   180     $boundary = "------------ms" . join("", @boundarychars[map{rand @boundarychars }( 1 .. 24 )]);
   182     #
   183     # tell cmsutil to generate a signed CMS message using the canonicalized data
   184     # The signedData has detached content (-T) and includes a signing time attribute (-G)
   185     #
   186     # if we do not provide a password on the command line, here's where we would be asked for it
   187     #
   188     open(CMS, "|$cmsutilpath -S -T -G $cmsutiloptions -o $tmpsigfile") or die "ERROR: cannot pipe to cmsutil";
   189     print CMS $entity;
   190     unless (close(CMS)) {
   191 	print STDERR "ERROR: signature generation failed.\n";
   192 	unlink($tmpsigfile);
   193 	exit 1;
   194     }
   196     open (SIG, $tmpsigfile) or die "ERROR: cannot find newly generated signature";
   198     #
   199     # construct a new multipart/signed MIME entity consisting of the original content and
   200     # the signature
   201     #
   202     # (we assume that cmsutil generates a SHA1 digest)
   203     $out .= "Content-Type: multipart/signed; protocol=\"application/pkcs7-signature\"; micalg=sha1; boundary=\"${boundary}\"\n";
   204     $out .= "\n";		# end of entity header
   205     $out .= "This is a cryptographically signed message in MIME format.\n"; # explanatory comment
   206     $out .= "\n--${boundary}\n";
   207     $out .= $entity;
   208     $out .= "\n--${boundary}\n";
   209     $out .= "Content-Type: application/pkcs7-signature; name=smime.p7s\n";
   210     $out .= "Content-Transfer-Encoding: base64\n";
   211     $out .= "Content-Disposition: attachment; filename=smime.p7s\n";
   212     $out .= "Content-Description: S/MIME Cryptographic Signature\n";
   213     $out .= "\n";		# end of signature subentity header
   215     local($/) = undef;		# slurp whole file
   216     $out .= encode_base64(<SIG>);	# append base64-encoded signature
   217     $out .= "\n--${boundary}--\n";
   219     close(SIG);
   220     unlink($tmpsigfile);
   222     $out;
   223 }
   225 sub usage {
   226     print STDERR "usage: smime [options]\n";
   227     print STDERR " options:\n";
   228     print STDERR " -S nick             generate signed message, use certificate named \"nick\"\n";
   229     print STDERR "  -p passwd          use \"passwd\" as security module password\n";
   230     print STDERR " -E rec1[,rec2...]   generate encrypted message for recipients\n";
   231     print STDERR " -D                  decode a S/MIME message\n";
   232     print STDERR "  -p passwd          use \"passwd\" as security module password\n";
   233     print STDERR "                     (required for decrypting only)\n";
   234     print STDERR " -C pathname         set pathname of \"cmsutil\"\n";
   235     print STDERR " -d directory        set directory containing certificate db\n";
   236     print STDERR "                     (default: ~/.netscape)\n";
   237     print STDERR "\nWith -S or -E, smime will take a regular RFC822 message or MIME entity\n";
   238     print STDERR "on stdin and generate a signed or encrypted S/MIME message with the same\n";
   239     print STDERR "headers and content from it. The output can be used as input to a MTA.\n";
   240     print STDERR "-D causes smime to strip off all S/MIME layers if possible and output\n";
   241     print STDERR "the \"inner\" message.\n";
   242 }
   244 #
   245 # start of main procedures
   246 #
   248 #
   249 # process command line options
   250 #
   251 unless (getopts('S:E:p:d:C:D')) {
   252     usage();
   253     exit 1;
   254 }
   256 unless (defined($opt_S) or defined($opt_E) or defined($opt_D)) {
   257     print STDERR "ERROR: -S and/or -E, or -D must be specified.\n";
   258     usage();
   259     exit 1;
   260 }
   262 $signopts = "";
   263 $encryptopts = "";
   264 $decodeopts = "";
   266 # pass -d option along
   267 if (defined($opt_d)) {
   268     $signopts .= "-d \"$opt_d\" ";
   269     $encryptopts .= "-d \"$opt_d\" ";
   270     $decodeopts .= "-d \"$opt_d\" ";
   271 }
   273 if (defined($opt_S)) {
   274     $signopts .= "-N \"$opt_S\" ";
   275 }
   277 if (defined($opt_p)) {
   278     $signopts .= "-p \"$opt_p\" ";
   279     $decodeopts .= "-p \"$opt_p\" ";
   280 }
   282 if (defined($opt_E)) {
   283     @recipients = split(",", $opt_E);
   284     $encryptopts .= "-r ";
   285     $encryptopts .= join (" -r ", @recipients);
   286 }
   288 if (defined($opt_C)) {
   289     $cmsutilpath = $opt_C;
   290 }
   292 #
   293 # split headers into mime entity headers and RFC822 headers
   294 # The RFC822 headers are preserved and stay on the outer layer of the message
   295 #
   296 $rfc822headers = "";
   297 $mimeheaders = "";
   298 $mimebody = "";
   299 $skippedheaders = "";
   300 while (<STDIN>) {
   301     last if (/^$/);
   302     if (/^content-\S+: /i) {
   303 	$lastref = \$mimeheaders;
   304     } elsif (/^mime-version: /i) {
   305 	$lastref = \$skippedheaders;			# skip it
   306     } elsif (/^\s/) {
   307 	;
   308     } else {
   309 	$lastref = \$rfc822headers;
   310     }
   311     $$lastref .= $_;
   312 }
   314 #
   315 # if there are no MIME entity headers, generate some default ones
   316 #
   317 if ($mimeheaders eq "") {
   318     $mimeheaders .= "Content-Type: text/plain; charset=us-ascii\n";
   319     $mimeheaders .= "Content-Transfer-Encoding: 7bit\n";
   320 }
   322 #
   323 # slurp in the entity body
   324 #
   325 $saveRS = $/;
   326 $/ = undef;
   327 $mimebody = <STDIN>;
   328 $/ = $saveRS;
   329 chomp($mimebody);
   331 if (defined $opt_D) {
   332     #
   333     # decode
   334     #
   335     # possible options would be:
   336     # - strip off only one layer
   337     # - strip off outer signature (if present)
   338     # - just print information about the structure of the message
   339     # - strip n layers, then dump DER of CMS message
   341     $layercounter = 1;
   343     while (1) {
   344 	%hdrhash = parseheaders($mimeheaders);
   345 	unless (exists($hdrhash{"content-type"}{MAIN})) {
   346 	    print STDERR "ERROR: no content type header found in MIME entity\n";
   347 	    last;	# no content-type - we're done
   348 	}
   350 	$contenttype = $hdrhash{"content-type"}{MAIN};
   351 	if ($contenttype eq "application/pkcs7-mime") {
   352 	    #
   353 	    # opaque-signed or enveloped message
   354 	    #
   355 	    unless (exists($hdrhash{"content-type"}{"smime-type"})) {
   356 		print STDERR "ERROR: no smime-type attribute in application/pkcs7-smime entity.\n";
   357 		last;
   358 	    }
   359 	    $smimetype = $hdrhash{"content-type"}{"smime-type"};
   360 	    if ($smimetype eq "signed-data" or $smimetype eq "enveloped-data") {
   361 		# it's verification or decryption time!
   363 		# can handle only base64 encoding for now
   364 		# all other encodings are treated as binary (8bit)
   365 		if ($hdrhash{"content-transfer-encoding"}{MAIN} eq "base64") {
   366 		    $mimebody = decode_base64($mimebody);
   367 		}
   369 		# if we need to dump the DER, we would do it right here
   371 		# now write the DER
   372 		$tmpderfile = "/tmp/der.$$";
   373 		open(TMP, ">$tmpderfile") or die "ERROR: cannot write signature data to temporary file";
   374 		print TMP $mimebody;
   375 		unless (close(TMP)) {
   376 		    print STDERR "ERROR: writing signature data to temporary file.\n";
   377 		    unlink($tmpderfile);
   378 		    exit 1;
   379 		}
   381 		$mimeheaders = "";
   382 		open(TMP, "$cmsutilpath -D $decodeopts -h $layercounter -i $tmpderfile |") or die "ERROR: cannot open pipe to cmsutil";
   383 		$layercounter++;
   384 		while (<TMP>) {
   385 		    last if (/^\r?$/);			# empty lines mark end of header
   386 		    if (/^SMIME: /) {			# add all SMIME info to the rfc822 hdrs
   387 			$lastref = \$rfc822headers;
   388 		    } elsif (/^\s/) {
   389 			;				# continuation lines go to the last dest
   390 		    } else {
   391 			$lastref = \$mimeheaders;	# all other headers are mime headers
   392 		    }
   393 		    $$lastref .= $_;
   394 		}
   395 		# slurp in rest of the data to $mimebody
   396 		$saveRS = $/; $/ = undef; $mimebody = <TMP>; $/ = $saveRS;
   397 		close(TMP);
   399 		unlink($tmpderfile);
   401 	    } else {
   402 		print STDERR "ERROR: unknown smime-type \"$smimetype\" in application/pkcs7-smime entity.\n";
   403 		last;
   404 	    }
   405 	} elsif ($contenttype eq "multipart/signed") {
   406 	    #
   407 	    # clear signed message
   408 	    #
   409 	    unless (exists($hdrhash{"content-type"}{"protocol"})) {
   410 		print STDERR "ERROR: content type has no protocol attribute in multipart/signed entity.\n";
   411 		last;
   412 	    }
   413 	    if ($hdrhash{"content-type"}{"protocol"} ne "application/pkcs7-signature") {
   414 		# we cannot handle this guy
   415 		print STDERR "ERROR: unknown protocol \"", $hdrhash{"content-type"}{"protocol"},
   416 			"\" in multipart/signed entity.\n";
   417 		last;
   418 	    }
   419 	    unless (exists($hdrhash{"content-type"}{"boundary"})) {
   420 		print STDERR "ERROR: no boundary attribute in multipart/signed entity.\n";
   421 		last;
   422 	    }
   423 	    $boundary = $hdrhash{"content-type"}{"boundary"};
   425 	    # split $mimebody along \n--$boundary\n - gets you four parts
   426 	    # first (0), any comments the sending agent might have put in
   427 	    # second (1), the message itself
   428 	    # third (2), the signature as a mime entity
   429 	    # fourth (3), trailing data (there shouldn't be any)
   431 	    @multiparts = split(/\r?\n--$boundary(?:--)?\r?\n/, $mimebody);
   433 	    #
   434 	    # parse the signature headers
   435 	    ($submimeheaders, $submimebody) = split(/^$/m, $multiparts[2]);
   436 	    %sighdrhash = parseheaders($submimeheaders);
   437 	    unless (exists($sighdrhash{"content-type"}{MAIN})) {
   438 		print STDERR "ERROR: signature entity has no content type.\n";
   439 		last;
   440 	    }
   441 	    if ($sighdrhash{"content-type"}{MAIN} ne "application/pkcs7-signature") {
   442 		# we cannot handle this guy
   443 		print STDERR "ERROR: unknown content type \"", $sighdrhash{"content-type"}{MAIN},
   444 			"\" in signature entity.\n";
   445 		last;
   446 	    }
   447 	    if ($sighdrhash{"content-transfer-encoding"}{MAIN} eq "base64") {
   448 		$submimebody = decode_base64($submimebody);
   449 	    }
   451 	    # we would dump the DER at this point
   453 	    $tmpsigfile = "/tmp/sig.$$";
   454 	    open(TMP, ">$tmpsigfile") or die "ERROR: cannot write signature data to temporary file";
   455 	    print TMP $submimebody;
   456 	    unless (close(TMP)) {
   457 		print STDERR "ERROR: writing signature data to temporary file.\n";
   458 		unlink($tmpsigfile);
   459 		exit 1;
   460 	    }
   462 	    $tmpmsgfile = "/tmp/msg.$$";
   463 	    open(TMP, ">$tmpmsgfile") or die "ERROR: cannot write message data to temporary file";
   464 	    print TMP $multiparts[1];
   465 	    unless (close(TMP)) {
   466 		print STDERR "ERROR: writing message data to temporary file.\n";
   467 		unlink($tmpsigfile);
   468 		unlink($tmpmsgfile);
   469 		exit 1;
   470 	    }
   472 	    $mimeheaders = "";
   473 	    open(TMP, "$cmsutilpath -D $decodeopts -h $layercounter -c $tmpmsgfile -i $tmpsigfile |") or die "ERROR: cannot open pipe to cmsutil";
   474 	    $layercounter++;
   475 	    while (<TMP>) {
   476 		last if (/^\r?$/);
   477 		if (/^SMIME: /) {
   478 		    $lastref = \$rfc822headers;
   479 		} elsif (/^\s/) {
   480 		    ;
   481 		} else {
   482 		    $lastref = \$mimeheaders;
   483 		}
   484 		$$lastref .= $_;
   485 	    }
   486 	    $saveRS = $/; $/ = undef; $mimebody = <TMP>; $/ = $saveRS;
   487 	    close(TMP);
   488 	    unlink($tmpsigfile);
   489 	    unlink($tmpmsgfile);
   491 	} else {
   493 	    # not a content type we know - we're done
   494 	    last;
   496 	}
   497     }
   499     # so now we have the S/MIME parsing information in rfc822headers
   500     # and the first mime entity we could not handle in mimeheaders and mimebody.
   501     # dump 'em out and we're done.
   502     print $rfc822headers;
   503     print $mimeheaders . "\n" . $mimebody;
   505 } else {
   507     #
   508     # encode (which is much easier than decode)
   509     #
   511     $mimeentity = $mimeheaders . "\n" . $mimebody;
   513     #
   514     # canonicalize inner entity (rudimentary yet)
   515     # convert single LFs to CRLF
   516     # if no Content-Transfer-Encoding header present:
   517     #  if 8 bit chars present, use Content-Transfer-Encoding: quoted-printable
   518     #  otherwise, use Content-Transfer-Encoding: 7bit
   519     #
   520     $mimeentity =~ s/\r*\n/\r\n/mg;
   522     #
   523     # now do the wrapping
   524     # we sign first, then encrypt because that's what Communicator needs
   525     #
   526     if (defined($opt_S)) {
   527 	$mimeentity = signentity($mimeentity, $signopts);
   528     }
   530     if (defined($opt_E)) {
   531 	$mimeentity = encryptentity($mimeentity, $encryptopts);	
   532     }
   534     #
   535     # XXX sign again to do triple wrapping (RFC2634)
   536     #
   538     #
   539     # now write out the RFC822 headers
   540     # followed by the final $mimeentity
   541     #
   542     print $rfc822headers;
   543     print "MIME-Version: 1.0 (NSS SMIME - http://www.mozilla.org/projects/security)\n";	# set up the flag
   544     print $mimeentity;
   545 }
   547 exit 0;

mercurial