Upload Local Certificates and Keys

Problem this snippet solves:

An script that uploads to a BIG-IP certificates and keys stored locally in PEM file format.

Code :

#!/usr/bin/perl
#----------------------------------------------------------------------------
# The contents of this file are subject to the iControl Public License
# Version 4.5 (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://www.f5.com/.
#
# Software distributed under the License is distributed on an "AS IS"
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
# the License for the specific language governing rights and limitations
# under the License.
#
# The Original Code is iControl Code and related documentation
# distributed by F5.
#
# The Initial Developer of the Original Code is F5 Networks,
# Inc. Seattle, WA, USA. Portions created by F5 are Copyright (C) 1996-2010 F5 Networks,
# Inc. All Rights Reserved.  iControl (TM) is a registered trademark of F5 Networks, Inc.
#
# Alternatively, the contents of this file may be used under the terms
# of the GNU General Public License (the "GPL"), in which case the
# provisions of GPL are applicable instead of those above.  If you wish
# to allow use of your version of this file only under the terms of the
# GPL and not to allow others to use your version of this file under the
# License, indicate your decision by deleting the provisions above and
# replace them with the notice and other provisions required by the GPL.
# If you do not delete the provisions above, a recipient may use your
# version of this file under either the License or the GPL.
#----------------------------------------------------------------------------

use strict;
use warnings;

use SOAP::Lite;
use MIME::Base64;
use Getopt::Long;
use FileHandle;

# https://devcentral.f5.com/s/wiki/iControl.Perl-SOAP-Lite-TypeCast-package.ashx
# Must be in lib path (use PERL5LIB env or 'use lib')
#
use iControlTypeCast;


=head1 NAME

bigip_cert_upload - Upload local PEM-encoded certificate and/or key to a BIG-IP using SOAP iControl

=head1 SYNOPSIS

 bigip_cert_upload  cert:[:id]|key:[:id] [ ...]
                   [-u ] [-p ] [-m ] [--[no]overwrite]
                   [--help]

=head1 DESCRIPTION

Upload PEM encoded certificate and/or keys to a BIG-IP using iControl SOAP.  More than one
certificate and/or key may be uploaded.  The targets for upload are identified as the
literal 'cert:' or 'key:' followed by the file name.  The file name may optionally be followed
by a colon (:), then the BIG-IP object identifier for the target.  If it is omitted, then the
filename is used after trimming any of ".crt", ".key", ".crt.pem", ".key.pem" or ".pem".
If a certificate and key have the same object identifier then they will be
associated on the BIG-IP.

The iControl connection is initiated against the provided I.  It assumes
https over port 443.  If no I is provided, the user B is assumed.  If no
I is provided, then you will be prompted for it (without stty echo).

The I must be one of: DEFAULT, WEBSERVER, EM, IQUERY, IQUERY_BIG3D.  These map directly
to the ManagementModeType.  See https://devcentral.f5.com/s/wiki/iControl.Management__KeyCertificate__ManagementModeType.ashx.
If no mode is provided, the mode is "DEFAULT".

If I<--overwrite> is provided, then an existing certificate and/or key on the system
matching the identifier of an uploaded certificate and/or key will be overwritten.
The default is to not overwrite.  This may be made explicit using I<--nooverwrite>.

=head1 EXAMPLE

 bigip_cert_upload 10.1.20.1 cert:www.example.com.crt cert:www.f5.com.crt:f5
                   key:www.example.com.key -p 'sUp3r.S#CRET' --overwrite

=cut

my $username  = "admin";
my $password;
my $mode      = "DEFAULT";
my $overwrite = 0;


GetOptions(
    "user:s"            => \$username,
    "passwd:s"          => \$password,
    "mode:s"            => \$mode,
    "overwrite!"        => \$overwrite,
    "help"              => \my $needs_help,
) or die Syntax();

die Syntax()                    if $needs_help;

my $bigip_mgmt_ip = shift       or die Syntax();

validate_ip( $bigip_mgmt_ip )   or die "Invalid target IP [$bigip_mgmt_ip]\n";

