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);
}