File modsign-verify of Package suse-module-tools.31943

#!/usr/bin/perl
# 
# Verify kernel module signature generated by /usr/src/linux/scripts/sign-file
# 
# Parts of this script were copied from sign-file, written by David Howels
#

my $USAGE = "Usage: modsign-verify [-v] [-q] [--certificate <x509> | --cert-dir <dir>] <module>\n";

use strict;
use warnings;
use IPC::Open2;
use Getopt::Long;
use File::Temp qw(tempfile);
use bigint;

my $cert;
my $cert_dir;
my $verbose = 1;
GetOptions(
	"certificate=s" => \$cert,
	"cert-dir=s" => \$cert_dir,
	"q|quiet" => sub { $verbose-- if $verbose; },
	"v|verbose" => sub { $verbose++; },
	"h|help" => sub {
		print $USAGE;
		print "Return codes: 0 good signature\n";
		print "              1 bad signature\n";
		print "              2 certificate not found\n";
		print "              3 module not signed\n";
		print "             >3 other error\n";
		exit(0);
	}
) or die($USAGE);

sub _verbose {
	my $level = shift;

	return if $verbose < $level;
	print STDERR @_;
}

sub info    { _verbose(1, @_); }
sub verbose { _verbose(2, @_); }
sub debug   { _verbose(3, @_); }

if (@ARGV > 1) {
	print STDERR "Excess arguments\n";
	die($USAGE);
} elsif (@ARGV < 1) {
	print STDERR "No module supplied\n";
	die($USAGE);
} elsif ($cert && $cert_dir) {
	print STDERR "Please specify either --certificate or --cert-dir, not both.\n";
	die($USAGE);
}
my $module_name = shift(@ARGV);
if (!$cert && !$cert_dir) {
	$cert_dir = "/etc/uefi/certs";
	verbose("Using default certificate directory $cert_dir\n");
}
my @certs;
if ($cert) {
	push(@certs, $cert);
} else {
	my $dh;
	if (!opendir($dh, $cert_dir)) {
		print STDERR "$cert_dir: $!\n";
		exit(2);
	}
	while (my $entry = readdir($dh)) {
		next if $entry =~ /^\./;
		next if !-f "$cert_dir/$entry";
		push(@certs, "$cert_dir/$entry");
	}
	closedir($dh);
	if (!@certs) {
		print STDERR "No certificates found in $cert_dir\n";
		exit(2);
	}
}

###############################################################################
## ASN.1 code copied from kernel-sign-file
###############################################################################

my $x509;

my $UNIV = 0 << 6;
my $APPL = 1 << 6;
my $CONT = 2 << 6;
my $PRIV = 3 << 6;

my $CONS = 0x20;

my $BOOLEAN	= 0x01;
my $INTEGER	= 0x02;
my $BIT_STRING	= 0x03;
my $OCTET_STRING = 0x04;
my $NULL	= 0x05;
my $OBJ_ID	= 0x06;
my $UTF8String	= 0x0c;
my $SEQUENCE	= 0x10;
my $SET		= 0x11;
my $UTCTime	= 0x17;
my $GeneralizedTime = 0x18;

sub encode_asn1_oid($)
{
    my ($o1, $o2, @oid) = split(/\./, $_[0]);
    my @bytes;

    push @bytes, 40*$o1 + $o2;

    while (scalar(@oid) > 0) {
	    my $c = $oid[0];
	    shift @oid;
	    my @base128 = ();

	    push @base128, ($c % 128);
	    while ($c > 128) {
		    $c /= 128;
		    push @base128, (($c % 128) | 128);
	    };
	    push @bytes, reverse(@base128);
  }
    return pack("C*", @bytes);
}

