Preliminary checkin at a state where smime can parse messages generated
authorchrisk%netscape.com
Fri, 23 Jun 2000 16:40:31 +0000
changeset 393 99d8f8ae4ac4b665f4192c7862cb0dcfea0e1bb0
parent 392 2e809c457ef640f0fa28b7ecaa726ebeee4faeb4
child 394 d053265c4463e9a817dd48484ae46aaacb36731d
push idunknown
push userunknown
push dateunknown
Preliminary checkin at a state where smime can parse messages generated by itself.
security/nss/cmd/smimetools/cmsutil.c
security/nss/cmd/smimetools/smime
--- a/security/nss/cmd/smimetools/cmsutil.c
+++ b/security/nss/cmd/smimetools/cmsutil.c
@@ -320,19 +320,28 @@ decode(FILE *out, FILE *infile, char *pr
 	default:
 	    break;
 	}
 	if (decodeOptions.headerLevel >= 0)
 	    fprintf(out, "\n");
     }
 
     if (!decodeOptions.suppressContent) {
-	/* XXX only if we do not have detached content... */
-	if ((item = NSS_CMSMessage_GetContent(cmsg)) != NULL) {
-	    fwrite(item->data, item->len, 1, out);
+	if (decodeOptions.contentFile) {
+	    char buffer[4096];
+	    size_t nbytes;
+	    /* detached content: print content file */
+	    fseek(decodeOptions.contentFile, 0, SEEK_SET);
+	    while ((nbytes = fread(buffer, 1, sizeof(buffer), decodeOptions.contentFile)) != 0) {
+		fwrite(buffer, nbytes, 1, out);
+	    }
+	} else {
+	    if ((item = NSS_CMSMessage_GetContent(cmsg)) != NULL) {
+		fwrite(item->data, item->len, 1, out);
+	    }
 	}
     }
 
     NSS_CMSMessage_Destroy(cmsg);
     return 0;
 }
 
 static void
--- a/security/nss/cmd/smimetools/smime
+++ b/security/nss/cmd/smimetools/smime
@@ -27,30 +27,30 @@
 # allow others to use your version of this file under the MPL,
 # indicate your decision by deleting the provisions above and
 # replace them with the notice and other provisions required by
 # the GPL.  If you do not delete the provisions above, a recipient
 # may use your version of this file under either the MPL or the
 # GPL.
 
 #
-# smime.pl - frontend for S/MIME message generation
+# smime.pl - frontend for S/MIME message generation and parsing
 #
 # $Id$
 #
 
 use Getopt::Std;
 
 @boundarychars = ( "0" .. "9", "A" .. "F" );
 
 # path to cmsutil
 $cmsutilpath = "cmsutil";
 
 #
