#!/usr/bin/perl -w
#
# Copyright (c) 2017 SUSE Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
#
# Notary interfacing
#

BEGIN {
  my ($wd) = $0 =~ m-(.*)/- ;
  $wd ||= '.';
  unshift @INC,  "$wd/build";
  unshift @INC,  "$wd";
}

use JSON::XS ();
use MIME::Base64 ();
use Digest::SHA ();
use Data::Dumper;

use BSConfiguration;	# for BSConfig::sign
use BSRPC ':https';
use BSUtil;
use BSASN1;
use BSPGP;
use BSBearer;
use BSTUF;

use strict;

my $targets_expire_delta = 3 * 366 * 24 * 3600;	# 3 years
my $notary_timeout = 300;
my $registry_timeout = 300;

my @signargs;

sub signedmultipartentry {
  my ($name, $d, @keyids) = @_;
  $d = BSTUF::signdata($d, \@signargs, grep {defined($_)} @keyids);
  return { 'headers' => [ "Content-Disposition: form-data; name=\"files\"; filename=\"$name\"", 'Content-Type: application/octet-stream' ], 'data' => $d };
}

# parse arguments
my $pubkeyfile;
my $dest_creds;
my $justadd;
my $digestfile;
my $list_mode;
my $delete_mode;
my $registryserver;

while (@ARGV) {
  if ($ARGV[0] eq '-p') {
    (undef, $pubkeyfile) = splice(@ARGV, 0, 2);
    next;
  }
  if ($ARGV[0] eq '--dest-creds') {
    $dest_creds = BSBearer::get_credentials($ARGV[1]);
    splice(@ARGV, 0, 2);
    next;
  }
  if ($ARGV[0] eq '-P' || $ARGV[0] eq '--project' || $ARGV[0] eq '-u' || $ARGV[0] eq '--signtype') {
    push @signargs, splice(@ARGV, 0, 2);
    next;
  }
  if ($ARGV[0] eq '-h') {
    splice(@ARGV, 0, 2);	# always sha256
    next;
  }
  if ($ARGV[0] eq '-F') {
    $digestfile = $ARGV[1];
    splice(@ARGV, 0, 2);
    next;
  }
  if ($ARGV[0] eq '--just-add') {
    $justadd = 1;
    shift @ARGV;
    next;
  }
  if ($ARGV[0] eq '-l') {
    $list_mode++;
    shift @ARGV;
    next;
  }
  if ($ARGV[0] eq '-D') {
    $delete_mode = 1;
    shift @ARGV;
    next;
  }
  if ($ARGV[0] eq '-R') {
    $registryserver = $ARGV[1];
    splice(@ARGV, 0, 2);
    next;
  }
  last;
}

my ($notaryserver, $gun, @tags);

if ($digestfile) {
  die("Usage: bs_notar -p pubkeyfile -F digestfile notaryserver gun\n") unless @ARGV == 2;
  ($notaryserver, $gun) = @ARGV;
} elsif ($list_mode) {
  die("Usage: bs_notar -l notaryserver gun [what]\n") unless @ARGV == 2 || @ARGV == 3;
  ($notaryserver, $gun) = @ARGV;
} elsif ($delete_mode) {
  die("Usage: bs_notar -D notaryserver gun\n") unless @ARGV == 2;
  ($notaryserver, $gun) = @ARGV;
} elsif ($registryserver) {
  die("Usage: bs_notar -p pubkeyfile -R registryserver notaryserver gun tags...\n") unless @ARGV >= 2;
  ($notaryserver, $gun, @tags) = @ARGV;
} else {
  # obsolete syntax, please use '-R' instead
  die("Usage: bs_notar -p pubkeyfile registryserver notaryserver gun tags...\n") unless @ARGV >= 3;
  ($registryserver, $notaryserver, $gun, @tags) = @ARGV;
}

my $notary_authenticator = BSBearer::generate_authenticator($dest_creds, 'verbose' => 1);

