#!/usr/bin/env perl -w
########################################################################
# refresh-ros-certs - Automatically mint fresh TLS certificates for a
#   set of RouterOS devices defined in %kRouters below.
#
#   Calls on the services of a RouterOS device with the CNAME 'ca',
#   which may or may not be in the defined router set.
#
# Copyright © 2025 by Warren Young.  Licenced under CC-BY-SA 4.0.
########################################################################

use strict;
use v5.26;

use Readonly;


#### CONFIGURABLES #####################################################

Readonly::Scalar our $kValidDays    => 35;
Readonly::Scalar our $kDomain       => 'example.com';
Readonly::Scalar our $kCountry      => 'ZZ';
Readonly::Scalar our $kState        => 'AA';
Readonly::Scalar our $kLocality     => 'Anytown';
Readonly::Scalar our $kOrganization => 'Networks R Us';
Readonly::Scalar our $kSignCert     => $kOrganization . 'CA';
Readonly::Scalar our $kUnit         => 'Network Core Ops';
Readonly::Scalar our $kBackupDir    => '~/network-config';
Readonly::Hash   our %kRouters = (
    MyRouter => {
        cnames => [
            'gw',
            'rb5009',
        ],
        ipaddr => '192.168.88.1',
    },
    MySwitch => {
        cnames => [
            'core-switch',
            'crs310',
        ],
        ipaddr => '192.168.88.10',
    },
);


#### WARRANTY VOID IF SEAL IS BROKEN ###################################

my ($sec, $min, $hour, $mday, $month, $year) = localtime();
my $ts = sprintf("%04d-%02d", $year + 1900, $month + 1);

Readonly::Array  our @kPWAlphabet   => ('A'..'Z','a'..'z',0..9);
Readonly::Scalar our $kPWLength     => 16;
Readonly::Scalar our $kPassphrase   =>
    join '', map { $kPWAlphabet[rand @kPWAlphabet] } 1..$kPWLength;
print "Generating certificates under passphrase '$kPassphrase'…\n";

for my $r (keys %kRouters) {
    my $ipaddr =   $kRouters{$r}{ipaddr};
    my @cnames = @{$kRouters{$r}{cnames}};
    unshift @cnames, $r;
    my $san = join(',', map { "DNS:$_.$kDomain" } @cnames) . ',';
    $san .= join(',', map { "DNS:$_" } @cnames) . ',';
    $san .= 'IP:' . $ipaddr;

    my $name = $r . '-TLS-' . $ts;
    my %attrs = (   
        'name'             => $name,
        'subject-alt-name' => $san,
        'country'          => $kCountry,
        'state'            => $kState,
        'locality'         => $kLocality,
        'organization'     => $kOrganization,
        'unit'             => $kUnit,
        'trusted'          => 'yes',
        'key-usage'        => 'tls-server',
        'days-valid'       => $kValidDays,
    );
    print "Minting certificate for $r…\n";
    my $cmd = "/certificate/add " .
        join(' ', map {
            $_ . '=' . '"' . $attrs{$_} . '"'
        } keys %attrs);
    system("ssh ca '$cmd'") == 0 or die "Minting failed, CMD: $cmd!\n";
    print "Signing minted certificate…\n";
    $cmd = "/certificate/sign " . $name . ' ca="' . $kSignCert . '"';
    system("ssh ca '$cmd'") == 0 or die "Signing failed, CMD: $cmd!\n";

    %attrs = (
        'export-passphrase' => $kPassphrase,
        'file-name'         => $name,
    );  
    print "Exporting signed certificate…\n";
    $cmd = "/certificate/export-certificate " . $name . ' ' .
        join(' ', map {
            $_ . '=' . '"' . $attrs{$_} . '"'
        } keys %attrs);
    system("ssh ca '$cmd'") == 0 or die "Export failed, CMD: $cmd!\n";

    my $bdir = $kBackupDir . '/' . lc($r);
    print "Copying signed cert halves to $bdir/tls.*…\n";
    system("scp ca:$name.crt $bdir/tls.crt") == 0
        or die "Failed to copy public certificate!\n";
    system("scp ca:$name.key $bdir/tls.key") == 0
        or die "Failed to copy private cert key!\n";

    print "Copying refreshed cert halves to $r…\n";
    system("scp $bdir/tls.* $r:") == 0 or die "Failed to copy cert halves to $r!\n";

    %attrs = (
        'file-name'  => 'tls.crt',
        'name'       => $name,
        'passphrase' => $kPassphrase,
        'trusted'    => 'yes',
    );  
    print "Importing refreshed certificate on $r…\n";
    $cmd = "/certificate/import " .
        join(' ', map {
            $_ . '=' . '"' . $attrs{$_} . '"'
        } keys %attrs);
    system("ssh $r '$cmd'") == 0 or die "Public cert import failed, CMD: $cmd!\n";
    $cmd =~ s{\.crt}{.key}x;
    system("ssh $r '$cmd'") == 0 or die "Private key import failed, CMD: $cmd!\n";

    print "Switching services on $r to new certs…\n";
    system("ssh $r '/ip/service/set api-ssl certificate=\"" . $name . '"\'"')
        or die "Failed to switch REST API to new TLS cert!\n";
    system("ssh $r '/ip/service/set www-ssl certificate=\"" . $name . '"\'"')
        or die "Failed to switch WebFig to new TLS cert!\n";
}