-# Thanks to Gisle Aas <gisle@aas.no> for the encode_base64 function
+# Thanks to Gisle Aas <gisle@aas.no> for the base64 functions
 # originally taken from MIME-Base64-2.11 at www.cpan.org
 #
 sub encode_base64($)
 {
     my $res = "";
     pos($_[0]) = 0;                          # ensure start at the beginning
     while ($_[0] =~ /(.{1,45})/gs) {
 	$res .= substr(pack('u', $1), 1);    # get rid of length byte after packing
@@ -60,16 +60,88 @@ sub encode_base64($)
     # fix padding at the end
     my $padding = (3 - length($_[0]) % 3) % 3;
     $res =~ s/.{$padding}$/'=' x $padding/e if $padding;
     # break encoded string into lines of no more than 76 characters each
     $res =~ s/(.{1,76})/$1\n/g;
     $res;
 }
 
+sub decode_base64($)
+{
+    local($^W) = 0; # unpack("u",...) gives bogus warning in 5.00[123]
+
+    my $str = shift;
+    my $res = "";
+
+    $str =~ tr|A-Za-z0-9+=/||cd;            # remove non-base64 chars
+    if (length($str) % 4) {
+	require Carp;
+	Carp::carp("Length of base64 data not a multiple of 4")
+    }
+    $str =~ s/=+$//;                        # remove padding
+    $str =~ tr|A-Za-z0-9+/| -_|;            # convert to uuencoded format
+    while ($str =~ /(.{1,60})/gs) {
+	my $len = chr(32 + length($1)*3/4); # compute length byte
+	$res .= unpack("u", $len . $1 );    # uudecode
+    }
+    $res;
+}
+
+#
+# parse headers into a hash
+#
+# %headers = parseheaders($headertext);
+#
+sub parseheaders($)
+{
+    my ($headerdata) = @_;
+    my $hdr;
+    my %hdrhash;
+    my $hdrname;
+    my $hdrvalue;
+    my @hdrvalues;
+    my $subhdrname;
+    my $subhdrvalue;
+
+    # the expression in split() correctly handles continuation lines
+    foreach $hdr (split(/\n(?=\S)/, $headerdata)) {
+	$hdr =~ s/\r*\n\s+/ /g;	# collapse continuation lines
+	($hdrname, $hdrvalue) = $hdr =~ m/^(\S+):\s+(.*)$/;
+
+	# ignore non-headers (or should we die horribly?)
+	next unless (defined($hdrname));
+	$hdrname =~ tr/A-Z/a-z/;
+	@hdrvalues = split(/\s*;\s*/, $hdrvalue);
+
+	# there is guaranteed to be at least one value
+	$hdrvalue = shift @hdrvalues;
+	if ($hdrvalue =~ /^\"(.*)\"$/) {
+	    $hdrvalue = $1;
+	}
+
+	$hdrhash{$hdrname}{MAIN} = $hdrvalue;
+	# print "XXX $hdrname = $hdrvalue\n";
+
+	# deal with additional name-value pairs
+	foreach $hdrvalue (@hdrvalues) {
+	    ($subhdrname, $subhdrvalue) = $hdrvalue =~ m/^(\S+)\s*=\s*(.*)$/;
+	    # ignore non-subheaders (or should we die?)
+	    next unless (defined($subhdrname));
+	    $subhdrname =~ tr/A-Z/a-z/;
+	    if ($subhdrvalue =~ /^\"(.*)\"$/) {
+		$subhdrvalue = $1;
+	    }
+	    $hdrhash{$hdrname}{$subhdrname} = $subhdrvalue;
+	}
+
+    }
+    return %hdrhash;
+}
+
 #
 # encryptentity($entity, $options) - encrypt an S/MIME entity
 #
 # entity  - string containing entire S/MIME entity to encrypt
 # options - options for cmsutil
 #
 # this will generate and return a new multipart/signed entity consisting
 # of the canonicalized original content, plus a signature block.
@@ -188,36 +260,39 @@ sub signentity($$)
 }
 
 sub usage {
     print STDERR "usage: smime [options]\n";
     print STDERR " options:\n";
     print STDERR " -S nick             generate signed message, use certificate named \"nick\"\n";
     print STDERR "  -p passwd          use \"passwd\" as security module password\n";
     print STDERR " -E rec1[,rec2...]   generate encrypted message for recipients\n";
+    print STDERR " -D                  decode a S/MIME message\n";
     print STDERR " -C pathname         set pathname of \"cmsutil\"\n";
     print STDERR "\nWith -S or -E, smime will take a regular RFC822 message or MIME entity\n";
-    print STDERR "and generate a signed or encrypted S/MIME message with the same headers\n";
-    print STDERR "and content from it. The output can be used as input to a MTA.\n";
+    print STDERR "on stdin and generate a signed or encrypted S/MIME message with the same\n";
+    print STDERR "headers and content from it. The output can be used as input to a MTA.\n";
+    print STDERR "-D causes smime to strip off all S/MIME layers if possible and output\n";
+    print STDERR "the \"inner\" message.\n";
 }
 
 #
 # start of main procedures
 #
 
 #
 # process command line options
 #
 unless (getopts('S:E:p:C:D')) {
     usage();
     exit 1;
 }
 