if ($list_mode) {
  my $what = $ARGV[2] || 'root';
  my $req = $what;
  $req = 'root' if $what eq 'cert';
  $req .= '.json' unless $req =~ /\.key$/ || $req =~ /\.json$/;
  my $param = {
    'uri' => "$notaryserver/v2/$gun/_trust/tuf/$req",
    'timeout' => $notary_timeout,
    'authenticator' => $notary_authenticator,
  };
  if ($what eq 'cert') {
    my $data = BSRPC::rpc($param, \&JSON::XS::decode_json);
    $data = $data->{'signed'};
    for my $keyid (@{$data->{'roles'}->{'root'}->{'keyids'}}) {
      my $k = $data->{'keys'}->{$keyid};
      die("bad keytype\n") unless $k->{'keytype'} eq 'rsa-x509';
      print MIME::Base64::decode_base64($k->{'keyval'}->{'public'});
    }
    exit(0);
  }
  my $pretty = $list_mode == 1;
  $pretty = 0 if $req !~ /\.json$/ && $req !~ /\.key$/;
  if (!$pretty) {
    my $data = BSRPC::rpc($param);
    print $data;
  } else {
    my $data = BSRPC::rpc($param, \&JSON::XS::decode_json);
    print JSON::XS->new->utf8->canonical->pretty->encode($data);
  }
  exit(0);
}

if ($delete_mode) {
  my $param = {
    'uri' => "$notaryserver/v2/$gun/_trust/tuf/",
    'request' => 'DELETE',
    'timeout' => $notary_timeout,
    'authenticator' => $notary_authenticator,
  };
  BSRPC::rpc($param);
  exit(0);
}

die("Need a pubkey file\n") unless defined $pubkeyfile;

#
# collect stuff to sign
#
my $manifests = {};
if ($digestfile) {
  local *DIG;
  open(DIG, '<', $digestfile) || die("$digestfile: $!\n");
  while(<DIG>) {
    chomp;
    next if /^#/ || /^\s*$/;
    die("bad line in digest file\n") unless /^([a-z0-9]+):([a-f0-9]+) (\d+) (.+?)\s*$/;
    $manifests->{$4} = {
      'hashes' => { $1 => MIME::Base64::encode_base64(pack('H*', $2), '') },
      'length' => (0 + $3),
    };
  }
  close(DIG) || die("$digestfile: $!\n");
} else {
  # calculate registry repo from notary gun
  my $repo = $gun;
  $repo =~ s/^[^\/]+\///;

  my $registry_authenticator = BSBearer::generate_authenticator($dest_creds, 'verbose' => 1);

  for my $tag (@tags) {
    my $param = {
      'headers' => [ 'Accept: application/vnd.docker.distribution.manifest.v2+json' ],
      'uri' => "$registryserver/v2/$repo/manifests/$tag",
      'authenticator' => $registry_authenticator,
      'timeout' => $registry_timeout,
    };
    my $manifest_json = BSRPC::rpc($param, undef);
    my $manifest = JSON::XS::decode_json($manifest_json);
    die("bad manifest for $repo:$tag\n") unless $manifest->{'schemaVersion'} == 2;
    $manifests->{$tag} = {
      'hashes' => { 'sha256' => MIME::Base64::encode_base64(Digest::SHA::sha256($manifest_json), '') },
      'length' => length($manifest_json),
    };
  }
}

#
# generate key material
#
my $gpgpubkey = BSPGP::unarmor(readstr($pubkeyfile));
my $pubkey_data = BSPGP::pk2keydata($gpgpubkey) || {};
die("need an rsa pubkey for container signing\n") unless ($pubkey_data->{'algo'} || '') eq 'rsa';
my $pubkey_times = BSPGP::pk2times($gpgpubkey) || {};
# generate pub key and cert from pgp key data
my $pub_bin = BSTUF::keydata2asn1($pubkey_data);

my $cert;
my $root_key;
my $targets_key;
my $timestamp_key;
my $snapshot_key;

my $root_version = 1;
my $targets_version = 1;

my $dodelete;		# new key, hopeless, need to delete old data
my $dorootupdate;	# same key with different cert