my %OIDs = (
    # joint-iso-itu-t(2) ds(5) attributeType(4)
    encode_asn1_oid("2.5.4.3")  => "commonName",
    encode_asn1_oid("2.5.4.6")  => "countryName",
    encode_asn1_oid("2.5.4.10") => "organizationName",
    encode_asn1_oid("2.5.4.11") => "organizationUnitName",
    # iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1) pkcs-1(1)
    encode_asn1_oid("1.2.840.113549.1.1.1") => "rsaEncryption",
    encode_asn1_oid("1.2.840.113549.1.1.5") => "sha1WithRSAEncryption",
    encode_asn1_oid("1.2.840.113549.1.9.1") => "emailAddress",
    # joint-iso-itu-t(2) ds(5) certificateExtension(29)
    encode_asn1_oid("2.5.29.35") => "authorityKeyIdentifier",
    encode_asn1_oid("2.5.29.14") => "subjectKeyIdentifier",
    encode_asn1_oid("2.5.29.19") => "basicConstraints",
    # iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1) pkcs-7(7)
    encode_asn1_oid("1.2.840.113549.1.7.1") => "pkcs7-data",
    encode_asn1_oid("1.2.840.113549.1.7.2") => "pkcs7-signed-data",
);

###############################################################################
#
# Extract an ASN.1 element from a string and return information about it.
#
###############################################################################
my $ASN1_EXTRACT_MSG = "asn1_extract";
sub asn1_extract($$@)
{
    my ($cursor, $expected_tag, $optional) = @_;

    return [ -1 ]
	if ($cursor->[1] == 0 && $optional);

    die $ASN1_EXTRACT_MSG, ": ", $cursor->[0],
	": ASN.1 data underrun (elem ", $cursor->[1], ")\n"
	if ($cursor->[1] < 2);

    my ($tag, $len) = unpack("CC", substr(${$cursor->[2]}, $cursor->[0], 2));

    if ($expected_tag != -1 && $tag != $expected_tag) {
	return [ -1 ]
	    if ($optional);
	die $ASN1_EXTRACT_MSG, ": ", $cursor->[0],
	    ": ASN.1 unexpected tag (", $tag, " not ", $expected_tag, ")\n";
    }

    $cursor->[0] += 2;
    $cursor->[1] -= 2;

    die $ASN1_EXTRACT_MSG, ": ", $cursor->[0], ": ASN.1 long tag\n"
	if (($tag & 0x1f) == 0x1f);
    die $ASN1_EXTRACT_MSG, ": ", $cursor->[0], ": ASN.1 indefinite length\n"
	if ($len == 0x80);

    if ($len > 0x80) {
	my $l = $len - 0x80;
	die $ASN1_EXTRACT_MSG, ": ", $cursor->[0], ": ASN.1 data underrun (len len $l)\n"
	    if ($cursor->[1] < $l);

	if ($l == 0x1) {
	    $len = unpack("C", substr(${$cursor->[2]}, $cursor->[0], 1));
	} elsif ($l == 0x2) {
	    $len = unpack("n", substr(${$cursor->[2]}, $cursor->[0], 2));
	} elsif ($l == 0x3) {
	    $len = unpack("C", substr(${$cursor->[2]}, $cursor->[0], 1)) << 16;
	    $len = unpack("n", substr(${$cursor->[2]}, $cursor->[0] + 1, 2));
	} elsif ($l == 0x4) {
	    $len = unpack("N", substr(${$cursor->[2]}, $cursor->[0], 4));
	} else {
		die $ASN1_EXTRACT_MSG, ": ", $cursor->[0],
		    ": ASN.1 element too long (", $l, ")\n";
	}

	$cursor->[0] += $l;
	$cursor->[1] -= $l;
    }

    die $ASN1_EXTRACT_MSG, ": ", $cursor->[0],
	": ASN.1 data underrun (", $len, ")\n"
	if ($cursor->[1] < $len);

    my $ret = [ $tag, [ $cursor->[0], $len, $cursor->[2] ] ];
    $cursor->[0] += $len;
    $cursor->[1] -= $len;

    return $ret;
}

###############################################################################
#
# Retrieve the data referred to by a cursor
#
###############################################################################
sub asn1_retrieve($)
{
    my ($cursor) = @_;
    my ($offset, $len, $data) = @$cursor;
    return substr($$data, $offset, $len);
}