-unless (defined($opt_S) or defined($opt_E)) {
-    print STDERR "ERROR: -S and/or -E must be specified.\n";
+unless (defined($opt_S) or defined($opt_E) or defined($opt_D)) {
+    print STDERR "ERROR: -S and/or -E, or -D must be specified.\n";
     usage();
     exit 1;
 }
 
 $signopts = "";
 $encryptopts = "";
 
 if (defined($opt_S)) {
@@ -238,61 +313,227 @@ if (defined($opt_C)) {
     $cmsutilpath = $opt_C;
 }
 
 #
 # split headers into mime entity headers and RFC822 headers
 # The RFC822 headers are preserved and stay on the outer layer of the message
 #
 $rfc822headers = "";
-$mimeentity = "";
+$mimeheaders = "";
+$mimebody = "";
+$skippedheaders = "";
 while (<STDIN>) {
     last if (/^$/);
     if (/^content-\S+: /i) {
-	$mimeentity .= $_;
+	$lastref = \$mimeheaders;
     } elsif (/^mime-version: /i) {
-	;			# skip it
+	$lastref = \$skippedheaders;			# skip it
+    } elsif (/^\s/) {
+	;
     } else {
-	$rfc822headers .= $_;
+	$lastref = \$rfc822headers;
     }
+    $$lastref .= $_;
 }
 
 #
 # if there are no MIME entity headers, generate some default ones
 #
-if ($mimeentity eq "") {
-    $mimeentity .= "Content-Type: text/plain; charset=us-ascii\n";
-    $mimeentity .= "Content-Transfer-Encoding: 7bit\n";
+if ($mimeheaders eq "") {
+    $mimeheaders .= "Content-Type: text/plain; charset=us-ascii\n";
+    $mimeheaders .= "Content-Transfer-Encoding: 7bit\n";
 }
 
 #
-# generate end of header-LF/LF pair
-#
-$mimeentity .= "\n";
-
-#
 # slurp in the entity body
 #
 $saveRS = $/;
 $/ = undef;
-$mimeentity .= <STDIN>;
+$mimebody = <STDIN>;
 $/ = $saveRS;
 
 if (defined $opt_D) {
     #
     # decode
     #
+    # possible options would be:
+    # - strip off only one layer
+    # - strip off outer signature (if present)
+    # - just print information about the structure of the message
+    # - strip n layers, then dump DER of CMS message
 
-    
+    while (1) {
+	%hdrhash = parseheaders($mimeheaders);
+	unless (exists($hdrhash{"content-type"}{MAIN})) {
+	    print STDERR "ERROR: no content type header found in MIME entity\n";
+	    last;	# no content-type - we're done
+	}
+
+	$contenttype = $hdrhash{"content-type"}{MAIN};
+	if ($contenttype eq "application/pkcs7-mime") {
+	    #
+	    # opaque-signed or enveloped message
+	    #
+	    unless (exists($hdrhash{"content-type"}{"smime-type"})) {
+		print STDERR "ERROR: no smime-type attribute in application/pkcs7-smime entity.\n";
+		last;
+	    }
+	    $smimetype = $hdrhash{"content-type"}{"smime-type"};
+	    if ($smimetype eq "signed-data" or $smimetype eq "enveloped-data") {
+		# it's verification or decryption time!
+		# XXX
+		if ($hdrhash{"content-transfer-encoding"}{MAIN} eq "base64") {
+		    $mimebody = decode_base64($mimebody);
+		}
+
+		# we would dump the DER at this point
+
+		$tmpsigfile = "/tmp/sig.$$";
+		open(TMP, ">$tmpsigfile") or die "ERROR: cannot write signature data to temporary file";
+		print TMP $mimebody;
+		unless (close(TMP)) {
+		    print STDERR "ERROR: writing signature data to temporary file.\n";
+		    unlink($tmpsigfile);
+		    exit 1;
+		}
+
+		$mimeheaders = "";
+		open(TMP, "$cmsutilpath -D -h 1 -i $tmpsigfile |") or die "ERROR: cannot open pipe to cmsutil";
+		while (<TMP>) {
+		    last if (/^\r?$/);
+		    if (/^SMIME: /) {
+			$lastref = \$rfc822headers;
+		    } elsif (/^\s/) {
+			;
+		    } else {
+			$lastref = \$mimeheaders;
+		    }
+		    $$lastref .= $_;
+		}
+		$olddelim = $/;
+		$/ = undef;
+		$mimebody = <TMP>;
+		$/ = $olddelim;
+		close(TMP);
+		unlink($tmpsigfile);
+	    } else {
+		print STDERR "ERROR: unknown smime-type \"$smimetype\" in application/pkcs7-smime entity.\n";
+		last;
+	    }
+	} elsif ($contenttype eq "multipart/signed") {
+	    # print STDERR "XXX multipart/signed\n";
+	    #
+	    # clear signed message
+	    #
+	    unless (exists($hdrhash{"content-type"}{"protocol"})) {
+		print STDERR "ERROR: content type has no protocol attribute in multipart/signed entity.\n";
+		last;
+	    }
+	    if ($hdrhash{"content-type"}{"protocol"} ne "application/pkcs7-signature") {
+		# we cannot handle this guy
+		print STDERR "ERROR: unknown protocol \"",
+			$hdrhash{"content-type"}{"protocol"},
+			"\" in multipart/signed entity.\n";
+		last;
+	    }
+	    unless (exists($hdrhash{"content-type"}{"boundary"})) {
+		print STDERR "ERROR: no boundary attribute in multipart/signed entity.\n";
+		last;
+	    }
+	    $boundary = $hdrhash{"content-type"}{"boundary"};
+
+	    # split $mimebody along \n--$boundary\n - gets you four parts
+	    # first (0), any comments the sending agent might have put in
+	    # second (1), the message itself
+	    # third (2), the signature as a mime entity
+	    # fourth (3), trailing data (there shouldn't be any)
+
+	    @multiparts = split(/\n--$boundary(?:--)?\n/, $mimebody);
+
+	    #
+	    # parse the signature headers
+	    ($submimeheaders, $submimebody) = split(/^$/m, $multiparts[2]);
+	    %sighdrhash = parseheaders($submimeheaders);
+	    unless (exists($sighdrhash{"content-type"}{MAIN})) {
+		print STDERR "ERROR: signature entity has no content type.\n";
+		last;
+	    }
+	    if ($sighdrhash{"content-type"}{MAIN} ne "application/pkcs7-signature") {
+		# we cannot handle this guy
+		print STDERR "ERROR: unknown content type \"",
+			$sighdrhash{"content-type"}{MAIN},
+			"\" in signature entity.\n";
+		last;
+	    }
+	    if ($sighdrhash{"content-transfer-encoding"}{MAIN} eq "base64") {
+		$submimebody = decode_base64($submimebody);
+	    }
+
+	    # we would dump the DER at this point
+
+	    $tmpsigfile = "/tmp/sig.$$";
+	    open(TMP, ">$tmpsigfile") or die "ERROR: cannot write signature data to temporary file";
+	    print TMP $submimebody;
+	    unless (close(TMP)) {
+		print STDERR "ERROR: writing signature data to temporary file.\n";
+		unlink($tmpsigfile);
+		exit 1;
+	    }
+
+	    $tmpmsgfile = "/tmp/msg.$$";
+	    open(TMP, ">$tmpmsgfile") or die "ERROR: cannot write message data to temporary file";
+	    print TMP $multiparts[1];
+	    unless (close(TMP)) {
+		print STDERR "ERROR: writing message data to temporary file.\n";
+		unlink($tmpsigfile);
+		unlink($tmpmsgfile);
+		exit 1;
+	    }
+
+	    $mimeheaders = "";
+	    open(TMP, "$cmsutilpath -D -h 1 -c $tmpmsgfile -i $tmpsigfile |") or die "ERROR: cannot open pipe to cmsutil";
+	    while (<TMP>) {
+		last if (/^\r?$/);
+		if (/^SMIME: /) {
+		    $lastref = \$rfc822headers;
+		} elsif (/^\s/) {
+		    ;
+		} else {
+		    $lastref = \$mimeheaders;
+		}
+		$$lastref .= $_;
+	    }
+	    $olddelim = $/;
+	    $/ = undef;
+	    $mimebody = <TMP>;
+	    $/ = $olddelim;
+	    close(TMP);
+	    unlink($tmpsigfile);
+	    unlink($tmpmsgfile);
+	} else {
+	    # not a content type we know - we're done
+	    last;
+	}
+    }
+
+    # so now we have the S/MIME parsing information in rfc822headers
+    # and the first mime entity we could not handle in mimeheaders and mimebody.
+    # so dump em out and we're done.
+    print $rfc822headers;
+    print $mimeheaders . "\n" . $mimebody;
 
 } else {
+
     #
-    # encode
+    # encode (much easier than decode)
     #
 
+    $mimeentity = $mimeheaders . "\n" . $mimebody;
+
     #
     # canonicalize inner entity (rudimentary yet)
     # convert single LFs to CRLF
     # if no Content-Transfer-Encoding header present:
     #  if 8 bit chars present, use Content-Transfer-Encoding: quoted-printable
     #  otherwise, use Content-Transfer-Encoding: 7bit
     #
     $mimeentity =~ s/\r*\n/\r\n/mg;