# create new to-be-signed cert
my $tbscert = BSTUF::mktbscert($gun, $pubkey_times->{'selfsig_create'}, $pubkey_times->{'key_expire'}, $pub_bin);

#
# reuse data from old root entry if we can
#
if (!$dodelete) {
  eval {
    my $param = {
      'uri' => "$notaryserver/v2/$gun/_trust/tuf/root.json",
      'timeout' => $notary_timeout,
      'authenticator' => $notary_authenticator,
    };
    my $oldroot = BSRPC::rpc($param, \&JSON::XS::decode_json);
    $root_version = 1 + $oldroot->{'signed'}->{'version'};
    my $oldroot_root_id = $oldroot->{'signed'}->{'roles'}->{'root'}->{'keyids'}->[0];
    my $oldroot_targets_id = $oldroot->{'signed'}->{'roles'}->{'targets'}->{'keyids'}->[0];
    my $oldroot_timestamp_id = $oldroot->{'signed'}->{'roles'}->{'timestamp'}->{'keyids'}->[0];
    my $oldroot_snapshot_id = $oldroot->{'signed'}->{'roles'}->{'snapshot'}->{'keyids'}->[0];
    my $oldroot_root_key = $oldroot->{'signed'}->{'keys'}->{$oldroot_root_id};
    die("oldroot is not of type rsa-x509\n") if $oldroot_root_key->{'keytype'} ne 'rsa-x509';
    my $oldroot_root_cert = MIME::Base64::decode_base64($oldroot_root_key->{'keyval'}->{'public'});
    my $oldroot_root_tbscert = BSTUF::gettbscert($oldroot_root_cert);
    if (BSTUF::removecertserial($oldroot_root_tbscert) eq BSTUF::removecertserial($tbscert)) {
      # same cert (possibly with different serial). reuse old cert.
      $cert = $oldroot_root_cert;
      $root_key = $oldroot_root_key;
      $targets_key = $oldroot->{'signed'}->{'keys'}->{$oldroot_targets_id};
      $timestamp_key = $oldroot->{'signed'}->{'keys'}->{$oldroot_timestamp_id};
      $snapshot_key = $oldroot->{'signed'}->{'keys'}->{$oldroot_snapshot_id};
    } elsif (BSTUF::getsubjectkeyinfo($oldroot_root_tbscert) eq BSTUF::getsubjectkeyinfo($tbscert)) {
      # different cert but same pubkey, e.g. different expiration time
      $dorootupdate = $oldroot_root_id;
      $timestamp_key = $oldroot->{'signed'}->{'keys'}->{$oldroot_timestamp_id};
      $snapshot_key = $oldroot->{'signed'}->{'keys'}->{$oldroot_snapshot_id};
    }
  };
  die($@) if $@ && $@ =~ /^5/;
  warn($@) if $@ && $@ !~ /^404 /;
}

$dodelete = 1 unless $cert || $dorootupdate;
if ($dodelete) {
  print "overwriting old key and cert...\n";
} elsif ($dorootupdate) {
  print "updating old key and cert...\n";
} else {
  print "reusing old key and cert...\n";
}


#
# setup needed keys
#
if (!$cert) {
  $cert = BSTUF::mkcert($tbscert, \@signargs);
}
if (!$root_key) {
  $root_key = {
    'keytype' => 'rsa-x509',
    'keyval' => { 'private' => undef, 'public' => MIME::Base64::encode_base64($cert, '')},
  };
}
if (!$targets_key) {
  $targets_key = {
    'keytype' => 'rsa',
    'keyval' => { 'private' => undef, 'public' => MIME::Base64::encode_base64($pub_bin, '') },
  };
}
if (!$timestamp_key) {
  my $param = {
    'uri' => "$notaryserver/v2/$gun/_trust/tuf/timestamp.key",
    'timeout' => $notary_timeout,
    'authenticator' => $notary_authenticator,
  };
  $timestamp_key = BSRPC::rpc($param, \&JSON::XS::decode_json);
}
if (!$snapshot_key) {
  my $param = {
    'uri' => "$notaryserver/v2/$gun/_trust/tuf/snapshot.key",
    'timeout' => $notary_timeout,
    'authenticator' => $notary_authenticator,
  };
  $snapshot_key = BSRPC::rpc($param, \&JSON::XS::decode_json);
}