# 2's complement representation of ASN1_INTEGER
sub asn1_int($)
{
    my ($p) = @_;
    my @bytes = unpack("C*", $p);
    my $byte;
    my $neg = 0;
    my $v = 0;

    if (($bytes[0] & 0x80) != 0) {
	    $neg = 1;
	    $bytes[0] &= ~0x80;
    }
    foreach $byte (@bytes) {
	    $v <<= 8;
	    $v += $byte;
    }
    if ($neg) {
	    $v -= (2 ** (8 * scalar(@bytes) - 1));
    };
    return $v;
}

sub asn1_pack($@)
{
    my ($tag, @data) = @_;
    my $ret = pack("C", $tag);
    my $data = join('', @data);
    my $l = length($data);
    return pack("CC", $tag, $l) . $data if $l < 127;
    my $ll = $l >> 8 ? $l >> 16 ? $l >> 24 ? 4 : 3 : 2 : 1;
    return pack("CCa*", $tag, $ll | 0x80,  substr(pack("N", $l), -$ll)) . $data;
}

my %hash_algos = (
    # iso(1) identified-organization(3) oiw(14) secsig(3) algorithms(2)
    2 => ["sha1", 160/8, encode_asn1_oid("1.3.14.3.2.26")],
    # joint-iso-itu-t(2) country(16) us(840) organization(1) gov(101) csor(3) nistAlgorithm(4) hashAlgs(2)
    4 => ["sha256", 256/8, encode_asn1_oid("2.16.840.1.101.3.4.2.1")],
    5 => ["sha384", 384/8, encode_asn1_oid("2.16.840.1.101.3.4.2.2")],
    6 => ["sha512", 512/8, encode_asn1_oid("2.16.840.1.101.3.4.2.3")],
    7 => ["sha224", 224/8, encode_asn1_oid("2.16.840.1.101.3.4.2.4")],
);

sub hash_prologue($$)
{
    my ($hash_len, $algo) = @_;
    my $obj = asn1_pack($UNIV | $OBJ_ID, $algo);
    my $seq = asn1_pack($UNIV | $CONS | $SEQUENCE, $obj . pack("CC", $NULL, 0));
    my $tail = pack("CC", $OCTET_STRING, $hash_len);
    my $head = pack("CC", $UNIV | $CONS | $SEQUENCE,
		    length($seq) + length($tail) + $hash_len);
    return $head . $seq . $tail;
}

sub find_hash_algo_by_oid($)
{
    my ($oid) = @_;
    my $key;
    my $k;

  SEARCH:
    foreach $k (keys %hash_algos) {
	    my ($_h, $_n, $_a) =  @{$hash_algos{$k}};
	    if ($oid eq $_a) {
		    $key = $k;
		    last SEARCH;
	    }
    }
    die "$module_name: unsupported hash algorithm OID=".sprintf("%v02x", $oid)
        if !defined($key);
    return $key;
}

###############################################################################
#
# Roughly parse the X.509 certificate
#
###############################################################################
sub parse_x509_dn(@)
{
	my ($parent, $cursor) = @_;
	my ($offset, $len, $data) = @$cursor;
	my %result = ();

	while ($cursor->[1]> 0) {
		my $_set = asn1_extract($cursor, $UNIV | $CONS | $SET);
		my $_seq = asn1_extract($_set->[1],
					$UNIV | $CONS | $SEQUENCE);
		my $_oid = asn1_extract($_seq->[1], $UNIV | $OBJ_ID);
		my $oid = asn1_retrieve($_oid->[1]);
		if (defined($OIDs{$oid})) {
			my $key = "$parent/$OIDs{$oid}";
			my $_x = asn1_extract($_seq->[1], -1);

			# debug "found $key at $_seq->[1][0]\n";
			$result{$key} = asn1_retrieve($_x->[1]);
		};
	}
	return \%result;
}

