######################################################################
#
# $Id: DsvRoutines.pm,v 1.45 2012/01/07 07:56:13 mavrik Exp $
#
######################################################################
#
# Copyright 2010-2012 The WebJob Project, All Rights Reserved.
#
######################################################################
#
# Purpose: Home for various DSV-related routines.
#
######################################################################

package WebJob::DsvRoutines;

require Exporter;

use 5.008;
use strict;
use vars qw($VERSION @ISA @EXPORT @EXPORT_OK);

use Cwd;
use File::Basename;
use File::Copy;
use File::Path;
use File::Temp qw(tempdir);
use WebJob::EadRoutines;
use WebJob::FdaRoutines;
use WebJob::KvpRoutines 1.029;
use WebJob::Properties;
use WebJob::ValidationRoutines;
use WebJob::VersionRoutines;

@EXPORT = qw(DsvCompareCertificates DsvCreateCertificate DsvCreateIdentityFiles DsvCreateKey DsvCreateRequest DsvCreateUueBundle DsvDeployUueBundle DsvGetDistinguishedName DsvGetFingerprint DsvGetIdentityKvps DsvGetIssuer DsvGetSubject DsvMakeDistinguishedName DsvParseDistinguishedName DsvUnpackUueBundle);

@EXPORT_OK = ();
@ISA = qw(Exporter);
$VERSION = do { my @r = (q$Revision: 1.45 $ =~ /(\d+)/g); sprintf("%d."."%03d" x $#r, @r); };

######################################################################
#
# DsvCompareCertificates
#
######################################################################

sub DsvCompareCertificates
{
  my ($phPArgs) = @_;

  ####################################################################
  #
  # Make sure that required inputs are defined.
  #
  ####################################################################

  my %hLArgs =
  (
    'Hash' => $phPArgs,
    'Keys' =>
    [
      'ActualCert',
      'OpenSslExe',
      'TargetCert',
    ],
  );
  if (!defined(VerifyHashKeys(\%hLArgs)))
  {
    $$phPArgs{'Error'} = $hLArgs{'Error'} if (defined($phPArgs));
    return undef;
  }

  ####################################################################
  #
  # Assign default values where necessary.
  #
  ####################################################################

  ####################################################################
  #
  # Make sure we have the necessary prerequisites.
  #
  ####################################################################

  if (!-f $$phPArgs{'ActualCert'})
  {
    $$phPArgs{'Error'} = "File ($$phPArgs{'ActualCert'}) does not exist or is not regular.";
    return undef;
  }

  if (!-f $$phPArgs{'TargetCert'})
  {
    $$phPArgs{'Error'} = "File ($$phPArgs{'TargetCert'}) does not exist or is not regular.";
    return undef;
  }

  my $sOpenSslExe = FdaFindExecutable($$phPArgs{'OpenSslExe'});
  if (!defined($sOpenSslExe))
  {
    $$phPArgs{'Error'} = "Executable ($$phPArgs{'OpenSslExe'}) is not executable or could not be found in the system PATH.";
    return undef;
  }

  ####################################################################
  #
  # Get target/actual subjects/fingerprints.
  #
  ####################################################################

  my ($sActualFingerprint, $sActualSubject, $sTargetFingerprint, $sTargetSubject);

  %hLArgs =
  (
    'File' => $$phPArgs{'TargetCert'},
    'OpenSslExe' => $sOpenSslExe,
    'Type' => "x509",
  );
  $sTargetFingerprint = DsvGetFingerprint(\%hLArgs);
  if (!defined($sTargetFingerprint))
  {
    $$phPArgs{'Error'} = $hLArgs{'Error'};
    return undef;
  }
  $sTargetSubject = DsvGetSubject(\%hLArgs);
  if (!defined($sTargetSubject))
  {
    $$phPArgs{'Error'} = $hLArgs{'Error'};
    return undef;
  }

  %hLArgs =
  (
    'File' => $$phPArgs{'ActualCert'},
    'OpenSslExe' => $sOpenSslExe,
    'Type' => "x509",
  );
  $sActualFingerprint = DsvGetFingerprint(\%hLArgs);
  if (!defined($sActualFingerprint))
  {
    $$phPArgs{'Error'} = $hLArgs{'Error'};
    return undef;
  }
  $sActualSubject = DsvGetSubject(\%hLArgs);
  if (!defined($sActualSubject))
  {
    $$phPArgs{'Error'} = $hLArgs{'Error'};
    return undef;
  }

  ####################################################################
  #
  # Compare target/actual subjects/fingerprints.
  #
  ####################################################################

  if ($sActualSubject ne $sTargetSubject)
  {
    $$phPArgs{'Error'} = "Subject mismatch (\"$sActualSubject\" != \"$sTargetSubject\").";
    return undef;
  }

  if ($sActualFingerprint ne $sTargetFingerprint)
  {
    $$phPArgs{'Error'} = "Fingerprint mismatch (\"$sActualFingerprint\" != \"$sTargetFingerprint\").";
    return undef;
  }

  1;
}


######################################################################
#
# DsvCreateCertificate
#
######################################################################

sub DsvCreateCertificate
{
  my ($phPArgs) = @_;

  ####################################################################
  #
  # Make sure that required inputs are defined.
  #
  ####################################################################

  my %hLArgs =
  (
    'Hash' => $phPArgs,
    'Keys' =>
    [
#     'CertDays',         # Optional
      'CertFile',
#     'ForceWrite',       # Optional
      'KeyFile',
      'OpenSslCfg',
      'OpenSslExe',
      'Subject',
    ],
  );
  if (!defined(VerifyHashKeys(\%hLArgs)))
  {
    $$phPArgs{'Error'} = $hLArgs{'Error'} if (defined($phPArgs));
    return undef;
  }

  ####################################################################
  #
  # Assign default values where necessary.
  #
  ####################################################################

  my $sCertDays = (defined($$phPArgs{'CertDays'})) ? $$phPArgs{'CertDays'} : 365;

  my $sForceWrite = (exists($$phPArgs{'ForceWrite'}) && $$phPArgs{'ForceWrite'}) ? 1 : 0;

  my $sSubject = DsvMakeDistinguishedName(\%{$$phPArgs{'Subject'}}) || "";

  ####################################################################
  #
  # Make sure we have the necessary prerequisites.
  #
  ####################################################################

  my $phGlobalRegexes = PropertiesGetGlobalRegexes();

  if ($sCertDays !~ /^$$phGlobalRegexes{'Decimal16Bit'}$/)
  {
    $$phPArgs{'Error'} = "The number of certificate valid days ($sCertDays) does not pass muster.";
    return undef;
  }

  if (!-f $$phPArgs{'KeyFile'})
  {
    $$phPArgs{'Error'} = "Key file ($$phPArgs{'KeyFile'}) does not exist or is not regular.";
    return undef;
  }

  if (!-f $$phPArgs{'OpenSslCfg'})
  {
    $$phPArgs{'Error'} = "OpenSSL config file ($$phPArgs{'OpenSslCfg'}) does not exist or is not regular.";
    return undef;
  }

  if ($sSubject eq "")
  {
    $$phPArgs{'Error'} = "The subject must contain at least one field.";
    return undef;
  }

  my $sOpenSslExe = FdaFindExecutable($$phPArgs{'OpenSslExe'});
  if (!defined($sOpenSslExe))
  {
    $$phPArgs{'Error'} = "Executable ($$phPArgs{'OpenSslExe'}) is not executable or could not be found in the system PATH.";
    return undef;
  }

  ####################################################################
  #
  # Prepare the output file handle.
  #
  ####################################################################

  my $sFileHandle;

  if ($$phPArgs{'CertFile'} eq "-")
  {
    $sFileHandle = \*STDOUT;
  }
  else
  {
    if (-f $$phPArgs{'CertFile'} && !$sForceWrite)
    {
      $$phPArgs{'Error'} = "The certificate file ($$phPArgs{'CertFile'}) already exists.";
      return undef;
    }
    if (!open(FH, "> $$phPArgs{'CertFile'}"))
    {
      $$phPArgs{'Error'} = "Failed to open/create \"$$phPArgs{'CertFile'}\" ($!).";
      return undef;
    }
    $sFileHandle = \*FH;
  }
  binmode($sFileHandle);

  ####################################################################
  #
  # Generate the certificate.
  #
  ####################################################################

  my $sCommandLine = qq("$sOpenSslExe" req -config "$$phPArgs{'OpenSslCfg'}" -new -key "$$phPArgs{'KeyFile'}" -x509 -sha1 -subj "$sSubject" -days $sCertDays);
  my $sOutput = qx($sCommandLine);
  my $sStatus = ($? >> 8) & 0xff;
  if ($sStatus != 0)
  {
    my $sMessage = ($sStatus == 255) ? $! : "exit code was $sStatus";
    $$phPArgs{'Error'} = "Command ($sCommandLine) failed ($sMessage).";
    close($sFileHandle);
    unlink($$phPArgs{'CertFile'}); # Don't leave an empty certificate file.
    return undef;
  }
  print $sFileHandle $sOutput;
  close($sFileHandle);

  1;
}


######################################################################
#
# DsvCreateIdentityFiles
#
######################################################################

sub DsvCreateIdentityFiles
{
  my ($phPArgs) = @_;

  ####################################################################
  #
  # Make sure that required inputs are defined.
  #
  ####################################################################

  my %hLArgs =
  (
    'Hash' => $phPArgs,
    'Keys' =>
    [
#     'CertDays',         # Optional
      'ClientId',
      'DsvToolExe',
#     'ForceWrite',       # Optional
      'Hostname',
#     'KeyBits',          # Optional
#     'KeyType',          # Optional
      'OpenSslExe',
      'OsClass',
#     'OutputDirectory',  # Optional
      'RegistrationCode',
      'Subject',
      'WebJobHome',
    ],
  );
  if (!defined(VerifyHashKeys(\%hLArgs)))
  {
    $$phPArgs{'Error'} = $hLArgs{'Error'} if (defined($phPArgs));
    return undef;
  }

  ####################################################################
  #
  # Assign default values where necessary.
  #
  ####################################################################

  my $sCertDays = (defined($$phPArgs{'CertDays'})) ? $$phPArgs{'CertDays'} : 365;

  my $sForceWrite = (exists($$phPArgs{'ForceWrite'}) && $$phPArgs{'ForceWrite'}) ? 1 : 0;

  my $sKeyBits = (defined($$phPArgs{'KeyBits'})) ? $$phPArgs{'KeyBits'} : 1024;

  my $sKeyType = (defined($$phPArgs{'KeyType'})) ? lc($$phPArgs{'KeyType'}) : "rsa";

  my $sOutputDirectory = (defined($$phPArgs{'OutputDirectory'})) ? $$phPArgs{'OutputDirectory'} : $$phPArgs{'WebJobHome'} . "/" . "etc";

  ####################################################################
  #
  # Make sure we have the necessary prerequisites.
  #
  ####################################################################

  my $phGlobalRegexes = PropertiesGetGlobalRegexes();

  if ($sCertDays !~ /^$$phGlobalRegexes{'Decimal16Bit'}$/)
  {
    $$phPArgs{'Error'} = "The number of certificate valid days ($sCertDays) does not pass muster.";
    return undef;
  }

  if ($$phPArgs{'ClientId'} !~ /^$$phGlobalRegexes{'ClientId'}$/)
  {
    $$phPArgs{'Error'} = "The client ID ($$phPArgs{'ClientId'}) does not pass muster.";
    return undef;
  }

  if ($$phPArgs{'Hostname'} !~ /^$$phGlobalRegexes{'Host'}$/)
  {
    $$phPArgs{'Error'} = "The hostname ($$phPArgs{'Hostname'}) does not pass muster.";
    return undef;
  }

  if ($sKeyBits !~ /^$$phGlobalRegexes{'Decimal16Bit'}$/)
  {
    $$phPArgs{'Error'} = "The number of key bits ($sKeyBits) does not pass muster.";
    return undef;
  }

  if ($sKeyBits % 256 || $sKeyBits < 1024 || $sKeyBits > 4096)
  {
    $$phPArgs{'Error'} = "The number of key bits ($sKeyBits) must be a multiple of 256 and in the range [1024-4096].";
    return undef;
  }

  if ($sKeyType !~ /^(?:dsa|rsa)$/)
  {
    $$phPArgs{'Error'} = "The key type ($sKeyType) must be \"dsa\" or \"rsa\".";
    return undef;
  }

  if ($$phPArgs{'OsClass'} !~ /^$$phGlobalRegexes{'OsClass'}$/)
  {
    $$phPArgs{'Error'} = "The OS class ($$phPArgs{'OsClass'}) does not pass muster.";
    return undef;
  }

  if ($$phPArgs{'RegistrationCode'} !~ /^$$phGlobalRegexes{'RegistrationCode'}$/)
  {
    $$phPArgs{'Error'} = "The registration code ($$phPArgs{'RegistrationCode'}) does not pass muster.";
    return undef;
  }

  my $sDsvToolExe = FdaFindExecutable($$phPArgs{'DsvToolExe'});
  if (!defined($sDsvToolExe))
  {
    $$phPArgs{'Error'} = "Executable ($$phPArgs{'DsvToolExe'}) is not executable or could not be found in the system PATH.";
    return undef;
  }

  my $sOpenSslExe = FdaFindExecutable($$phPArgs{'OpenSslExe'});
  if (!defined($sOpenSslExe))
  {
    $$phPArgs{'Error'} = "Executable ($$phPArgs{'OpenSslExe'}) is not executable or could not be found in the system PATH.";
    return undef;
  }

  ####################################################################
  #
  # Conditionally create the output directory.
  #
  ####################################################################

  if (!-d $sOutputDirectory)
  {
    eval { mkpath($sOutputDirectory, 0, 0750) };
    if ($@)
    {
      my $sMessage = $@; $sMessage =~ s/[\r\n]+/ /g; $sMessage =~ s/\s+/ /g; $sMessage =~ s/\s+$//;
      $$phPArgs{'Error'} = "Failed to create \"$sOutputDirectory\" ($sMessage).";
      return undef;
    }
  }

  ####################################################################
  #
  # Establish a new umask, and save the old one. The files created by
  # this routine should only be readable by their owner.
  #
  ####################################################################

  my $sOldUmask = umask(0077);

  ####################################################################
  #
  # Conditionally create a temporary/generic OpenSSL config file.
  #
  ####################################################################

  my $sOpenSslCfg = $sOutputDirectory . "/" . "openssl.cfg";

  my $sOpenSslCfgGenerated = 0;

  if ($sForceWrite || !-f $sOpenSslCfg)
  {
    if (!open(FH, "> $sOpenSslCfg"))
    {
      $$phPArgs{'Error'} = "Failed to open/create \"$sOpenSslCfg\" ($!).";
      return undef;
    }
    print FH <<EOF;
[ req ]
distinguished_name=req_distinguished_name

[ req_distinguished_name ]

EOF
    close(FH);
    $sOpenSslCfgGenerated = 1;
  }
# $$phPArgs{'Files'}{$sOpenSslCfg} = $sOpenSslCfgGenerated;

  ####################################################################
  #
  # Conditionally create the client's signing key.
  #
  ####################################################################

  my $sClientKeyA = $sOutputDirectory . "/" . "client-key-a.pem";
  my $sClientKeyB = $sOutputDirectory . "/" . "client-key-b.pem";

  my $sClientKeyAGenerated = 0;
  my $sClientKeyACloned = 0;
  my $sClientKeyBCloned = 0;

  if ($sForceWrite || !-f $sClientKeyA)
  {
    if (!$sForceWrite && -f $sClientKeyB)
    {
      if (!copy($sClientKeyB, $sClientKeyA))
      {
        $$phPArgs{'Error'} = "Failed to clone $sClientKeyB ($!).";
        unlink($sOpenSslCfg) if ($sOpenSslCfgGenerated);
        return undef;
      }
      $sClientKeyACloned = 1;
    }
    else
    {
      my %hLArgs =
      (
        'DsvToolExe' => $sDsvToolExe,
        'ForceWrite' => 1,
        'KeyBits' => $sKeyBits,
        'KeyFile' => $sClientKeyA,
        'KeyType' => $sKeyType,
      );
      if (!DsvCreateKey(\%hLArgs))
      {
        $$phPArgs{'Error'} = $hLArgs{'Error'};
        unlink($sOpenSslCfg) if ($sOpenSslCfgGenerated);
        return undef;
      }
      $sClientKeyAGenerated = 1;
    }
  }
  $$phPArgs{'Files'}{$sClientKeyA} = ($sClientKeyACloned || $sClientKeyAGenerated);

  if ($sForceWrite || !-f $sClientKeyB || $sClientKeyAGenerated)
  {
    if (!copy($sClientKeyA, $sClientKeyB))
    {
      $$phPArgs{'Error'} = "Failed to clone $sClientKeyA ($!).";
      unlink($sOpenSslCfg) if ($sOpenSslCfgGenerated);
      return undef;
    }
    $sClientKeyBCloned = 1;
  }
  $$phPArgs{'Files'}{$sClientKeyB} = $sClientKeyBCloned;

  foreach my $sClientKey ($sClientKeyA, $sClientKeyB)
  {
    if (!chmod(0600, $sClientKey))
    {
      $$phPArgs{'Error'} = "Failed to set permissions on \"$sClientKey\" ($!).";
      unlink($sOpenSslCfg) if ($sOpenSslCfgGenerated);
      return undef;
    }
  }

  ####################################################################
  #
  # Conditionally create the client's signing certificate.
  #
  ####################################################################

  my $sClientDsvA = $sOutputDirectory . "/" . "client-dsv-a.pem";
  my $sClientDsvB = $sOutputDirectory . "/" . "client-dsv-b.pem";

  my $sClientDsvAGenerated = 0;
  my $sClientDsvACloned = 0;
  my $sClientDsvBCloned = 0;

  if ($sForceWrite || !-f $sClientDsvA || $sClientKeyAGenerated)
  {
    if (!$sForceWrite && !$sClientKeyAGenerated && -f $sClientDsvB)
    {
      if (!copy($sClientDsvB, $sClientDsvA))
      {
        $$phPArgs{'Error'} = "Failed to clone $sClientDsvB ($!).";
        unlink($sOpenSslCfg) if ($sOpenSslCfgGenerated);
        return undef;
      }
      $sClientDsvACloned = 1;
    }
    else
    {
      my %hLArgs =
      (
        'CertDays' => $sCertDays,
        'CertFile' => $sClientDsvA,
        'ForceWrite' => 1,
        'KeyFile' => $sClientKeyA,
        'OpenSslCfg' => $sOpenSslCfg,
        'OpenSslExe' => $sOpenSslExe,
        'Subject' => $$phPArgs{'Subject'},
      );
      if (!DsvCreateCertificate(\%hLArgs))
      {
        $$phPArgs{'Error'} = $hLArgs{'Error'};
        unlink($sOpenSslCfg) if ($sOpenSslCfgGenerated);
        return undef;
      }
      $sClientDsvAGenerated = 1;
    }
  }
  $$phPArgs{'Files'}{$sClientDsvA} = ($sClientDsvACloned || $sClientDsvAGenerated);

  if ($sForceWrite || !-f $sClientDsvB || $sClientDsvAGenerated)
  {
    if (!copy($sClientDsvA, $sClientDsvB))
    {
      $$phPArgs{'Error'} = "Failed to clone $sClientDsvA ($!).";
        unlink($sOpenSslCfg) if ($sOpenSslCfgGenerated);
      return undef;
    }
    $sClientDsvBCloned = 1;
  }
  $$phPArgs{'Files'}{$sClientDsvB} = $sClientDsvBCloned;

  ####################################################################
  #
  # Create the client's registration key/value pairs.
  #
  ####################################################################

  my $sClientKvpA = $sOutputDirectory . "/" . "client-kvp-a.cfg";
  my $sClientKvpB = $sOutputDirectory . "/" . "client-kvp-b.cfg";

  my $sClientKvpAGenerated = 0;
  my $sClientKvpACloned = 0;
  my $sClientKvpBCloned = 0;

  if ($sForceWrite || !-f $sClientKvpA)
  {
    if (!$sForceWrite && -f $sClientKvpB)
    {
      if (!copy($sClientKvpB, $sClientKvpA))
      {
        $$phPArgs{'Error'} = "Failed to clone $sClientKvpB ($!).";
        unlink($sOpenSslCfg) if ($sOpenSslCfgGenerated);
        return undef;
      }
      $sClientKvpACloned = 1;
    }
    else
    {
      if (!open(FH, "> $sClientKvpA"))
      {
        $$phPArgs{'Error'} = "Failed to open/create \"$sClientKvpA\" ($!).";
        unlink($sOpenSslCfg) if ($sOpenSslCfgGenerated);
        return undef;
      }
      print FH <<EOF;
ClientId=$$phPArgs{'ClientId'}
Hostname=$$phPArgs{'Hostname'}
OsClass=$$phPArgs{'OsClass'}
RegistrationCode=$$phPArgs{'RegistrationCode'}
WebJobHome=$$phPArgs{'WebJobHome'}
EOF
      close(FH);
      $sClientKvpAGenerated = 1;
    }
  }
  $$phPArgs{'Files'}{$sClientKvpA} = ($sClientKvpACloned || $sClientKvpAGenerated);

  if ($sForceWrite || !-f $sClientKvpB || $sClientKvpAGenerated)
  {
    if (!copy($sClientKvpA, $sClientKvpB))
    {
      $$phPArgs{'Error'} = "Failed to clone $sClientKvpA ($!).";
      unlink($sOpenSslCfg) if ($sOpenSslCfgGenerated);
      return undef;
    }
    $sClientKvpBCloned = 1;
  }
  $$phPArgs{'Files'}{$sClientKvpB} = $sClientKvpBCloned;

  ####################################################################
  #
  # Create the client's certificate signing request.
  #
  ####################################################################

  my $sClientReqA = $sOutputDirectory . "/" . "client-req-a.pem";
  my $sClientReqB = $sOutputDirectory . "/" . "client-req-b.pem";

  my $sClientReqAGenerated = 0;
  my $sClientReqACloned = 0;
  my $sClientReqBCloned = 0;

  if ($sForceWrite || !-f $sClientReqA || $sClientKeyAGenerated)
  {
    if (!$sForceWrite && !$sClientKeyAGenerated && -f $sClientReqB)
    {
      if (!copy($sClientReqB, $sClientReqA))
      {
        $$phPArgs{'Error'} = "Failed to clone $sClientReqB ($!).";
        unlink($sOpenSslCfg) if ($sOpenSslCfgGenerated);
        return undef;
      }
      $sClientReqACloned = 1;
    }
    else
    {
      my %hLArgs =
      (
        'ForceWrite' => 1,
        'KeyFile' => $sClientKeyA,
        'OpenSslCfg' => $sOpenSslCfg,
        'OpenSslExe' => $sOpenSslExe,
        'ReqFile' => $sClientReqA,
        'Subject' => $$phPArgs{'Subject'},
      );
      if (!DsvCreateRequest(\%hLArgs))
      {
        $$phPArgs{'Error'} = $hLArgs{'Error'};
        unlink($sOpenSslCfg) if ($sOpenSslCfgGenerated);
        return undef;
      }
      $sClientReqAGenerated = 1;
    }
  }
  $$phPArgs{'Files'}{$sClientReqA} = ($sClientReqACloned || $sClientReqAGenerated);

  if ($sForceWrite || !-f $sClientReqB || $sClientReqAGenerated)
  {
    if (!copy($sClientReqA, $sClientReqB))
    {
      $$phPArgs{'Error'} = "Failed to clone $sClientReqA ($!).";
      unlink($sOpenSslCfg) if ($sOpenSslCfgGenerated);
      return undef;
    }
    $sClientReqBCloned = 1;
  }
  $$phPArgs{'Files'}{$sClientReqB} = $sClientReqBCloned;

  ####################################################################
  #
  # Create the client's custom config file.
  #
  ####################################################################

  my $sCustomCfgA = $sOutputDirectory . "/" . "custom-a.cfg";
  my $sCustomCfgB = $sOutputDirectory . "/" . "custom-b.cfg";

  my $sCustomCfgAGenerated = 0;
  my $sCustomCfgACloned = 0;
  my $sCustomCfgBCloned = 0;

  if ($sForceWrite || !-f $sCustomCfgA)
  {
    if (!$sForceWrite && -f $sCustomCfgB)
    {
      if (!copy($sCustomCfgB, $sCustomCfgA))
      {
        $$phPArgs{'Error'} = "Failed to clone $sCustomCfgB ($!).";
        unlink($sOpenSslCfg) if ($sOpenSslCfgGenerated);
        return undef;
      }
      $sCustomCfgACloned = 1;
    }
    else
    {
      if (!open(FH, "> $sCustomCfgA"))
      {
        $$phPArgs{'Error'} = "Failed to open/create \"$sCustomCfgA\" ($!).";
        unlink($sOpenSslCfg) if ($sOpenSslCfgGenerated);
        return undef;
      }
      print FH <<EOF;
EOF
      close(FH);
      $sCustomCfgAGenerated = 1;
    }
  }
  $$phPArgs{'Files'}{$sCustomCfgA} = ($sCustomCfgACloned || $sCustomCfgAGenerated);

  if ($sForceWrite || !-f $sCustomCfgB || $sCustomCfgAGenerated)
  {
    if (!copy($sCustomCfgA, $sCustomCfgB))
    {
      $$phPArgs{'Error'} = "Failed to clone $sCustomCfgA ($!).";
      unlink($sOpenSslCfg) if ($sOpenSslCfgGenerated);
      return undef;
    }
    $sCustomCfgBCloned = 1;
  }
  $$phPArgs{'Files'}{$sCustomCfgB} = $sCustomCfgBCloned;

  ####################################################################
  #
  # Remove the OpenSSL config file, but only if we created it.
  #
  ####################################################################

  unlink($sOpenSslCfg) if ($sOpenSslCfgGenerated);

  ####################################################################
  #
  # Restore the old umask.
  #
  ####################################################################

  umask($sOldUmask);

  1;
}


######################################################################
#
# DsvCreateKey
#
######################################################################

sub DsvCreateKey
{
  my ($phPArgs) = @_;

  ####################################################################
  #
  # Make sure that required inputs are defined.
  #
  ####################################################################

  my %hLArgs =
  (
    'Hash' => $phPArgs,
    'Keys' =>
    [
      'DsvToolExe',
#     'ForceWrite',       # Optional
#     'KeyBits',          # Optional
      'KeyFile',
#     'KeyType',          # Optional
    ],
  );
  if (!defined(VerifyHashKeys(\%hLArgs)))
  {
    $$phPArgs{'Error'} = $hLArgs{'Error'} if (defined($phPArgs));
    return undef;
  }

  ####################################################################
  #
  # Assign default values where necessary.
  #
  ####################################################################

  my $sForceWrite = (exists($$phPArgs{'ForceWrite'}) && $$phPArgs{'ForceWrite'}) ? 1 : 0;

  my $sKeyBits = (defined($$phPArgs{'KeyBits'})) ? $$phPArgs{'KeyBits'} : 1024;

  my $sKeyType = (defined($$phPArgs{'KeyType'})) ? lc($$phPArgs{'KeyType'}) : "rsa";

  ####################################################################
  #
  # Make sure we have the necessary prerequisites.
  #
  ####################################################################

  my $phGlobalRegexes = PropertiesGetGlobalRegexes();

  if ($sKeyBits !~ /^$$phGlobalRegexes{'Decimal16Bit'}$/)
  {
    $$phPArgs{'Error'} = "The number of key bits ($sKeyBits) does not pass muster.";
    return undef;
  }

  if ($sKeyBits % 256 || $sKeyBits < 1024 || $sKeyBits > 4096)
  {
    $$phPArgs{'Error'} = "The number of key bits ($sKeyBits) must be a multiple of 256 and in the range [1024-4096].";
    return undef;
  }

  if ($sKeyType !~ /^(?:dsa|rsa)$/)
  {
    $$phPArgs{'Error'} = "The key type ($sKeyType) must be \"dsa\" or \"rsa\".";
    return undef;
  }

  my $sDsvToolExe = FdaFindExecutable($$phPArgs{'DsvToolExe'});
  if (!defined($sDsvToolExe))
  {
    $$phPArgs{'Error'} = "Executable ($$phPArgs{'DsvToolExe'}) is not executable or could not be found in the system PATH.";
    return undef;
  }

  ####################################################################
  #
  # Prepare the output file handle.
  #
  ####################################################################

  my $sFileHandle;

  if ($$phPArgs{'KeyFile'} eq "-")
  {
    $sFileHandle = \*STDOUT;
  }
  else
  {
    if (-f $$phPArgs{'KeyFile'} && !$sForceWrite)
    {
      $$phPArgs{'Error'} = "The key file ($$phPArgs{'KeyFile'}) already exists.";
      return undef;
    }
    if (!open(FH, "> $$phPArgs{'KeyFile'}"))
    {
      $$phPArgs{'Error'} = "Failed to open/create \"$$phPArgs{'KeyFile'}\" ($!).";
      return undef;
    }
    $sFileHandle = \*FH;
    if (!chmod(0600, $$phPArgs{'KeyFile'}))
    {
      $$phPArgs{'Error'} = "Failed to set permissions on \"$$phPArgs{'KeyFile'}\" ($!).";
      close($sFileHandle);
      unlink($$phPArgs{'KeyFile'}); # Don't leave an empty key file.
      return undef;
    }
  }
  binmode($sFileHandle);

  ####################################################################
  #
  # Generate the key.
  #
  ####################################################################

  my $phGlobalExitCodes = PropertiesGetGlobalExitCodes();
  my $sCommandLine = qq("$sDsvToolExe" --generate-key --type $sKeyType --bits $sKeyBits);
  my $sOutput = qx($sCommandLine);
  my $sStatus = ($? >> 8) & 0xff;
  if ($sStatus != 0)
  {
    my $sFailure = (exists($$phGlobalExitCodes{'webjob-dsvtool'}{$sStatus})) ? $$phGlobalExitCodes{'webjob-dsvtool'}{$sStatus} : $sStatus;
    $$phPArgs{'Error'} = "Command ($sCommandLine) failed ($sFailure).";
    close($sFileHandle);
    unlink($$phPArgs{'KeyFile'}); # Don't leave an empty key file.
    return undef;
  }
  print $sFileHandle $sOutput;
  close($sFileHandle);

  1;
}


######################################################################
#
# DsvCreateRequest
#
######################################################################

sub DsvCreateRequest
{
  my ($phPArgs) = @_;

  ####################################################################
  #
  # Make sure that required inputs are defined.
  #
  ####################################################################

  my %hLArgs =
  (
    'Hash' => $phPArgs,
    'Keys' =>
    [
      'ReqFile',
#     'ForceWrite',       # Optional
      'KeyFile',
      'OpenSslCfg',
      'OpenSslExe',
      'Subject',
    ],
  );
  if (!defined(VerifyHashKeys(\%hLArgs)))
  {
    $$phPArgs{'Error'} = $hLArgs{'Error'} if (defined($phPArgs));
    return undef;
  }

  ####################################################################
  #
  # Assign default values where necessary.
  #
  ####################################################################

  my $sForceWrite = (exists($$phPArgs{'ForceWrite'}) && $$phPArgs{'ForceWrite'}) ? 1 : 0;

  my $sSubject = DsvMakeDistinguishedName(\%{$$phPArgs{'Subject'}}) || "";

  ####################################################################
  #
  # Make sure we have the necessary prerequisites.
  #
  ####################################################################

  if (!-f $$phPArgs{'KeyFile'})
  {
    $$phPArgs{'Error'} = "Key file ($$phPArgs{'KeyFile'}) does not exist or is not regular.";
    return undef;
  }

  if (!-f $$phPArgs{'OpenSslCfg'})
  {
    $$phPArgs{'Error'} = "OpenSSL config file ($$phPArgs{'OpenSslCfg'}) does not exist or is not regular.";
    return undef;
  }

  if ($sSubject eq "")
  {
    $$phPArgs{'Error'} = "The subject must contain at least one field.";
    return undef;
  }

  my $sOpenSslExe = FdaFindExecutable($$phPArgs{'OpenSslExe'});
  if (!defined($sOpenSslExe))
  {
    $$phPArgs{'Error'} = "Executable ($$phPArgs{'OpenSslExe'}) is not executable or could not be found in the system PATH.";
    return undef;
  }

  ####################################################################
  #
  # Prepare the output file handle.
  #
  ####################################################################

  my $sFileHandle;

  if ($$phPArgs{'ReqFile'} eq "-")
  {
    $sFileHandle = \*STDOUT;
  }
  else
  {
    if (-f $$phPArgs{'ReqFile'} && !$sForceWrite)
    {
      $$phPArgs{'Error'} = "The request file ($$phPArgs{'ReqFile'}) already exists.";
      return undef;
    }
    if (!open(FH, "> $$phPArgs{'ReqFile'}"))
    {
      $$phPArgs{'Error'} = "Failed to open/create \"$$phPArgs{'ReqFile'}\" ($!).";
      return undef;
    }
    $sFileHandle = \*FH;
  }
  binmode($sFileHandle);

  ####################################################################
  #
  # Generate the request.
  #
  ####################################################################

  my $sCommandLine = qq("$sOpenSslExe" req -config "$$phPArgs{'OpenSslCfg'}" -new -key "$$phPArgs{'KeyFile'}" -subj "$sSubject");
  my $sOutput = qx($sCommandLine);
  my $sStatus = ($? >> 8) & 0xff;
  if ($sStatus != 0)
  {
    my $sMessage = ($sStatus == 255) ? $! : "exit code was $sStatus";
    $$phPArgs{'Error'} = "Command ($sCommandLine) failed ($sMessage).";
    close($sFileHandle);
    unlink($$phPArgs{'ReqFile'}); # Don't leave an empty request file.
    return undef;
  }
  print $sFileHandle $sOutput;
  close($sFileHandle);

  1;
}


######################################################################
#
# DsvCreateUueBundle
#
######################################################################

sub DsvCreateUueBundle
{
  my ($phPArgs) = @_;

  ####################################################################
  #
  # Make sure that required inputs are defined.
  #
  ####################################################################

  my %hLArgs =
  (
    'Hash' => $phPArgs,
    'Keys' =>
    [
      'DsvCert',
#     'DsvKey',     # Required unless NoSign
#     'DsvToolExe', # Required unless NoSign
      'FileHandle',
      'Files',
#     'KeepSigsInMemory', # Optional
#     'Mode',       # Optional
#     'NoSign',     # Optional
    ],
  );
  push(@{$hLArgs{'Keys'}}, "DsvKey", "DsvToolExe") unless ($$phPArgs{'NoSign'});
  if (!defined(VerifyHashKeys(\%hLArgs)))
  {
    $$phPArgs{'Error'} = $hLArgs{'Error'} if (defined($phPArgs));
    return undef;
  }

  ####################################################################
  #
  # Assign default values where necessary.
  #
  ####################################################################

  my $sKeepSigsInMemory = (exists($$phPArgs{'KeepSigsInMemory'}) && $$phPArgs{'KeepSigsInMemory'}) ? 1 : 0;

  my $sMode = (exists($$phPArgs{'Mode'}) && defined($$phPArgs{'Mode'})) ? $$phPArgs{'Mode'} : undef;

  my $sNoSign = (exists($$phPArgs{'NoSign'}) && $$phPArgs{'NoSign'}) ? 1 : 0;

  ####################################################################
  #
  # Make sure we have the necessary prerequisites.
  #
  ####################################################################

  my ($sDsvToolExe);

  if (!-f $$phPArgs{'DsvCert'})
  {
    $$phPArgs{'Error'} = "Certificate ($$phPArgs{'DsvCert'}) does not exist or is not regular.";
    return undef;
  }

  if (scalar(@{$$phPArgs{'Files'}}) < 1)
  {
    $$phPArgs{'Error'} = "At least one file is required.";
    return undef;
  }

  if (!$sNoSign)
  {
    if (!-f $$phPArgs{'DsvKey'})
    {
      $$phPArgs{'Error'} = "Private key ($$phPArgs{'DsvKey'}) does not exist or is not regular.";
      return undef;
    }

    $sDsvToolExe = FdaFindExecutable($$phPArgs{'DsvToolExe'});
    if (!defined($sDsvToolExe))
    {
      $$phPArgs{'Error'} = "Executable ($$phPArgs{'DsvToolExe'}) is not executable or could not be found in the system PATH.";
      return undef;
    }
  }

  if (defined($sMode) && $sMode !~ /^[0-7]{3}$/)
  {
    $$phPArgs{'Error'} = "Mode must be a string of exactly three octal digits (e.g., 640).";
    return undef;
  }

  ####################################################################
  #
  # Conditionally sign the files going into this bundle, but do them
  # one at a time so that an error is caught as soon as it occurs. If
  # the NoSign option is set, then each signature file must already
  # exist. Note, however, that no signature checks are done here, so
  # it is possible to create a bundle that won't validate. The caller
  # assumes that responsibility by enabling NoSign.
  #
  ####################################################################

  my @aMembersToEncode = ();
  my %hMemberMap = ();

  foreach my $sFile (@{$$phPArgs{'Files'}})
  {
    if (!-f $sFile)
    {
      $$phPArgs{'Error'} = "File ($sFile) does not exist or is not regular.";
      return undef;
    }
    my $sSigData = "";
    my $sSigFile = $sFile . ".sig";
    if ($sNoSign)
    {
      if (!-f $sSigFile)
      {
        $$phPArgs{'Error'} = "File ($sSigFile) does not exist or is not regular, but it is required since the NoSign option was set.";
        return undef;
      }
    }
    else
    {
      my $sCommandLine = qq("$sDsvToolExe" -s -k "$$phPArgs{'DsvKey'}");
      if ($sKeepSigsInMemory)
      {
        $sCommandLine .= qq( - < "$sFile");
      }
      else
      {
        $sCommandLine .= qq( "$sFile" 2>&1);
      }
      my $sOutput = qx($sCommandLine);
      my $sStatus = ($? >> 8) & 0xff;
      if ($sStatus != 0)
      {
        my $phGlobalExitCodes = PropertiesGetGlobalExitCodes();
        my $sFailure = (exists($$phGlobalExitCodes{'webjob-dsvtool'}{$sStatus})) ? $$phGlobalExitCodes{'webjob-dsvtool'}{$sStatus} : $sStatus;
        $$phPArgs{'Error'} = "Command ($sCommandLine) failed ($sFailure).";
        return undef;
      }
      if ($sKeepSigsInMemory)
      {
        $sSigData = $sOutput;
      }
      else
      {
        my $sOctalMode = (defined($sMode)) ? oct($sMode) : ((stat($sFile))[2] & 00777);
        if (!chmod($sOctalMode, $sSigFile))
        {
          $$phPArgs{'Error'} = "Failed to set permissions on \"$sSigFile\" ($!).";
          return undef;
        }
      }
    }
    push(@aMembersToEncode, $sFile);
    $hMemberMap{$sFile} = basename($sFile);
    if ($sKeepSigsInMemory)
    {
      push(@aMembersToEncode, \$sSigData);
      $hMemberMap{\$sSigData} = basename($sSigFile);
    }
    else
    {
      push(@aMembersToEncode, $sSigFile);
      $hMemberMap{$sSigFile} = basename($sSigFile);
    }
  }

  ####################################################################
  #
  # Include the certificate that corresponds to the signing key. Note
  # that every bundle must use a standard name for the certificate --
  # that mapping is done here.
  #
  ####################################################################

  push(@aMembersToEncode, $$phPArgs{'DsvCert'});
  $hMemberMap{$$phPArgs{'DsvCert'}} = "bundle-dsv.pem"; # Required mapping.

  ####################################################################
  #
  # Encode each member and write the data to the specified handle.
  #
  ####################################################################

  foreach my $sItem (@aMembersToEncode)
  {
    my %hLArgs =
    (
      'DataSrc' => $sItem, # Note: This is either a scalar or a scalar reference.
      'DataDst' => $$phPArgs{'FileHandle'},
      'Name'    => $hMemberMap{$sItem},
    );
    $hLArgs{'Mode'} = $sMode if (defined($sMode));
    if (!EadUuEncode(\%hLArgs))
    {
      $$phPArgs{'Error'} = $hLArgs{'Error'};
      return undef;
    }
  }

  1;
}


######################################################################
#
# DsvDeployUueBundle
#
######################################################################

sub DsvDeployUueBundle
{
  my ($phPArgs) = @_;

  ####################################################################
  #
  # Make sure that required inputs are defined.
  #
  ####################################################################

  my %hLArgs =
  (
    'Hash' => $phPArgs,
    'Keys' =>
    [
      'Bundle',
      'DsvToolExe',
#     'ForceWrite',       # Optional
      'OpenSslExe',
#     'OutputDirectory',  # Optional
      'TargetCert',
    ],
  );
  if (!defined(VerifyHashKeys(\%hLArgs)))
  {
    $$phPArgs{'Error'} = $hLArgs{'Error'} if (defined($phPArgs));
    return undef;
  }

  ####################################################################
  #
  # Assign default values where necessary.
  #
  ####################################################################

  my $sForceWrite = (exists($$phPArgs{'ForceWrite'}) && $$phPArgs{'ForceWrite'}) ? 1 : 0;

  ####################################################################
  #
  # Make sure we have the necessary prerequisites.
  #
  ####################################################################

  if (!-f $$phPArgs{'Bundle'})
  {
    $$phPArgs{'Error'} = "Bundle ($$phPArgs{'Bundle'}) does not exist or is not regular.";
    return undef;
  }

  if (!-f $$phPArgs{'TargetCert'})
  {
    $$phPArgs{'Error'} = "File ($$phPArgs{'TargetCert'}) does not exist or is not regular.";
    return undef;
  }

  my $sDsvToolExe = FdaFindExecutable($$phPArgs{'DsvToolExe'});
  if (!defined($sDsvToolExe))
  {
    $$phPArgs{'Error'} = "Executable ($$phPArgs{'DsvToolExe'}) is not executable or could not be found in the system PATH.";
    return undef;
  }

  my $sOpenSslExe = FdaFindExecutable($$phPArgs{'OpenSslExe'});
  if (!defined($sOpenSslExe))
  {
    $$phPArgs{'Error'} = "Executable ($$phPArgs{'OpenSslExe'}) is not executable or could not be found in the system PATH.";
    return undef;
  }

  ####################################################################
  #
  # Create a temporary work area.
  #
  ####################################################################

  my ($sTempDirectory);

  eval { $sTempDirectory = tempdir(CLEANUP => 1); };
  if ($@)
  {
    my $sMessage = $@; $sMessage =~ s/[\r\n]+/ /g; $sMessage =~ s/\s+/ /g; $sMessage =~ s/\s+$//;
    $$phPArgs{'Error'} = "Unable to create a temporary work area ($sMessage).";
    return undef;
  }

  ####################################################################
  #
  # Unpack the uuencoded bundle in a temporary work area.
  #
  ####################################################################

  my ($sActualCert);

  %hLArgs =
  (
    'Bundle' => $$phPArgs{'Bundle'},
    'DsvToolExe' => $sDsvToolExe,
    'OutputDirectory' => $sTempDirectory,
    'VerifySignatures' => 1,
  );
  if (!DsvUnpackUueBundle(\%hLArgs))
  {
    $$phPArgs{'Error'} = $hLArgs{'Error'};
    return undef;
  }

  $sActualCert = $hLArgs{'Members'}{'bundle-dsv.pem'};

  ####################################################################
  #
  # Make sure the bundle the certificate is trusted.
  #
  ####################################################################

  %hLArgs =
  (
    'OpenSslExe' => $sOpenSslExe,
    'ActualCert' => $sActualCert,
    'TargetCert' => $$phPArgs{'TargetCert'},
  );
  if (!DsvCompareCertificates(\%hLArgs))
  {
    $$phPArgs{'Error'} = $hLArgs{'Error'};
    return undef;
  }

  ####################################################################
  #
  # Unpack the uuencoded bundle in the output directory.
  #
  ####################################################################

  %hLArgs =
  (
    'Bundle' => $$phPArgs{'Bundle'},
    'DsvToolExe' => $sDsvToolExe,
    'ExcludeMembers' => [ "bundle-dsv.pem" ],
    'ForceWrite' => $sForceWrite,
    'Members' => $$phPArgs{'Members'},
    'OutputDirectory' => $$phPArgs{'OutputDirectory'},
    'VerifySignatures' => 0,
    'VerifyStructure' => 0, # This is required because members are being excluded.
  );
  if (!DsvUnpackUueBundle(\%hLArgs))
  {
    $$phPArgs{'Error'} = $hLArgs{'Error'};
    return undef;
  }

  1;
}


######################################################################
#
# DsvGetDistinguishedName
#
######################################################################

sub DsvGetDistinguishedName
{
  my ($phPArgs) = @_;

  ####################################################################
  #
  # Make sure that required inputs are defined.
  #
  ####################################################################

  my %hLArgs =
  (
    'Hash' => $phPArgs,
    'Keys' =>
    [
      'Entity',
      'File',
      'OpenSslExe',
#     'Type', # Optional
    ],
  );
  if (!defined(VerifyHashKeys(\%hLArgs)))
  {
    $$phPArgs{'Error'} = $hLArgs{'Error'} if (defined($phPArgs));
    return undef;
  }

  ####################################################################
  #
  # Assign default values where necessary.
  #
  ####################################################################

  my $sType = (defined($$phPArgs{'Type'})) ? lc($$phPArgs{'Type'}) : "x509";

  ####################################################################
  #
  # Make sure we have the necessary prerequisites.
  #
  ####################################################################

  if (!-f $$phPArgs{'File'})
  {
    $$phPArgs{'Error'} = "File ($$phPArgs{'File'}) does not exist or is not regular.";
    return undef;
  }

  if ($sType !~ /^(?:req|x509)$/)
  {
    $$phPArgs{'Error'} = "Type ($sType) must be \"req\" or \"x509\".";
    return undef;
  }

  my $sOpenSslExe = FdaFindExecutable($$phPArgs{'OpenSslExe'});
  if (!defined($sOpenSslExe))
  {
    $$phPArgs{'Error'} = "Executable ($$phPArgs{'OpenSslExe'}) is not executable or could not be found in the system PATH.";
    return undef;
  }

  ####################################################################
  #
  # Get the subject.
  #
  ####################################################################

  my ($sCommandLine, $sEntity, $sEntityOption, $sOutput, $sStatus);

  if ($$phPArgs{'Entity'} =~ /^issuer$/i)
  {
    $sEntity = "Issuer";
    $sEntityOption = "issuer";
  }
  elsif ($$phPArgs{'Entity'} =~ /^subject$/i)
  {
    $sEntity = "Subject";
    $sEntityOption = "subject";
  }
  else
  {
    $$phPArgs{'Error'} = "Entity ($$phPArgs{'Entity'}) is not known/supported.";
    return undef;
  }

  $sCommandLine = qq("$sOpenSslExe" $sType -noout -$sEntityOption -in "$$phPArgs{'File'}");
  if ($sType eq "req")
  {
    $sCommandLine .= ($^O =~ /MSWin(32|64)/i) ? qq( -config "nul") : qq( -config "/dev/null");
  }
  $sOutput = qx($sCommandLine);
  $sStatus = ($? >> 8) & 0xff;
  if ($sStatus != 0)
  {
    $$phPArgs{'Error'} = "Failed to obtain subject ($sStatus).";
    return undef;
  }
  $sOutput .= "";
  if ($sOutput !~ /^(?:issuer|subject)=\s*(.+)$/)
  {
    $$phPArgs{'Error'} = "OpenSSL output did not pass muster.";
    return undef;
  }
  my $sDistinguishedName = $1;

  ####################################################################
  #
  # Parse the distinguished name, and build an entity hash.
  #
  ####################################################################

  my ($sError);

  if (!DsvParseDistinguishedName($sDistinguishedName, \%{$$phPArgs{$sEntity}}, \$sError))
  {
    $$phPArgs{'Error'} = "Failed to parse distinguished name ($sError).";
    return undef;
  }

  ####################################################################
  #
  # Reconstruct the distinguished name in a consistent way. This is
  # important for consumers who use this information to compare DNs
  # taken from different files.
  #
  ####################################################################

  my $sReconstructedDn = DsvMakeDistinguishedName(\%{$$phPArgs{$sEntity}});
  if (!defined($sReconstructedDn))
  {
    $$phPArgs{'Error'} = "Distinguished name is not defined. That should not happen!";
    return undef;
  }

  return $sReconstructedDn;
}


######################################################################
#
# DsvGetFingerprint
#
######################################################################

sub DsvGetFingerprint
{
  my ($phPArgs) = @_;

  ####################################################################
  #
  # Make sure that required inputs are defined.
  #
  ####################################################################

  my %hLArgs =
  (
    'Hash' => $phPArgs,
    'Keys' =>
    [
      'File',
      'OpenSslExe',
    ],
  );
  if (!defined(VerifyHashKeys(\%hLArgs)))
  {
    $$phPArgs{'Error'} = $hLArgs{'Error'} if (defined($phPArgs));
    return undef;
  }

  ####################################################################
  #
  # Assign default values where necessary.
  #
  ####################################################################

  ####################################################################
  #
  # Make sure we have the necessary prerequisites.
  #
  ####################################################################

  if (!-f $$phPArgs{'File'})
  {
    $$phPArgs{'Error'} = "File ($$phPArgs{'File'}) does not exist or is not regular.";
    return undef;
  }

  my $sOpenSslExe = FdaFindExecutable($$phPArgs{'OpenSslExe'});
  if (!defined($sOpenSslExe))
  {
    $$phPArgs{'Error'} = "Executable ($$phPArgs{'OpenSslExe'}) is not executable or could not be found in the system PATH.";
    return undef;
  }

  ####################################################################
  #
  # Get fingerprint.
  #
  ####################################################################

  my ($sOutput, $sStatus);

  $sOutput = qx("$sOpenSslExe" x509 -noout -fingerprint -in "$$phPArgs{'File'}");
  $sStatus = ($? >> 8) & 0xff;
  if ($sStatus != 0)
  {
    $$phPArgs{'Error'} = "Failed to execute OpenSSL fingerprint command ($sStatus).";
    return undef;
  }
  $sOutput .= "";
  if ($sOutput !~ /^(?:MD5|SHA1?)\s+Fingerprint=(.+)$/)
  {
    $$phPArgs{'Error'} = "Failed to parse target fingerprint output.";
    return undef;
  }
  my $sFingerprint = $1;

  return $sFingerprint;
}


######################################################################
#
# DsvGetIdentityKvps
#
######################################################################

sub DsvGetIdentityKvps
{
  my ($phPArgs) = @_;

  ####################################################################
  #
  # Make sure that required inputs are defined.
  #
  ####################################################################

  my %hLArgs =
  (
    'Hash' => $phPArgs,
    'Keys' =>
    [
      'ClientDsvFile',
      'ClientKvpFile',
      'ClientReqFile',
      'OpenSslExe',
    ],
  );
  if (!defined(VerifyHashKeys(\%hLArgs)))
  {
    $$phPArgs{'Error'} = $hLArgs{'Error'} if (defined($phPArgs));
    return undef;
  }

  ####################################################################
  #
  # Assign default values where necessary.
  #
  ####################################################################

  ####################################################################
  #
  # Make sure we have the necessary prerequisites.
  #
  ####################################################################

  my $sOpenSslExe = FdaFindExecutable($$phPArgs{'OpenSslExe'});
  if (!defined($sOpenSslExe))
  {
    $$phPArgs{'Error'} = "Executable ($$phPArgs{'OpenSslExe'}) is not executable or could not be found in the system PATH.";
    return undef;
  }

  ####################################################################
  #
  # Get the key/value pairs supplied by the client.
  #
  ####################################################################

  my (%hClientKvps, $phGlobalRegexes);

  $phGlobalRegexes = PropertiesGetGlobalRegexes();

  if (-f $$phPArgs{'ClientKvpFile'})
  {
    my %hLArgs =
    (
      'File'           => $$phPArgs{'ClientKvpFile'},
      'Properties'     => \%hClientKvps,
      'RequireAllKeys' => 1,
      'Template'       =>
      {
        'ClientId'     => $$phGlobalRegexes{'ClientId'},
        'Hostname'     => $$phGlobalRegexes{'Host'},
        'OsClass'      => $$phGlobalRegexes{'OsClass'},
        'RegistrationCode' => $$phGlobalRegexes{'RegistrationCode'},
        'WebJobHome'   => $$phGlobalRegexes{'AbsolutePath'},
      },
      'VerifyValues'   => 1,
    );
    if (!KvpGetKvps(\%hLArgs))
    {
      $$phPArgs{'Error'} = $hLArgs{'Error'};
      return undef;
    }
  }

  ####################################################################
  #
  # Get the DSV certificate fingerprint, subject, and common name.
  #
  ####################################################################

  if (-f $$phPArgs{'ClientDsvFile'})
  {
    my %hLArgs =
    (
      'File' => $$phPArgs{'ClientDsvFile'},
      'OpenSslExe' => $sOpenSslExe,
    );
    $hClientKvps{'DsvFingerprint'} = DsvGetFingerprint(\%hLArgs);
    if (!defined($hClientKvps{'DsvFingerprint'}))
    {
      $$phPArgs{'Error'} = $hLArgs{'Error'};
      return undef;
    }
    %hLArgs =
    (
      'File' => $$phPArgs{'ClientDsvFile'},
      'OpenSslExe' => $sOpenSslExe,
      'Type' => "x509",
    );
    $hClientKvps{'DsvSubject'} = DsvGetSubject(\%hLArgs);
    if (!defined($hClientKvps{'DsvSubject'}))
    {
      $$phPArgs{'Error'} = $hLArgs{'Error'};
      return undef;
    }
    $hClientKvps{'DsvCommonName'} = $hLArgs{'Subject'}{'CN'};
  }
  else
  {
    $$phPArgs{'Error'} = "File ($$phPArgs{'ClientDsvFile'}) does not exist or is not regular.";
    return undef;
  }

  ####################################################################
  #
  # Get the SSL certificate signing request subject and common name.
  #
  ####################################################################

  if (-f $$phPArgs{'ClientReqFile'})
  {
    %hLArgs =
    (
      'File' => $$phPArgs{'ClientReqFile'},
      'OpenSslExe' => $sOpenSslExe,
      'Type' => "req",
    );
    $hClientKvps{'CsrSubject'} = DsvGetSubject(\%hLArgs);
    if (!defined($hClientKvps{'CsrSubject'}))
    {
      $$phPArgs{'Error'} = $hLArgs{'Error'};
      return undef;
    }
    $hClientKvps{'CsrCommonName'} = $hLArgs{'Subject'}{'CN'};
  }
  else
  {
    $$phPArgs{'Error'} = "File ($$phPArgs{'ClientReqFile'}) does not exist or is not regular.";
    return undef;
  }

  ####################################################################
  #
  # Make sure that the client ID and both common names match.
  #
  ####################################################################

  if ($hClientKvps{'ClientId'} ne $hClientKvps{'DsvCommonName'})
  {
    $$phPArgs{'Error'} = "Client ID vs. DSV common name mismatch ($hClientKvps{'ClientId'} != $hClientKvps{'DsvCommonName'}).";
    return undef;
  }

  if ($hClientKvps{'ClientId'} ne $hClientKvps{'CsrCommonName'})
  {
    $$phPArgs{'Error'} = "Client ID vs. SSL common name mismatch ($hClientKvps{'ClientId'} != $hClientKvps{'CsrCommonName'}).";
    return undef;
  }

  return \%hClientKvps;
}


######################################################################
#
# DsvGetIssuer
#
######################################################################

sub DsvGetIssuer
{
  my ($phPArgs) = @_;

  $$phPArgs{'Entity'} = "issuer";

  return DsvGetDistinguishedName($phPArgs);
}


######################################################################
#
# DsvGetSubject
#
######################################################################

sub DsvGetSubject
{
  my ($phPArgs) = @_;

  $$phPArgs{'Entity'} = "subject";

  return DsvGetDistinguishedName($phPArgs);
}


######################################################################
#
# DsvMakeDistinguishedName
#
######################################################################

sub DsvMakeDistinguishedName
{
  my ($phEntity) = @_;

  ####################################################################
  #
  # Join entity hash elements to make a DN string.
  #
  ####################################################################

  my ($sDistinguishedName);

  $sDistinguishedName .=  "/C=" . $$phEntity{'C'}  if (defined($$phEntity{'C'}));
  $sDistinguishedName .= "/ST=" . $$phEntity{'ST'} if (defined($$phEntity{'ST'}));
  $sDistinguishedName .=  "/L=" . $$phEntity{'L'}  if (defined($$phEntity{'L'}));
  $sDistinguishedName .=  "/O=" . $$phEntity{'O'}  if (defined($$phEntity{'O'}));
  $sDistinguishedName .= "/OU=" . $$phEntity{'OU'} if (defined($$phEntity{'OU'}));
  $sDistinguishedName .= "/CN=" . $$phEntity{'CN'} if (defined($$phEntity{'CN'}));
  $sDistinguishedName .= "/emailAddress=" . $$phEntity{'emailAddress'} if (defined($$phEntity{'emailAddress'}));

  return $sDistinguishedName;
}


######################################################################
#
# DsvParseDistinguishedName
#
######################################################################

sub DsvParseDistinguishedName
{
  my ($sDistinguishedName, $phEntity, $psError) = @_;

  ####################################################################
  #
  # Split a DN string into pieces to make an entity hash.
  #
  ####################################################################

  my (%hFieldCodes);

  %hFieldCodes =
  (
    'C'                      => 'C',
    'CN'                     => 'CN',
    'L'                      => 'L',
    'O'                      => 'O',
    'OU'                     => 'OU',
    'ST'                     => 'ST',
    'countryName'            => 'C',
    'commonName'             => 'CN',
    'emailAddress'           => 'emailAddress',
    'localityName'           => 'L',
    'organizationName'       => 'O',
    'organizationalUnitName' => 'OU',
    'stateOrProvinceName'    => 'ST',
  );

  $sDistinguishedName =~ s,^/,,; # Remove the leading slash.

  foreach my $sKvp (split(/\//, $sDistinguishedName))
  {
    $sKvp =~ s/[\r\n]+$//; # Remove CRs and LFs.
    $sKvp =~ s/^\s+//; # Remove leading whitespace.
    if (my ($sKey, $sValue) = ($sKvp =~ /^([A-Za-z]+)=(.+)$/))
    {
      if (exists($hFieldCodes{$sKey}))
      {
        $sValue =~ s/^\s+//; # Remove leading whitespace.
        $sValue =~ s/\s+$//; # Remove trailing whitespace.
        $$phEntity{$hFieldCodes{$sKey}} = $sValue;
      }
      else
      {
        $$psError = "\"$sKey\" is an invalid/unknown field code";
        return undef;
      }
    }
    else
    {
      $$psError = "unable to parse \"$sKvp\" as a key/value pair";
      return undef;
    }
  }

  1;
}


######################################################################
#
# DsvUnpackUueBundle
#
######################################################################

sub DsvUnpackUueBundle
{
  my ($phPArgs) = @_;

  ####################################################################
  #
  # Make sure that required inputs are defined.
  #
  ####################################################################

  my %hLArgs =
  (
    'Hash' => $phPArgs,
    'Keys' =>
    [
      'Bundle',
      'DsvToolExe',
#     'ExcludeMembers',   # Optional
#     'ForceWrite',       # Optional
#     'IncludeMembers',   # Optional
#     'MemberMap',        # Optional
#     'OutputDirectory',  # Optional
#     'VerifySignatures', # Optional
#     'VerifyStructure',  # Optional
    ],
  );
  if (!defined(VerifyHashKeys(\%hLArgs)))
  {
    $$phPArgs{'Error'} = $hLArgs{'Error'} if (defined($phPArgs));
    return undef;
  }

  ####################################################################
  #
  # Assign default values where necessary.
  #
  ####################################################################

  my $sOutputDirectory = (defined($$phPArgs{'OutputDirectory'})) ? $$phPArgs{'OutputDirectory'} : cwd();

  my $sVerifySignatures = (exists($$phPArgs{'VerifySignatures'}) && !$$phPArgs{'VerifySignatures'}) ? 0 : 1;

  my $sVerifyStructure = (exists($$phPArgs{'VerifyStructure'}) && !$$phPArgs{'VerifyStructure'}) ? 0 : 1;

  ####################################################################
  #
  # Make sure we have the necessary prerequisites.
  #
  ####################################################################

  if (!-f $$phPArgs{'Bundle'})
  {
    $$phPArgs{'Error'} = "Bundle ($$phPArgs{'Bundle'}) does not exist or is not regular.";
    return undef;
  }

  if (!$sVerifyStructure && $sVerifySignatures)
  {
    $$phPArgs{'Error'} = "If structure verification is explicitly disabled, then signature verification must be explicitly disbled as well.";
    return undef;
  }

  my $sDsvToolExe = FdaFindExecutable($$phPArgs{'DsvToolExe'});
  if (!defined($sDsvToolExe))
  {
    $$phPArgs{'Error'} = "Executable ($$phPArgs{'DsvToolExe'}) is not executable or could not be found in the system PATH.";
    return undef;
  }

  ####################################################################
  #
  # Determine which version of webjob-dsvtool is in play.
  #
  ####################################################################
   
  my ($sCommandLine, $sOutput, $sStatus, $sVersionNumber);

  $sCommandLine = qq("$sDsvToolExe" --version);
  $sOutput = qx($sCommandLine);
  $sStatus = ($? >> 8) & 0xff;
  if ($sStatus != 0)
  {
    my $phGlobalExitCodes = PropertiesGetGlobalExitCodes();
    my $sFailure = (exists($$phGlobalExitCodes{'webjob-dsvtool'}{$sStatus})) ? $$phGlobalExitCodes{'webjob-dsvtool'}{$sStatus} : $sStatus;
    $$phPArgs{'Error'} = "Command ($sCommandLine) failed ($sFailure).";
    return undef;
  }
  $sOutput =~ s/-dsvtool//; # Hack 1 to make version string look like it came from webjob.
  $sOutput =~ s/,.*$/ dsv/; # Hack 2 to make version string look like it came from webjob.
  %hLArgs =
  (
    'VersionOutput' => $sOutput,
  );
  if (!VersionParseOutput(\%hLArgs))
  {
    $$phPArgs{'Error'} = $hLArgs{'Error'};
    return undef;
  }
  $sVersionNumber = $hLArgs{'VersionNumber'};

  ####################################################################
  #
  # Conditionally create the output directory.
  #
  ####################################################################

  if (!-d $sOutputDirectory)
  {
    eval { mkpath($sOutputDirectory, 0, 0700) };
    if ($@)
    {
      my $sMessage = $@; $sMessage =~ s/[\r\n]+/ /g; $sMessage =~ s/\s+/ /g; $sMessage =~ s/\s+$//;
      $$phPArgs{'Error'} = "Failed to create \"$sOutputDirectory\" ($sMessage).";
      return undef;
    }
  }

  ####################################################################
  #
  # Unpack the bundle.
  #
  ####################################################################

  %hLArgs =
  (
    'AbortOnError' => 1,
    'DataSrc' => $$phPArgs{'Bundle'},
    'OutputDirectory' => $sOutputDirectory,
  );
  $hLArgs{'ExcludeMembers'} = $$phPArgs{'ExcludeMembers'} if (exists($$phPArgs{'ExcludeMembers'}));
  $hLArgs{'ForceWrite'} = $$phPArgs{'ForceWrite'} if (exists($$phPArgs{'ForceWrite'}));
  $hLArgs{'IncludeMembers'} = $$phPArgs{'IncludeMembers'} if (exists($$phPArgs{'IncludeMembers'}));
  $hLArgs{'MemberMap'} = $$phPArgs{'MemberMap'} if (exists($$phPArgs{'MemberMap'}));

  my $sResult = EadUuDecode(\%hLArgs);
  if (!defined($sResult))
  {
    $$phPArgs{'Error'} = $hLArgs{'Error'};
    return undef;
  }

  ####################################################################
  #
  # Conditionally compensate for mapped member names.
  #
  ####################################################################

  my $sBundleDsv = "bundle-dsv.pem";

  if (exists($$phPArgs{'MemberMap'}{'bundle-dsv.pem'}))
  {
    $sBundleDsv = $$phPArgs{'MemberMap'}{'bundle-dsv.pem'};
  }

  ####################################################################
  #
  # Inspect the files that were extracted. The bundle should contain
  # one certificate and one or more payload pairs. A payload pair is
  # an original payload file plus its corresponding signature file.
  #
  ####################################################################

  my @aSigFiles = ();

  foreach my $paMemberTuple (@{$hLArgs{'MemberTuples'}})
  {
    my ($sStatus, $sFile, $sMessage) = @$paMemberTuple;
    my $sName = basename($sFile);
    $$phPArgs{'Members'}{$sName} = $sFile;
    push(@aSigFiles, $sFile) if ($sFile =~ /\.sig$/);
  }

  if ($sVerifyStructure)
  {
    my $sSigCount = scalar(@aSigFiles);
    if ($sSigCount < 1)
    {
      $$phPArgs{'Error'} = "Invalid signature count ($sSigCount). There must be one signature for each payload.";
      return undef;
    }

    my $sActualMemberCount = scalar(@{$hLArgs{'MemberTuples'}});
    my $sTargetMemberCount = ($sSigCount * 2) + 1;
    if ($sActualMemberCount != $sTargetMemberCount)
    {
      $$phPArgs{'Error'} = "Member count mismatch ($sActualMemberCount != $sTargetMemberCount).";
      return undef;
    }

    if (!exists($$phPArgs{'Members'}{$sBundleDsv}))
    {
      $$phPArgs{'Error'} = "Bundle certificate is missing.";
      return undef;
    }
  }

  ####################################################################
  #
  # Adjust path separators if running on a Windows platform.
  #
  ####################################################################

  if ($^O =~ /MSWin(32|64)/i)
  {
    foreach my $sName (keys(%{$$phPArgs{'Members'}}))
    {
      $$phPArgs{'Members'}{$sName} =~ s,/,\\,g;
    }
  }

  ####################################################################
  #
  # Conditionally verify signatures.
  #
  ####################################################################

  if ($sVerifySignatures)
  {
    foreach my $sSigFile (@aSigFiles)
    {
      (my $sChkFile = $sSigFile) =~ s/[.]sig$//;
      my $sCommandLine = qq("$sDsvToolExe" -c -f "$$phPArgs{'Members'}{$sBundleDsv}");
      $sCommandLine .= qq( "$sChkFile") if ($sVersionNumber <= 0x10300000); # Use legacy syntax for any version before 1.3.0.
      $sCommandLine .= qq( "$sSigFile" 2>&1);
      my @aOutput = qx($sCommandLine);
      my $sStatus = ($? >> 8) & 0xff;
      if ($sStatus != 0)
      {
        if ($sStatus == 5)
        {
          $$phPArgs{'Error'} = "File ($sChkFile) failed signature check.";
        }
        else
        {
          my $sMessage = $aOutput[0]; $sMessage =~ s/[\r\n]+/ /g; $sMessage =~ s/\s+/ /g; $sMessage =~ s/\s+$//;
          $$phPArgs{'Error'} = "File ($sChkFile) failed signature check with an error ($sMessage).";
        }
        return undef;
      }
    }
  }

  1;
}

1;

__END__

=pod

=head1 NAME

WebJob::DsvRoutines - Home for various DSV-related routines

=head1 SYNOPSIS

    use WebJob::DsvRoutines;

=head1 DESCRIPTION

This module is a collection of various DSV-related routines designed
to support various WebJob utilities.  As such, minimal effort was put
into supporting this code for general consumption.  In other words,
use at your own risk and don't expect the interface to remain the same
or backwards compatible from release to release.  This module does not
provide an OO interface, nor will it do so anytime soon.

=head1 AUTHOR

Klayton Monroe

=head1 LICENSE

All documentation and code are distributed under same terms and
conditions as B<WebJob>.

=cut