my @arg_targets = @ARGV         or die Syntax();

validate_mode( $mode )          or die "Invalid mode [$mode]\n";

# indexed by $target_id, values are hashref by "key" and "cert", values are value of files (that is,
# the PEM blob)
my %targets = process_targets( @arg_targets );

$password = get_password_by_prompting()   unless defined $password;


my $soap_requestor = initiate_soap_requestor( $bigip_mgmt_ip, $username, $password );

foreach my $target_id (keys %targets) {
    if (exists $targets{$target_id}{cert}) {
        eval {
            upload_certificate( $soap_requestor, $target_id, $targets{$target_id}{cert}, $mode, $overwrite )
                if exists $targets{$target_id}{cert};
        };

        if ($@) { warn "Failed to upload certificate with target id [$target_id]: $@"; }
        else    { print "Successfully uploaded certificate with target id [$target_id]\n"; }
    }

    if (exists $targets{$target_id}{key}) {
        eval {
            upload_key( $soap_requestor, $target_id, $targets{$target_id}{key}, $mode, $overwrite )
                if exists $targets{$target_id}{key};
        };

        if ($@) { warn "Failed to upload key with target id [$target_id]: $@"; }
        else    { print "Successfully uploaded key with target id [$target_id]\n"; }
    }
}


###
#  Obligatory SOAP::Lite definition for HTTP Basic Challenge.  Lamentably,
#  this means we must treate $username and $password as globals here, but such is
#  the nature of SOAP::Lite's interface
#####
sub SOAP::Transport::HTTP::Client::get_basic_credentials {
    return "$username" => "$password";
}



########
#
# $b = validate_ip( $ip );
#
# Return true if $ip is a valid IPv4 address; false otherwise
#
########
sub validate_ip {
    my $octet = qr/25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9]/o;
    return shift =~ /^($octet\.){3}$octet$/;
}


########
#
# $b = validate_mode( $mode );
#
# Return true if $mode is DEFAULT, WEBSERVER, EM, IQUERY or IQUERY_BIG3D.  Return
# false otherwise.
#
########
sub validate_mode {
    my $mode = shift;

    return $mode eq "DEFAULT" || $mode eq "WEBSERVER" || $mode eq "EM" ||
           $mode eq "IQUERY"  || $mode eq "IQUERY_BIG3D";
}


########
#
# %t = process_targets( @targets );
#
# Given a series of @targets of the format ":[:]" (where  is
# "cert" or "key",  is a filename, and I is the target identifier -- and
# is optional -- create a hash indexed by target id (which is the filename less
# a trailing token of ".pem", ".crt", ".key", ".crt.pem" or ".key.pem", if the
# target id is not explicitly specified).  The value of the hash is also a hashref
# indexed by "cert" and "key".  One or the other may be absent if there is no
# cert or key with the given target id.  The second-order value is the contents
# of the file.
#
# If the format of a member of @target is invalid or if the file named provided does
# not exist or is not readable, then die() with an appropiate error.  If the file
# contents do not contain a PEM preamble and trailer, or if it contains characters
# outside the base-64 encoding set, it will also die() with an appropriate error.
#
########
sub process_targets {
    my %targets;

    foreach my $specifier (@_) {
        if ($specifier =~ /^(cert|key):(.+?)(:([^:]+))?$/) {
            my ($type, $file, $id) = ($1, $2, $4);

            my $fh = new FileHandle $file
                or die "Failed to open file [$file] for reading: $!\n";

            my $contents = join( "", (<$fh>) );

            close $fh;

            unless ($contents =~ /^-----BEGIN (CERTIFICATE|PRIVATE KEY)-----[A-Za-z0-9=\+\/\r\n]+-----END (CERTIFICATE|PRIVATE KEY)-----$/ms) {
                die "The file [$file] does not appear to contain a valid PEM encoded $type\n";
            }

            if (!defined $id) {
                $id = $file;

                $id =~ s/\.pem$//;
                $id =~ s/(\.crt)|(\.key)$//;
            }

            $targets{$id}{$type} = $contents;
        }
        else {
            die "Target specifier [$specifier] is not properly formatted\n";
        }
    }

    return %targets;
}