sub parse_x509_der($)
{
	my ($bytes) = @_;

	my $cursor = [ 0, length($bytes), \$bytes ];

	my $cert = asn1_extract($cursor, $UNIV | $CONS | $SEQUENCE);
	my $tbs = asn1_extract($cert->[1], $UNIV | $CONS | $SEQUENCE);
	my $version = asn1_extract($tbs->[1], $CONT | $CONS | 0, 1);
	my $serial_number = asn1_extract($tbs->[1], $UNIV | $INTEGER);
	my $sig_type = asn1_extract($tbs->[1], $UNIV | $CONS | $SEQUENCE);
	my $issuer = asn1_extract($tbs->[1], $UNIV | $CONS | $SEQUENCE);
	my $issuer_dn = parse_x509_dn("issuer", $issuer->[1]);
	my $validity = asn1_extract($tbs->[1], $UNIV | $CONS | $SEQUENCE);
	my $subject = asn1_extract($tbs->[1], $UNIV | $CONS | $SEQUENCE);
	my $key = asn1_extract($tbs->[1], $UNIV | $CONS | $SEQUENCE);
	my $pubkey = asn1_pack($UNIV | $CONS | $SEQUENCE,
			     asn1_retrieve($key->[1]));

	my $issuer_uid = asn1_extract($tbs->[1], $CONT | $CONS | 1, 1);
	my $subject_uid = asn1_extract($tbs->[1], $CONT | $CONS | 2, 1);
	my $extension_list = asn1_extract($tbs->[1], $CONT | $CONS | 3, 1);

	my $subject_key_id = ();
	my $authority_key_id = ();

	#
	# Parse the extension list
	#
	if ($extension_list->[0] != -1) {
		my $extensions = asn1_extract($extension_list->[1], $UNIV | $CONS | $SEQUENCE);

		while ($extensions->[1]->[1] > 0) {
			my $ext = asn1_extract($extensions->[1], $UNIV | $CONS | $SEQUENCE);
			my $x_oid = asn1_extract($ext->[1], $UNIV | $OBJ_ID);
			my $x_crit = asn1_extract($ext->[1], $UNIV | $BOOLEAN, 1);
			my $x_val = asn1_extract($ext->[1], $UNIV | $OCTET_STRING);

			my $raw_oid = asn1_retrieve($x_oid->[1]);
			next if (!exists($OIDs{$raw_oid}));
			my $x_type = $OIDs{$raw_oid};

			my $raw_value = asn1_retrieve($x_val->[1]);

			if ($x_type eq "subjectKeyIdentifier") {
				my $vcursor = [ 0, length($raw_value), \$raw_value ];

				$subject_key_id = asn1_extract($vcursor, $UNIV | $OCTET_STRING);
			}
		}
	}
	my %result = (
	    "subject_key_id" => asn1_retrieve($subject_key_id->[1]),
	    "serial" => asn1_int(asn1_retrieve($serial_number->[1])),
	    "pubkey" => $pubkey,
	    %$issuer_dn,
	    );
	return \%result;
}

#
# Function to read the contents of a file into a variable.
#
sub read_file($)
{
    my ($file) = @_;
    my $contents;
    my $len;

    open(FD, "<$file") || die $file;
    binmode FD;
    my @st = stat(FD);
    die $file if (!@st);
    $len = read(FD, $contents, $st[7]) || die $file;
    close(FD) || die $file;
    die "$file: Wanted length ", $st[7], ", got ", $len, "\n"
	if ($len != $st[7]);
    return $contents;
}

sub openssl_pipe($$) {
	my ($input, $cmd) = @_;
	my ($pid, $res);

	$pid = open2(*read_from, *write_to, $cmd) || die $cmd;
	binmode write_to;
	if (defined($input) && $input ne "") {
		print write_to $input || return "";
	}
	close(write_to) || die "$cmd: $!";

	binmode read_from;
	read(read_from, $res, 4096) || return "";
	close(read_from) || return "";
	waitpid($pid, 0) || die;
	return "" if ($? >> 8);
	return $res;
}