my $root_key_id = Digest::SHA::sha256_hex(BSTUF::canonical_json($root_key));
my $targets_key_id = Digest::SHA::sha256_hex(BSTUF::canonical_json($targets_key));
my $timestamp_key_id = Digest::SHA::sha256_hex(BSTUF::canonical_json($timestamp_key));
my $snapshot_key_id = Digest::SHA::sha256_hex(BSTUF::canonical_json($snapshot_key));

#
# setup root 
#
my $keys = {};
$keys->{$root_key_id} = $root_key;
$keys->{$targets_key_id} = $targets_key;
$keys->{$timestamp_key_id} = $timestamp_key;
$keys->{$snapshot_key_id} = $snapshot_key;

my $roles = {};
$roles->{'root'}      = { 'keyids' => [ $root_key_id ],      'threshold' => 1 };
$roles->{'snapshot'}  = { 'keyids' => [ $snapshot_key_id ],  'threshold' => 1 };
$roles->{'targets'}   = { 'keyids' => [ $targets_key_id ],   'threshold' => 1 };
$roles->{'timestamp'} = { 'keyids' => [ $timestamp_key_id ], 'threshold' => 1 };


my $root = {
  '_type' => 'Root',
  'consistent_snapshot' => $JSON::XS::false,
  'expires' => BSTUF::rfc3339time($pubkey_times->{'key_expire'}),
  'keys' => $keys,
  'roles' => $roles,
  'version' => $root_version,
};

#
# setup targets
#
my $oldtargets;
eval {
  my $param = {
    'uri' => "$notaryserver/v2/$gun/_trust/tuf/targets.json",
    'timeout' => $notary_timeout,
    'authenticator' => $notary_authenticator,
  };
  $oldtargets = BSRPC::rpc($param, \&JSON::XS::decode_json);
  $targets_version = 1 + $oldtargets->{'signed'}->{'version'};
};
die($@) if $justadd && $@ && $@ =~ /^5/;
if ($justadd && $oldtargets) {
  for my $tag (sort keys %{$oldtargets->{'signed'}->{'targets'} || {}}) {
    next if $manifests->{$tag};
    print "taking old tag $tag\n";
    $manifests->{$tag} = $oldtargets->{'signed'}->{'targets'}->{$tag};
  }
}
if (!$dodelete && !$dorootupdate && BSUtil::identical($manifests, $oldtargets->{'signed'}->{'targets'})) {
  print "no change...\n";
  exit 0;
}

my $targets = {
  '_type' => 'Targets',
  'delegations' => { 'keys' => {}, 'roles' => []},
  'expires' => BSTUF::rfc3339time(time() + $targets_expire_delta),
  'targets' => $manifests,
  'version' => $targets_version,
};

#
# delete old data if necessary
#
if ($dodelete) {
  my $param = {
    'uri' => "$notaryserver/v2/$gun/_trust/tuf/",
    'request' => 'DELETE',
    'timeout' => $notary_timeout,
    'authenticator' => $notary_authenticator,
  };
  BSRPC::rpc($param);
}

#
# sign and send data
#
my @parts;
push @parts, signedmultipartentry('root', $root, $root_key_id, $dorootupdate) if $dodelete || $dorootupdate;
push @parts, signedmultipartentry('targets', $targets, $targets_key_id);

my $boundary = Digest::SHA::sha256_hex(join('', map {$_->{'data'}} @parts));
my $param = {
  'uri' => "$notaryserver/v2/$gun/_trust/tuf/",
  'request' => 'POST',
  'data' => BSHTTP::makemultipart($boundary, @parts),
  'headers' => [ "Content-Type: multipart/form-data; boundary=$boundary" ],
  'timeout' => $notary_timeout,
  'authenticator' => $notary_authenticator,
};

print BSRPC::rpc($param);
