1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/security/nss/cmd/smimetools/smime Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,547 @@ 1.4 +#!/usr/local/bin/perl 1.5 + 1.6 +# This Source Code Form is subject to the terms of the Mozilla Public 1.7 +# License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 +# file, You can obtain one at http://mozilla.org/MPL/2.0/. 1.9 + 1.10 +# 1.11 +# smime.pl - frontend for S/MIME message generation and parsing 1.12 +# 1.13 + 1.14 +use Getopt::Std; 1.15 + 1.16 +@boundarychars = ( "0" .. "9", "A" .. "F" ); 1.17 + 1.18 +# path to cmsutil 1.19 +$cmsutilpath = "cmsutil"; 1.20 + 1.21 +# 1.22 +# Thanks to Gisle Aas <gisle@aas.no> for the base64 functions 1.23 +# originally taken from MIME-Base64-2.11 at www.cpan.org 1.24 +# 1.25 +sub encode_base64($) 1.26 +{ 1.27 + my $res = ""; 1.28 + pos($_[0]) = 0; # ensure start at the beginning 1.29 + while ($_[0] =~ /(.{1,45})/gs) { 1.30 + $res .= substr(pack('u', $1), 1); # get rid of length byte after packing 1.31 + chop($res); 1.32 + } 1.33 + $res =~ tr|` -_|AA-Za-z0-9+/|; 1.34 + # fix padding at the end 1.35 + my $padding = (3 - length($_[0]) % 3) % 3; 1.36 + $res =~ s/.{$padding}$/'=' x $padding/e if $padding; 1.37 + # break encoded string into lines of no more than 76 characters each 1.38 + $res =~ s/(.{1,76})/$1\n/g; 1.39 + $res; 1.40 +} 1.41 + 1.42 +sub decode_base64($) 1.43 +{ 1.44 + local($^W) = 0; # unpack("u",...) gives bogus warning in 5.00[123] 1.45 + 1.46 + my $str = shift; 1.47 + my $res = ""; 1.48 + 1.49 + $str =~ tr|A-Za-z0-9+=/||cd; # remove non-base64 chars 1.50 + if (length($str) % 4) { 1.51 + require Carp; 1.52 + Carp::carp("Length of base64 data not a multiple of 4") 1.53 + } 1.54 + $str =~ s/=+$//; # remove padding 1.55 + $str =~ tr|A-Za-z0-9+/| -_|; # convert to uuencoded format 1.56 + while ($str =~ /(.{1,60})/gs) { 1.57 + my $len = chr(32 + length($1)*3/4); # compute length byte 1.58 + $res .= unpack("u", $len . $1 ); # uudecode 1.59 + } 1.60 + $res; 1.61 +} 1.62 + 1.63 +# 1.64 +# parse headers into a hash 1.65 +# 1.66 +# %headers = parseheaders($headertext); 1.67 +# 1.68 +sub parseheaders($) 1.69 +{ 1.70 + my ($headerdata) = @_; 1.71 + my $hdr; 1.72 + my %hdrhash; 1.73 + my $hdrname; 1.74 + my $hdrvalue; 1.75 + my @hdrvalues; 1.76 + my $subhdrname; 1.77 + my $subhdrvalue; 1.78 + 1.79 + # the expression in split() correctly handles continuation lines 1.80 + foreach $hdr (split(/\n(?=\S)/, $headerdata)) { 1.81 + $hdr =~ s/\r*\n\s+/ /g; # collapse continuation lines 1.82 + ($hdrname, $hdrvalue) = $hdr =~ m/^(\S+):\s+(.*)$/; 1.83 + 1.84 + # ignore non-headers (or should we die horribly?) 1.85 + next unless (defined($hdrname)); 1.86 + $hdrname =~ tr/A-Z/a-z/; # lowercase the header name 1.87 + @hdrvalues = split(/\s*;\s*/, $hdrvalue); # split header values (XXXX quoting) 1.88 + 1.89 + # there is guaranteed to be at least one value 1.90 + $hdrvalue = shift @hdrvalues; 1.91 + if ($hdrvalue =~ /^\s*\"(.*)\"\s*$/) { # strip quotes if there 1.92 + $hdrvalue = $1; 1.93 + } 1.94 + 1.95 + $hdrhash{$hdrname}{MAIN} = $hdrvalue; 1.96 + # print "XXX $hdrname = $hdrvalue\n"; 1.97 + 1.98 + # deal with additional name-value pairs 1.99 + foreach $hdrvalue (@hdrvalues) { 1.100 + ($subhdrname, $subhdrvalue) = $hdrvalue =~ m/^(\S+)\s*=\s*(.*)$/; 1.101 + # ignore non-name-value pairs (or should we die?) 1.102 + next unless (defined($subhdrname)); 1.103 + $subhdrname =~ tr/A-Z/a-z/; 1.104 + if ($subhdrvalue =~ /^\s*\"(.*)\"\s*$/) { # strip quotes if there 1.105 + $subhdrvalue = $1; 1.106 + } 1.107 + $hdrhash{$hdrname}{$subhdrname} = $subhdrvalue; 1.108 + } 1.109 + 1.110 + } 1.111 + return %hdrhash; 1.112 +} 1.113 + 1.114 +# 1.115 +# encryptentity($entity, $options) - encrypt an S/MIME entity, 1.116 +# creating a new application/pkcs7-smime entity 1.117 +# 1.118 +# entity - string containing entire S/MIME entity to encrypt 1.119 +# options - options for cmsutil 1.120 +# 1.121 +# this will generate and return a new application/pkcs7-smime entity containing 1.122 +# the enveloped input entity. 1.123 +# 1.124 +sub encryptentity($$) 1.125 +{ 1.126 + my ($entity, $cmsutiloptions) = @_; 1.127 + my $out = ""; 1.128 + my $boundary; 1.129 + 1.130 + $tmpencfile = "/tmp/encryptentity.$$"; 1.131 + 1.132 + # 1.133 + # generate a random boundary string 1.134 + # 1.135 + $boundary = "------------ms" . join("", @boundarychars[map{rand @boundarychars }( 1 .. 24 )]); 1.136 + 1.137 + # 1.138 + # tell cmsutil to generate a enveloped CMS message using our data 1.139 + # 1.140 + open(CMS, "|$cmsutilpath -E $cmsutiloptions -o $tmpencfile") or die "ERROR: cannot pipe to cmsutil"; 1.141 + print CMS $entity; 1.142 + unless (close(CMS)) { 1.143 + print STDERR "ERROR: encryption failed.\n"; 1.144 + unlink($tmpsigfile); 1.145 + exit 1; 1.146 + } 1.147 + 1.148 + $out = "Content-Type: application/pkcs7-mime; smime-type=enveloped-data; name=smime.p7m\n"; 1.149 + $out .= "Content-Transfer-Encoding: base64\n"; 1.150 + $out .= "Content-Disposition: attachment; filename=smime.p7m\n"; 1.151 + $out .= "\n"; # end of entity header 1.152 + 1.153 + open (ENC, $tmpencfile) or die "ERROR: cannot find newly generated encrypted content"; 1.154 + local($/) = undef; # slurp whole file 1.155 + $out .= encode_base64(<ENC>), "\n"; # entity body is base64-encoded CMS message 1.156 + close(ENC); 1.157 + 1.158 + unlink($tmpencfile); 1.159 + 1.160 + $out; 1.161 +} 1.162 + 1.163 +# 1.164 +# signentity($entity, $options) - sign an S/MIME entity 1.165 +# 1.166 +# entity - string containing entire S/MIME entity to sign 1.167 +# options - options for cmsutil 1.168 +# 1.169 +# this will generate and return a new multipart/signed entity consisting 1.170 +# of the canonicalized original content, plus a signature block. 1.171 +# 1.172 +sub signentity($$) 1.173 +{ 1.174 + my ($entity, $cmsutiloptions) = @_; 1.175 + my $out = ""; 1.176 + my $boundary; 1.177 + 1.178 + $tmpsigfile = "/tmp/signentity.$$"; 1.179 + 1.180 + # 1.181 + # generate a random boundary string 1.182 + # 1.183 + $boundary = "------------ms" . join("", @boundarychars[map{rand @boundarychars }( 1 .. 24 )]); 1.184 + 1.185 + # 1.186 + # tell cmsutil to generate a signed CMS message using the canonicalized data 1.187 + # The signedData has detached content (-T) and includes a signing time attribute (-G) 1.188 + # 1.189 + # if we do not provide a password on the command line, here's where we would be asked for it 1.190 + # 1.191 + open(CMS, "|$cmsutilpath -S -T -G $cmsutiloptions -o $tmpsigfile") or die "ERROR: cannot pipe to cmsutil"; 1.192 + print CMS $entity; 1.193 + unless (close(CMS)) { 1.194 + print STDERR "ERROR: signature generation failed.\n"; 1.195 + unlink($tmpsigfile); 1.196 + exit 1; 1.197 + } 1.198 + 1.199 + open (SIG, $tmpsigfile) or die "ERROR: cannot find newly generated signature"; 1.200 + 1.201 + # 1.202 + # construct a new multipart/signed MIME entity consisting of the original content and 1.203 + # the signature 1.204 + # 1.205 + # (we assume that cmsutil generates a SHA1 digest) 1.206 + $out .= "Content-Type: multipart/signed; protocol=\"application/pkcs7-signature\"; micalg=sha1; boundary=\"${boundary}\"\n"; 1.207 + $out .= "\n"; # end of entity header 1.208 + $out .= "This is a cryptographically signed message in MIME format.\n"; # explanatory comment 1.209 + $out .= "\n--${boundary}\n"; 1.210 + $out .= $entity; 1.211 + $out .= "\n--${boundary}\n"; 1.212 + $out .= "Content-Type: application/pkcs7-signature; name=smime.p7s\n"; 1.213 + $out .= "Content-Transfer-Encoding: base64\n"; 1.214 + $out .= "Content-Disposition: attachment; filename=smime.p7s\n"; 1.215 + $out .= "Content-Description: S/MIME Cryptographic Signature\n"; 1.216 + $out .= "\n"; # end of signature subentity header 1.217 + 1.218 + local($/) = undef; # slurp whole file 1.219 + $out .= encode_base64(<SIG>); # append base64-encoded signature 1.220 + $out .= "\n--${boundary}--\n"; 1.221 + 1.222 + close(SIG); 1.223 + unlink($tmpsigfile); 1.224 + 1.225 + $out; 1.226 +} 1.227 + 1.228 +sub usage { 1.229 + print STDERR "usage: smime [options]\n"; 1.230 + print STDERR " options:\n"; 1.231 + print STDERR " -S nick generate signed message, use certificate named \"nick\"\n"; 1.232 + print STDERR " -p passwd use \"passwd\" as security module password\n"; 1.233 + print STDERR " -E rec1[,rec2...] generate encrypted message for recipients\n"; 1.234 + print STDERR " -D decode a S/MIME message\n"; 1.235 + print STDERR " -p passwd use \"passwd\" as security module password\n"; 1.236 + print STDERR " (required for decrypting only)\n"; 1.237 + print STDERR " -C pathname set pathname of \"cmsutil\"\n"; 1.238 + print STDERR " -d directory set directory containing certificate db\n"; 1.239 + print STDERR " (default: ~/.netscape)\n"; 1.240 + print STDERR "\nWith -S or -E, smime will take a regular RFC822 message or MIME entity\n"; 1.241 + print STDERR "on stdin and generate a signed or encrypted S/MIME message with the same\n"; 1.242 + print STDERR "headers and content from it. The output can be used as input to a MTA.\n"; 1.243 + print STDERR "-D causes smime to strip off all S/MIME layers if possible and output\n"; 1.244 + print STDERR "the \"inner\" message.\n"; 1.245 +} 1.246 + 1.247 +# 1.248 +# start of main procedures 1.249 +# 1.250 + 1.251 +# 1.252 +# process command line options 1.253 +# 1.254 +unless (getopts('S:E:p:d:C:D')) { 1.255 + usage(); 1.256 + exit 1; 1.257 +} 1.258 + 1.259 +unless (defined($opt_S) or defined($opt_E) or defined($opt_D)) { 1.260 + print STDERR "ERROR: -S and/or -E, or -D must be specified.\n"; 1.261 + usage(); 1.262 + exit 1; 1.263 +} 1.264 + 1.265 +$signopts = ""; 1.266 +$encryptopts = ""; 1.267 +$decodeopts = ""; 1.268 + 1.269 +# pass -d option along 1.270 +if (defined($opt_d)) { 1.271 + $signopts .= "-d \"$opt_d\" "; 1.272 + $encryptopts .= "-d \"$opt_d\" "; 1.273 + $decodeopts .= "-d \"$opt_d\" "; 1.274 +} 1.275 + 1.276 +if (defined($opt_S)) { 1.277 + $signopts .= "-N \"$opt_S\" "; 1.278 +} 1.279 + 1.280 +if (defined($opt_p)) { 1.281 + $signopts .= "-p \"$opt_p\" "; 1.282 + $decodeopts .= "-p \"$opt_p\" "; 1.283 +} 1.284 + 1.285 +if (defined($opt_E)) { 1.286 + @recipients = split(",", $opt_E); 1.287 + $encryptopts .= "-r "; 1.288 + $encryptopts .= join (" -r ", @recipients); 1.289 +} 1.290 + 1.291 +if (defined($opt_C)) { 1.292 + $cmsutilpath = $opt_C; 1.293 +} 1.294 + 1.295 +# 1.296 +# split headers into mime entity headers and RFC822 headers 1.297 +# The RFC822 headers are preserved and stay on the outer layer of the message 1.298 +# 1.299 +$rfc822headers = ""; 1.300 +$mimeheaders = ""; 1.301 +$mimebody = ""; 1.302 +$skippedheaders = ""; 1.303 +while (<STDIN>) { 1.304 + last if (/^$/); 1.305 + if (/^content-\S+: /i) { 1.306 + $lastref = \$mimeheaders; 1.307 + } elsif (/^mime-version: /i) { 1.308 + $lastref = \$skippedheaders; # skip it 1.309 + } elsif (/^\s/) { 1.310 + ; 1.311 + } else { 1.312 + $lastref = \$rfc822headers; 1.313 + } 1.314 + $$lastref .= $_; 1.315 +} 1.316 + 1.317 +# 1.318 +# if there are no MIME entity headers, generate some default ones 1.319 +# 1.320 +if ($mimeheaders eq "") { 1.321 + $mimeheaders .= "Content-Type: text/plain; charset=us-ascii\n"; 1.322 + $mimeheaders .= "Content-Transfer-Encoding: 7bit\n"; 1.323 +} 1.324 + 1.325 +# 1.326 +# slurp in the entity body 1.327 +# 1.328 +$saveRS = $/; 1.329 +$/ = undef; 1.330 +$mimebody = <STDIN>; 1.331 +$/ = $saveRS; 1.332 +chomp($mimebody); 1.333 + 1.334 +if (defined $opt_D) { 1.335 + # 1.336 + # decode 1.337 + # 1.338 + # possible options would be: 1.339 + # - strip off only one layer 1.340 + # - strip off outer signature (if present) 1.341 + # - just print information about the structure of the message 1.342 + # - strip n layers, then dump DER of CMS message 1.343 + 1.344 + $layercounter = 1; 1.345 + 1.346 + while (1) { 1.347 + %hdrhash = parseheaders($mimeheaders); 1.348 + unless (exists($hdrhash{"content-type"}{MAIN})) { 1.349 + print STDERR "ERROR: no content type header found in MIME entity\n"; 1.350 + last; # no content-type - we're done 1.351 + } 1.352 + 1.353 + $contenttype = $hdrhash{"content-type"}{MAIN}; 1.354 + if ($contenttype eq "application/pkcs7-mime") { 1.355 + # 1.356 + # opaque-signed or enveloped message 1.357 + # 1.358 + unless (exists($hdrhash{"content-type"}{"smime-type"})) { 1.359 + print STDERR "ERROR: no smime-type attribute in application/pkcs7-smime entity.\n"; 1.360 + last; 1.361 + } 1.362 + $smimetype = $hdrhash{"content-type"}{"smime-type"}; 1.363 + if ($smimetype eq "signed-data" or $smimetype eq "enveloped-data") { 1.364 + # it's verification or decryption time! 1.365 + 1.366 + # can handle only base64 encoding for now 1.367 + # all other encodings are treated as binary (8bit) 1.368 + if ($hdrhash{"content-transfer-encoding"}{MAIN} eq "base64") { 1.369 + $mimebody = decode_base64($mimebody); 1.370 + } 1.371 + 1.372 + # if we need to dump the DER, we would do it right here 1.373 + 1.374 + # now write the DER 1.375 + $tmpderfile = "/tmp/der.$$"; 1.376 + open(TMP, ">$tmpderfile") or die "ERROR: cannot write signature data to temporary file"; 1.377 + print TMP $mimebody; 1.378 + unless (close(TMP)) { 1.379 + print STDERR "ERROR: writing signature data to temporary file.\n"; 1.380 + unlink($tmpderfile); 1.381 + exit 1; 1.382 + } 1.383 + 1.384 + $mimeheaders = ""; 1.385 + open(TMP, "$cmsutilpath -D $decodeopts -h $layercounter -i $tmpderfile |") or die "ERROR: cannot open pipe to cmsutil"; 1.386 + $layercounter++; 1.387 + while (<TMP>) { 1.388 + last if (/^\r?$/); # empty lines mark end of header 1.389 + if (/^SMIME: /) { # add all SMIME info to the rfc822 hdrs 1.390 + $lastref = \$rfc822headers; 1.391 + } elsif (/^\s/) { 1.392 + ; # continuation lines go to the last dest 1.393 + } else { 1.394 + $lastref = \$mimeheaders; # all other headers are mime headers 1.395 + } 1.396 + $$lastref .= $_; 1.397 + } 1.398 + # slurp in rest of the data to $mimebody 1.399 + $saveRS = $/; $/ = undef; $mimebody = <TMP>; $/ = $saveRS; 1.400 + close(TMP); 1.401 + 1.402 + unlink($tmpderfile); 1.403 + 1.404 + } else { 1.405 + print STDERR "ERROR: unknown smime-type \"$smimetype\" in application/pkcs7-smime entity.\n"; 1.406 + last; 1.407 + } 1.408 + } elsif ($contenttype eq "multipart/signed") { 1.409 + # 1.410 + # clear signed message 1.411 + # 1.412 + unless (exists($hdrhash{"content-type"}{"protocol"})) { 1.413 + print STDERR "ERROR: content type has no protocol attribute in multipart/signed entity.\n"; 1.414 + last; 1.415 + } 1.416 + if ($hdrhash{"content-type"}{"protocol"} ne "application/pkcs7-signature") { 1.417 + # we cannot handle this guy 1.418 + print STDERR "ERROR: unknown protocol \"", $hdrhash{"content-type"}{"protocol"}, 1.419 + "\" in multipart/signed entity.\n"; 1.420 + last; 1.421 + } 1.422 + unless (exists($hdrhash{"content-type"}{"boundary"})) { 1.423 + print STDERR "ERROR: no boundary attribute in multipart/signed entity.\n"; 1.424 + last; 1.425 + } 1.426 + $boundary = $hdrhash{"content-type"}{"boundary"}; 1.427 + 1.428 + # split $mimebody along \n--$boundary\n - gets you four parts 1.429 + # first (0), any comments the sending agent might have put in 1.430 + # second (1), the message itself 1.431 + # third (2), the signature as a mime entity 1.432 + # fourth (3), trailing data (there shouldn't be any) 1.433 + 1.434 + @multiparts = split(/\r?\n--$boundary(?:--)?\r?\n/, $mimebody); 1.435 + 1.436 + # 1.437 + # parse the signature headers 1.438 + ($submimeheaders, $submimebody) = split(/^$/m, $multiparts[2]); 1.439 + %sighdrhash = parseheaders($submimeheaders); 1.440 + unless (exists($sighdrhash{"content-type"}{MAIN})) { 1.441 + print STDERR "ERROR: signature entity has no content type.\n"; 1.442 + last; 1.443 + } 1.444 + if ($sighdrhash{"content-type"}{MAIN} ne "application/pkcs7-signature") { 1.445 + # we cannot handle this guy 1.446 + print STDERR "ERROR: unknown content type \"", $sighdrhash{"content-type"}{MAIN}, 1.447 + "\" in signature entity.\n"; 1.448 + last; 1.449 + } 1.450 + if ($sighdrhash{"content-transfer-encoding"}{MAIN} eq "base64") { 1.451 + $submimebody = decode_base64($submimebody); 1.452 + } 1.453 + 1.454 + # we would dump the DER at this point 1.455 + 1.456 + $tmpsigfile = "/tmp/sig.$$"; 1.457 + open(TMP, ">$tmpsigfile") or die "ERROR: cannot write signature data to temporary file"; 1.458 + print TMP $submimebody; 1.459 + unless (close(TMP)) { 1.460 + print STDERR "ERROR: writing signature data to temporary file.\n"; 1.461 + unlink($tmpsigfile); 1.462 + exit 1; 1.463 + } 1.464 + 1.465 + $tmpmsgfile = "/tmp/msg.$$"; 1.466 + open(TMP, ">$tmpmsgfile") or die "ERROR: cannot write message data to temporary file"; 1.467 + print TMP $multiparts[1]; 1.468 + unless (close(TMP)) { 1.469 + print STDERR "ERROR: writing message data to temporary file.\n"; 1.470 + unlink($tmpsigfile); 1.471 + unlink($tmpmsgfile); 1.472 + exit 1; 1.473 + } 1.474 + 1.475 + $mimeheaders = ""; 1.476 + open(TMP, "$cmsutilpath -D $decodeopts -h $layercounter -c $tmpmsgfile -i $tmpsigfile |") or die "ERROR: cannot open pipe to cmsutil"; 1.477 + $layercounter++; 1.478 + while (<TMP>) { 1.479 + last if (/^\r?$/); 1.480 + if (/^SMIME: /) { 1.481 + $lastref = \$rfc822headers; 1.482 + } elsif (/^\s/) { 1.483 + ; 1.484 + } else { 1.485 + $lastref = \$mimeheaders; 1.486 + } 1.487 + $$lastref .= $_; 1.488 + } 1.489 + $saveRS = $/; $/ = undef; $mimebody = <TMP>; $/ = $saveRS; 1.490 + close(TMP); 1.491 + unlink($tmpsigfile); 1.492 + unlink($tmpmsgfile); 1.493 + 1.494 + } else { 1.495 + 1.496 + # not a content type we know - we're done 1.497 + last; 1.498 + 1.499 + } 1.500 + } 1.501 + 1.502 + # so now we have the S/MIME parsing information in rfc822headers 1.503 + # and the first mime entity we could not handle in mimeheaders and mimebody. 1.504 + # dump 'em out and we're done. 1.505 + print $rfc822headers; 1.506 + print $mimeheaders . "\n" . $mimebody; 1.507 + 1.508 +} else { 1.509 + 1.510 + # 1.511 + # encode (which is much easier than decode) 1.512 + # 1.513 + 1.514 + $mimeentity = $mimeheaders . "\n" . $mimebody; 1.515 + 1.516 + # 1.517 + # canonicalize inner entity (rudimentary yet) 1.518 + # convert single LFs to CRLF 1.519 + # if no Content-Transfer-Encoding header present: 1.520 + # if 8 bit chars present, use Content-Transfer-Encoding: quoted-printable 1.521 + # otherwise, use Content-Transfer-Encoding: 7bit 1.522 + # 1.523 + $mimeentity =~ s/\r*\n/\r\n/mg; 1.524 + 1.525 + # 1.526 + # now do the wrapping 1.527 + # we sign first, then encrypt because that's what Communicator needs 1.528 + # 1.529 + if (defined($opt_S)) { 1.530 + $mimeentity = signentity($mimeentity, $signopts); 1.531 + } 1.532 + 1.533 + if (defined($opt_E)) { 1.534 + $mimeentity = encryptentity($mimeentity, $encryptopts); 1.535 + } 1.536 + 1.537 + # 1.538 + # XXX sign again to do triple wrapping (RFC2634) 1.539 + # 1.540 + 1.541 + # 1.542 + # now write out the RFC822 headers 1.543 + # followed by the final $mimeentity 1.544 + # 1.545 + print $rfc822headers; 1.546 + print "MIME-Version: 1.0 (NSS SMIME - http://www.mozilla.org/projects/security)\n"; # set up the flag 1.547 + print $mimeentity; 1.548 +} 1.549 + 1.550 +exit 0;