sub cert_matches($$$$) {
	my ($cert, $subject_key_id, $issuer, $serial) = @_;
	my $bytes = read_file($cert);

	$ASN1_EXTRACT_MSG = $cert;
	my $cert_props = parse_x509_der($bytes);

	if (defined($subject_key_id)) {
		debug("$cert has key id " .
		      unpack("H*", $cert_props->{"subject_key_id"}) . "\n");
		if ($cert_props->{"subject_key_id"} eq $subject_key_id) {
			return $cert_props;
		} else {
			return 0;
		}
	}

	die "missing input data in cert_matches()"
	    if (!defined($issuer) || !defined($serial));

	if (!defined($cert_props->{"serial"}) ||
	    $cert_props->{"serial"} ne $serial) {
		debug "$cert: serial number mismatch: $serial != ". $cert_props->{"serial"}."\n";
		return 0;
	}
	foreach my $k (keys(%$issuer)) {
		if (!defined($cert_props->{$k}) ||
			     $issuer->{$k} ne $cert_props->{$k}) {
			debug "$cert: $k does not match signature\n";
			return 0;
		}
	}
	return $cert_props;
}

my $module = read_file($module_name);
my $module_len = length($module);
my $magic_number = "~Module signature appended~\n";
my $magic_len = length($magic_number);
my $info_len = 12;

sub eat
{
	my $length = shift;
	if ($module_len < $length) {
		die "Module size too short\n";
	}
	my $res = substr($module, -$length);
	$module = substr($module, 0, $module_len - $length);
	$module_len -= $length;
	return $res;
}

if (eat($magic_len) ne $magic_number) {
	print "$module_name: module not signed\n";
	exit(3);
}
my $info = eat($info_len);
my ($algo, $hash, $id_type, $name_len, $key_len, $sig_len) =
	unpack("CCCCCxxxN", $info);