########
#
# $passwd = get_password_by_prompting();
#
# Prompt for a password, disabling stty echo on input.  Generally, the stty
# suppression will only work on a standard Unix terminal.  Use Term::ReadKey
# if you want something more portable.
#
########
sub get_password_by_prompting {
    print "Enter password: ";
    system( "stty", "-echo" ) == 0
        or do {
            warn "Cannot disable terminal echo so password will be visible when typed!\n";
            print "Enter password: ";
        };

    my $password = ;
    chomp $password;

    system( "stty", "echo" ) == 0
        or die "Failed to re-enable terminal echo\n";   # probably won't actually be visible...

    return $password;
}


########
#
# $s = initiate_soap_requestor( $remote_ip, $username, $passwd );
#
# Create a SOAP::Lite request object intended to connect to $remote_ip
# using $username and $passwd as credentials, and return the object, if successful.
# On failure, die() with an appropriate error
#
########
sub initiate_soap_requestor {
    my ($remote_ip, $username, $password) = @_;

    my $soap_requestor = SOAP::Lite
      ->uri  ( "urn:iControl:Management/KeyCertificate" )
      ->proxy( "https://$remote_ip/iControl/iControlPortal.cgi" );

    eval {
        $soap_requestor->transport->http_request->header(
            'Authorization' => 'Basic ' . MIME::Base64::encode( "$username:$password", "" )
        );
    };

    die "SOAP Request Creation failure: $@\n"   if $@;

    return $soap_requestor;
}


########
#
# upload_certificate( $soap_requestor, $target_id, $pem_blob, $mode, $overwrite );
#
# Attempt to upload the $pem_blob using the certificate_import_from_pem
# iControl method, issued from the $soap_requestor.  If there is a
# failure, die(), setting $@ to an error string describing the failure.
#
########
sub upload_certificate {
    my ($soap_requestor, $target_id, $pem_blob, $mode, $overwrite) = @_;

    # we *could* upload all certificates at once, but it would be more difficult to determine
    # and report failures
    #
    my $soap_response = $soap_requestor->certificate_import_from_pem(
                            SOAP::Data->name( mode      => "MANAGEMENT_MODE_$mode"   ),
                            SOAP::Data->name( cert_ids  => [$target_id]              ),
                            SOAP::Data->name( pem_data  => [$pem_blob]               ),
                            SOAP::Data->name( overwrite => $overwrite                ),
                        );

    if ($soap_response->fault) {
        die "Failed to retrieve response: ${ \$soap_response->faultcode }: ${ \$soap_response->faultstring }\n";
    }
}



########
#
# upload_key( $soap_requestor, $target_id, $pem_blob, $mode, $overwrite );
#
# Attempt to upload the $pem_blob using the key_import_from_pem
# iControl method, issued from the $soap_requestor.  If there is a
# failure, die(), setting $@ to an error string describing the failure.
#
########
sub upload_key {
    my ($soap_requestor, $target_id, $pem_blob, $mode, $overwrite) = @_;

    my $soap_response = $soap_requestor->key_import_from_pem(
                            SOAP::Data->name( mode      => "MANAGEMENT_MODE_$mode"   ),
                            SOAP::Data->name( key_ids   => [$target_id]              ),
                            SOAP::Data->name( pem_data  => [$pem_blob]               ),
                            SOAP::Data->name( overwrite => $overwrite                ),
                        );

    if ($soap_response->fault) {
        die "Failed to retrieve response: ${ \$soap_response->faultcode }: ${ \$soap_response->faultstring }\n";
    }
}


sub Syntax {
    my $a = $0;
       $a =~ s|^.*/||;
    return "$a  cert:[:id]|key:[:id] [ ...]\n" .
           "             [-u ] [-p ] [-m ] [--[no]overwrite]\n" .
           "             [--help]\n";
}
Published Mar 09, 2015
Version 1.0
No CommentsBe the first to comment