my $signature = eat($sig_len);
# cert is identified either by subject key id, or by issuer DN + serial no
my $issuer_dn;
my $serial;
my $key_id;
my $name;
if ($id_type == 1) {

	if (unpack("n", $signature) != $sig_len - 2) {
		die "Invalid signature format\n";
	}
	$signature = substr($signature, 2);
	$key_id = eat($key_len);
	$name = eat($name_len);

	if ($algo != 1) {
		die "Unsupported signature algorithm\n";
	}
} elsif ($id_type == 2) {
	# PKCS7 signature
	$ASN1_EXTRACT_MSG = $module_name;
	my $cursor = [ 0, length($signature), \$signature ];
	my $seq0 = asn1_extract($cursor, $UNIV | $CONS | $SEQUENCE);
	my $signed_data = asn1_extract($seq0->[1], $UNIV | $OBJ_ID);
	die "$module_name: no PKCS#7 signed_data structure\n"
	    if $OIDs{asn1_retrieve($signed_data->[1])} !~ /^pkcs7-signed-data$/;

	my $ctx1 = asn1_extract($seq0->[1], $UNIV | $CONT | $CONS);
	my $seq1 = asn1_extract($ctx1->[1], $UNIV | $CONS | $SEQUENCE);
	my $sig_version = asn1_extract($seq1->[1], $UNIV | $INTEGER);

	my $digest_algo_seq_set = asn1_extract($seq1->[1],
					       $UNIV | $CONS | $SET);
	my $digest_algo_seq = asn1_extract($digest_algo_seq_set->[1],
					   $UNIV | $CONS | $SEQUENCE);
	my $digest_algo = asn1_extract($digest_algo_seq->[1], $UNIV | $OBJ_ID);
	$hash = find_hash_algo_by_oid(asn1_retrieve($digest_algo->[1]));

	my $seq2 = asn1_extract($seq1->[1], $UNIV | $CONS | $SEQUENCE);
	my $pkcs7_data = asn1_extract($seq2->[1], $UNIV | $OBJ_ID);
	die "$module_name: invalid PKCS#7 data"
	    if $OIDs{asn1_retrieve($pkcs7_data->[1])} !~ /^pkcs7-data$/;

	my $si_set = asn1_extract($seq1->[1], $UNIV | $CONS | $SET);
	my $si_seq = asn1_extract($si_set->[1], $UNIV | $CONS | $SEQUENCE);
	my $si_version = asn1_extract($si_seq->[1], $UNIV | $INTEGER);

	my $_key_id = asn1_extract($si_seq->[1], -1);
	my $key_id;

	if ($_key_id->[0] == ($CONT | 0)) {
		# key_id: kernel-sign-file -k
		$key_id  = asn1_extract($_key_id->[1], $CONT | 0);
	} else {
		# issuer / serial
		my $issuer = asn1_extract($_key_id->[1],
					  $UNIV | $CONS | $SEQUENCE);
		my $_serial = asn1_extract($_key_id->[1], $UNIV | $INTEGER);
		$serial = asn1_int(asn1_retrieve($_serial->[1]));
		$issuer_dn = parse_x509_dn("issuer", $issuer->[1]);
		if (defined($issuer_dn->{"issuer/commonName"})) {
			$name = "cn=" . $issuer_dn->{"issuer/commonName"} .
			    ",serial=$serial";
		}
	}

	my $seq4 = asn1_extract($si_seq->[1], $UNIV | $CONS | $SEQUENCE);
	my $digest2 = asn1_extract($seq4->[1], $UNIV | $OBJ_ID);
	my $hash2 = find_hash_algo_by_oid(asn1_retrieve($digest2->[1]));
	die "$module_name: inconsistent hash" if $hash2 != $hash;

	my $seq5 = asn1_extract($si_seq->[1], $UNIV | $CONS | $SEQUENCE);
	my $enc = asn1_extract($seq5->[1], $UNIV | $OBJ_ID);
	die "$module_name: invalid encryption type".
	    sprintf("%v02x", asn1_retrieve($enc->[1]))
	    if $OIDs{asn1_retrieve($enc->[1])} ne "rsaEncryption";
	my $_sig = asn1_extract($si_seq->[1], $UNIV | $OCTET_STRING);
	$signature = asn1_retrieve($_sig->[1]);
} else {
    die "unsupported signature type $id_type";
}

#
# Digest the data
#
my ($prologue, $hash_len, $dgst, $oid);
die "Unsupported hash algorithm\n" if not exists $hash_algos{$hash};

($dgst, $hash_len, $oid) = @{$hash_algos{$hash}};
$prologue = hash_prologue($hash_len, $oid);

verbose("Signature type: ", $id_type == 1 ? "legacy" : "pkcs#7", "\n");
verbose("Signed by: $name\n") if defined ($name);
verbose("Key id: " . unpack("H*", $key_id) . "\n") if (defined($key_id));
verbose("Hash algorithm: $dgst\n");

my $digest = openssl_pipe($module, "openssl dgst -$dgst -binary");
my $original_message = $prologue . $digest;

my $good = 0;
my $matched = 0;
for my $cert (sort @certs) {
	debug("Trying $cert\n");

	my $cert_props = cert_matches($cert, $key_id, $issuer_dn, $serial);
	next unless $cert_props;
	verbose("Found matching certificate $cert\n");
	$matched = $cert;

	my ($fh, $filename) = tempfile() or die "Cannot create temporary file: $!\n";
	print $fh $cert_props->{"pubkey"};
	close($fh);
	my $verified_message = openssl_pipe($signature,
		"openssl rsautl -verify -inkey $filename -keyform DER -pubin");
	unlink($filename);
	if ($original_message ne $verified_message) {
		verbose "$module_name: signature validation failed for $cert\n";
		next;
	}
	print "$module_name: good signature\n";
	$good = 1;
	exit(0);
}
if (!$matched) {
	print "certificate not found\n";
	exit(2);
} else {
	print "$module_name: bad signature\n";
	exit(1);
}
openSUSE Build Service is sponsored by