Compare commits

..

2 commits

Author SHA1 Message Date
Greg T. Wallace
f102a1838b v1.1.0-b1 2024-07-09 19:05:29 -04:00
Greg T. Wallace
40eca754e0 add ecdsa key support and enable 4,092 RSA
* apcssh: add descriptive error when required file(s) not passed
* create: dont create key+cert file when key isn't supported by NMC2
* config: fix usage messages re: key types
* p15 files: dont generate key+cert when it isn't needed (aka NMC2 doesn't support key)
* pkcs15: pre-calculate envelope when making the p15 struct
* pkcs15: omit key ID 8 & 9 from EC keys
* pkcs15: update key decode logic
* pkcs15: add key type value for easy determination of compatibility
* pkcs15: add ec key support
* pkcs15: separate functions for key and key+cert p15 files
* update README
see: https://github.com/gregtwallace/apc-p15-tool/issues/6
2024-07-09 19:05:14 -04:00
12 changed files with 341 additions and 324 deletions

View file

@ -8,10 +8,89 @@ on:
env:
GITHUB_REF: ${{ github.ref }}
GO_VERSION: '1.22.4'
jobs:
build-all:
runs-on: ubuntu-24.04
build-common:
runs-on: ubuntu-latest
steps:
- name: Checkout Main Repo
uses: actions/checkout@v4
with:
repository: gregtwallace/apc-p15-tool
ref: ${{ env.GITHUB_REF }}
fetch-depth: 0
- name: Save README
uses: actions/upload-artifact@v4
with:
name: README.md
path: ./README.md
- name: Save LICENSE
uses: actions/upload-artifact@v4
with:
name: LICENSE.md
path: ./LICENSE.md
- name: Save CHANGELOG
uses: actions/upload-artifact@v4
with:
name: CHANGELOG.md
path: ./CHANGELOG.md
build-linux-arm64:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v4
with:
repository: gregtwallace/apc-p15-tool
ref: ${{ env.GITHUB_REF }}
fetch-depth: 0
- name: Update apt
run: sudo apt update
- name: Install cross-compiler for linux/arm64
run: sudo apt-get -y install gcc-aarch64-linux-gnu
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '${{ env.GO_VERSION }}'
- name: Build Tool
run: go build -o ./apc-p15-tool -v ./cmd/tool
env:
GOOS: linux
GOARCH: arm64
CC: aarch64-linux-gnu-gcc
CGO_ENABLED: 0
- name: Save Compiled Binary
uses: actions/upload-artifact@v4
with:
name: apc-p15-tool-linux-arm64
path: ./apc-p15-tool
- name: Build Install Only
run: go build -o ./apc-p15-install -v ./cmd/install_only
env:
GOOS: linux
GOARCH: arm64
CC: aarch64-linux-gnu-gcc
CGO_ENABLED: 0
- name: Save Compiled Binary
uses: actions/upload-artifact@v4
with:
name: apc-p15-install-linux-arm64
path: ./apc-p15-install
build-linux-amd64:
runs-on: ubuntu-latest
steps:
- name: Checkout Backend Repo
uses: actions/checkout@v4
@ -23,14 +102,203 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
go-version: '${{ env.GO_VERSION }}'
- name: Build All
run: |
python ./build_release.py
- name: Build Tool
run: go build -o ./apc-p15-tool -v ./cmd/tool
env:
GOOS: linux
GOARCH: amd64
CGO_ENABLED: 0
- name: Save Zip of all targets
- name: Save Compiled Binary
uses: actions/upload-artifact@v4
with:
name: apc-p15-tool-release
path: ./_out/_release
name: apc-p15-tool-linux-amd64
path: ./apc-p15-tool
- name: Build Install Only
run: go build -o ./apc-p15-install -v ./cmd/install_only
env:
GOOS: linux
GOARCH: amd64
CGO_ENABLED: 0
- name: Save Compiled Binary
uses: actions/upload-artifact@v4
with:
name: apc-p15-install-linux-amd64
path: ./apc-p15-install
build-windows-amd64:
runs-on: windows-latest
steps:
- name: Checkout Backend Repo
uses: actions/checkout@v4
with:
repository: gregtwallace/apc-p15-tool
ref: ${{ env.GITHUB_REF }}
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '${{ env.GO_VERSION }}'
- name: Build Tool
run: go build -o ./apc-p15-tool.exe -v ./cmd/tool
env:
GOOS: windows
GOARCH: amd64
CGO_ENABLED: 0
- name: Save Compiled Binary
uses: actions/upload-artifact@v4
with:
name: apc-p15-tool-windows-amd64
path: ./apc-p15-tool.exe
- name: Build Install Only
run: go build -o ./apc-p15-install.exe -v ./cmd/install_only
env:
GOOS: windows
GOARCH: amd64
CGO_ENABLED: 0
- name: Save Compiled Binary
uses: actions/upload-artifact@v4
with:
name: apc-p15-install-windows-amd64
path: ./apc-p15-install.exe
release-file-linux-arm64:
needs: [build-common, build-linux-arm64]
runs-on: ubuntu-latest
steps:
- name: Make release directory
run: mkdir ./release
- name: Download Tool Binary
uses: actions/download-artifact@v4
with:
name: apc-p15-tool-linux-arm64
path: ./release
- name: Download Install Binary
uses: actions/download-artifact@v4
with:
name: apc-p15-install-linux-arm64
path: ./release
- name: Download README
uses: actions/download-artifact@v4
with:
name: README.md
path: ./release
- name: Download LICENSE
uses: actions/download-artifact@v4
with:
name: LICENSE.md
path: ./release
- name: Download CHANGELOG
uses: actions/download-artifact@v4
with:
name: CHANGELOG.md
path: ./release
- name: Save Release
uses: actions/upload-artifact@v4
with:
name: apc-p15-tool_linux_arm64
path: ./release
release-file-linux-amd64:
needs: [build-common, build-linux-amd64]
runs-on: ubuntu-latest
steps:
- name: Make release directory
run: mkdir ./release
- name: Download Tool Binary
uses: actions/download-artifact@v4
with:
name: apc-p15-tool-linux-amd64
path: ./release
- name: Download Install Binary
uses: actions/download-artifact@v4
with:
name: apc-p15-install-linux-amd64
path: ./release
- name: Download README
uses: actions/download-artifact@v4
with:
name: README.md
path: ./release
- name: Download LICENSE
uses: actions/download-artifact@v4
with:
name: LICENSE.md
path: ./release
- name: Download CHANGELOG
uses: actions/download-artifact@v4
with:
name: CHANGELOG.md
path: ./release
- name: Save Release
uses: actions/upload-artifact@v4
with:
name: apc-p15-tool_linux_amd64
path: ./release
release-file-windows-amd64:
needs: [build-common, build-windows-amd64]
runs-on: ubuntu-latest
steps:
- name: Make release directory
run: mkdir ./release
- name: Download Tool Binary
uses: actions/download-artifact@v4
with:
name: apc-p15-tool-windows-amd64
path: ./release
- name: Download Install Binary
uses: actions/download-artifact@v4
with:
name: apc-p15-install-windows-amd64
path: ./release
- name: Download README
uses: actions/download-artifact@v4
with:
name: README.md
path: ./release
- name: Download LICENSE
uses: actions/download-artifact@v4
with:
name: LICENSE.md
path: ./release
- name: Download CHANGELOG
uses: actions/download-artifact@v4
with:
name: CHANGELOG.md
path: ./release
- name: Save Release
uses: actions/upload-artifact@v4
with:
name: apc-p15-tool_windows_amd64
path: ./release

View file

@ -1,61 +1,13 @@
# APC P15 Tool Changelog
## [v1.2.3] - 2025-06-19
## [v1.1.0-b1] - 2024-07-09
Minor updates to the application. Large updates to the build process to
improve building, releasing, and maintainability.
BETA: First effort to add support for EC key support for NMC3.
- Go updated to 1.24.4 and all dependencies updated.
- Added FreeBSD arm64 and amd64 builds.
- Build process overhauled for simplicity. Build is now OS agnostic. PowerShell
script was removed and replaced with a python script.
- Build instructions added to README.
- GitHub build action now only runs in one Ubuntu container and cross-compiles.
- Release windows and macos as zip files and all others as gztar.
- Add file permissions for non-windows and non-macos releases.
## [v1.2.2] - 2025-04-22
All dependencies updated.
Add darwin arm64 and amd64 builds.
## [v1.2.1] - 2025-03-17
Fix time check for UPS when it is set to GMT timezone.
All dependencies updated.
## [v1.2.0] - 2025-01-27
Add a new feature to `install` that checks the time of the UPS to confirm
it is accurate. A log message is added that advises either way. Even if
the check fails, the install still proceeds with attempting to install
the new certificate.
Dependencies were also all updated.
## [v1.1.0] - 2024-09-17
> [!IMPORTANT]
> The flag `apchost` on the `install` command has been renamed to
> `hostname`. This flag should contain the hostname only. If a non-
> default SSH port is needed, specify it in the `sshport` flag.
This version brings support for for RSA 4,092 bit and EC keys. These
keys are only compatible with NMC3 running newer firmwares. To know
if your firmware is new enough, SSH into your UPS and type `ssh` and enter.
If the UPS responds `Command Not Found` the firmware is too old or
otherwise incompatible.
This version also adds a post `install` check that connects to the web
ui and verifies the certificate served is the expected one. You can
specify a non standard ssl port with the `sslport` flag or skip the check
entirely with the `skipverify` flag.
This version also enables RSA 4,092 bit length. Code was updated
so the NMC2 key+cert file is not generated when NMC2 is not
supported. Log messages were also updated to signal user when
a compatibility issue is present.
## [v1.0.0] - 2024-07-01

View file

@ -63,19 +63,13 @@ NMC2 device in a home lab and have no way to guarantee success in all cases.
NMC2:
- RSA 1,024, 2,048, 3,072* bit lengths.
NMC3*:
NMC3:
- RSA 1,024, 2,048, 3,072, and 4,092 bit lengths.
- ECDSA curves P-256, P-384, and P-521.
\* 3,072 bit length is not officially supported by my NMC2, but appears to work
* 3,072 bit length is not officially supported by my NMC2, but appears to work
fine.
\* The additional key types supported by NMC3 require newer firmware on the
device. I am unsure what the version cutoff is, but you can check support
by connecting to the UPS via SSH and typing `ssl`. If `Command Not Found`
is returned, the firmware is too old and only the key types listed under
NMC2 will work.
1,024 bit RSA is no longer considered completely secure; avoid keys of
this size if possible. Most (all?) public ACME services won't accept keys
of this size anyway.
@ -146,7 +140,7 @@ disk. It instead installs the files directly on the NMC. Logic
automatically deduces if the device is an NMC2 or NMC3 and performs
the appropriate installation steps.
e.g. `./apc-p15-tool install --keyfile ./apckey.pem --certfile ./apccert.pem --hostname myapc.example.com --username apc --password someSecret --fingerprint 123abc`
e.g. `./apc-p15-tool install --keyfile ./apckey.pem --certfile ./apccert.pem --apchost myapc.example.com:22 --username apc --password someSecret --fingerprint 123abc`
## Note About Install Automation
@ -171,20 +165,6 @@ separate script.
![Cert Warden with APC P15 Tool](https://raw.githubusercontent.com/gregtwallace/apc-p15-tool/main/img/apc-p15-tool.png)
## Building
Python3, Go, and git all must be installed to run the build script.
Once the dependencies are installed, clone this repo and run
`python build_release.py`. If you only want to build for certain OS or
ARCH targets, edit the `targets` array in the `build_release.py` file
before running it.
## Links
@Owl-Tec's write up using this tool with ACDS:
https://owltec.ca/Windows+Server/Deploying+An+Internal+HTTPS+Certificate+for+a+UPS+APC+with+ADCS+(Active+Directory+Certificate+Services)+with+APC+P15+Tool
## Thanks
Special thanks to the following people and resources which helped me

36
build.ps1 Normal file
View file

@ -0,0 +1,36 @@
# Parent dir is root
$scriptDir = Get-Location
$outDir = Join-Path -Path $scriptDir -ChildPath "/_out"
# Windows x64
$env:GOARCH = "amd64"
$env:GOOS = "windows"
$env:CGO_ENABLED = 0
go build -o $outDir/apc-p15-tool-amd64.exe ./cmd/tool
$env:GOARCH = "amd64"
$env:GOOS = "windows"
$env:CGO_ENABLED = 0
go build -o $outDir/apc-p15-install-amd64.exe ./cmd/install_only
# Linux x64
$env:GOARCH = "amd64"
$env:GOOS = "linux"
$env:CGO_ENABLED = 0
go build -o $outDir/apc-p15-tool-amd64 ./cmd/tool
$env:GOARCH = "amd64"
$env:GOOS = "linux"
$env:CGO_ENABLED = 0
go build -o $outDir/apc-p15-install-amd64 ./cmd/install_only
# Linux arm64
$env:GOARCH = "arm64"
$env:GOOS = "linux"
$env:CGO_ENABLED = 0
go build -o $outDir/apc-p15-tool-arm64 ./cmd/tool
$env:GOARCH = "arm64"
$env:GOOS = "linux"
$env:CGO_ENABLED = 0
go build -o $outDir/apc-p15-install-arm64 ./cmd/install_only

View file

@ -1,89 +0,0 @@
#!/usr/bin/env python3
import os.path
import shutil
import subprocess
import tarfile
# Configuration
# output path (relative to this script)
outRelativeDir = "_out"
# target strings must be in the format:
# `GOOS_GOARCH`
# see: https://github.com/golang/go/blob/master/src/internal/syslist/syslist.go
# or unofficially: https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63
targets = [
"windows_amd64",
"linux_amd64",
"linux_arm64",
"darwin_amd64",
"darwin_arm64",
"freebsd_amd64",
"freebsd_arm64",
]
###
# Script
# relative dir is root
scriptDir = dirname = os.path.dirname(__file__)
outBaseDir = os.path.join(scriptDir, outRelativeDir)
releaseDir = os.path.join(outBaseDir, "_release")
# recreate paths
if os.path.exists(outBaseDir):
shutil.rmtree(outBaseDir)
os.makedirs(outBaseDir)
os.makedirs(releaseDir)
# get version number / tag
gitTag = subprocess.check_output(["git", "describe", "--tags", "--abbrev=0"]).decode('utf-8').strip()
# loop through and build all targets
for target in targets:
# environment vars
split = target.split("_")
GOOS = split[0]
GOARCH = split[1]
os.environ["GOOS"] = GOOS
os.environ["GOARCH"] = GOARCH
os.environ["CGO_ENABLED"] = "0"
# send build product to GOOS_GOARCH subfolders
targetOutDir = os.path.join(outBaseDir, target)
if not os.path.exists(targetOutDir):
os.makedirs(targetOutDir)
# special case for windows to add file extensions
extension = ""
if GOOS.lower() == "windows":
extension = ".exe"
# build binary and install only binary
subprocess.run(["go", "build", "-o", f"{targetOutDir}/apc-p15-tool{extension}", "./cmd/tool"])
subprocess.run(["go", "build", "-o", f"{targetOutDir}/apc-p15-install{extension}", "./cmd/install_only"])
# copy other important files for release
shutil.copy("README.md", targetOutDir)
shutil.copy("CHANGELOG.md", targetOutDir)
shutil.copy("LICENSE.md", targetOutDir)
# compress release file
# special case for windows & mac to use zip format
if GOOS.lower() == "windows" or GOOS.lower() == "darwin":
shutil.make_archive(f"{releaseDir}/apc-p15-tool-{gitTag}_{target}", "zip", targetOutDir)
else:
# for others, use gztar and set permissions on the files
# filter for setting permissions
def set_permissions(tarinfo):
if tarinfo.name == "apc-p15-tool" or tarinfo.name == "apc-p15-install":
tarinfo.mode = 0o0755
else:
tarinfo.mode = 0o0644
return tarinfo
# make tar
with tarfile.open(f"{releaseDir}/apc-p15-tool-{gitTag}_{target}.tar.gz", "w:gz") as tar:
for file in os.listdir(targetOutDir):
tar.add(os.path.join(targetOutDir, file), arcname=file, recursive=False, filter=set_permissions)

8
go.mod
View file

@ -1,14 +1,14 @@
module apc-p15-tool
go 1.24.4
go 1.22.4
require (
github.com/peterbourgon/ff/v4 v4.0.0-alpha.4
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1
golang.org/x/crypto v0.39.0
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3
golang.org/x/crypto v0.18.0
)
require golang.org/x/sys v0.33.0 // indirect
require golang.org/x/sys v0.16.0 // indirect
replace apc-p15-tool/cmd/install_only => /cmd/install_only

16
go.sum
View file

@ -2,13 +2,13 @@ github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/peterbourgon/ff/v4 v4.0.0-alpha.4 h1:aiqS8aBlF9PsAKeMddMSfbwp3smONCn3UO8QfUg0Z7Y=
github.com/peterbourgon/ff/v4 v4.0.0-alpha.4/go.mod h1:H/13DK46DKXy7EaIxPhk2Y0EC8aubKm35nBjBe8AAGc=
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I=
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs=
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

Binary file not shown.

Before

(image error) Size: 93 KiB

After

(image error) Size: 102 KiB

Before After
Before After

View file

@ -1,62 +0,0 @@
package apcssh
import (
"fmt"
"regexp"
"strings"
"time"
)
// GetTime sends the APC `system` command and then attempts to parse the
// response to determine the UPS current date/time.
func (cli *Client) GetTime() (time.Time, error) {
result, err := cli.cmd("date")
if err != nil {
return time.Time{}, fmt.Errorf("apcssh: failed to get time (%s)", err)
} else if !strings.EqualFold(result.code, "e000") {
return time.Time{}, fmt.Errorf("apcssh: failed to get time (%s: %s)", result.code, result.codeText)
}
// capture each portion of the date information
regex := regexp.MustCompile(`Date:\s*(\S*)\s*[\r\n]Time:\s*(\S*)\s*[\r\n]Format:\s*(\S*)\s*[\r\n]Time Zone:\s*(\S*)\s*[\r\n]?`)
datePieces := regex.FindStringSubmatch(result.resultText)
if len(datePieces) != 5 {
return time.Time{}, fmt.Errorf("apcssh: failed to get time (length of datetime value pieces was %d (expected: 5))", len(datePieces))
}
dateVal := datePieces[1]
timeVal := datePieces[2]
formatUPSVal := datePieces[3]
timeZoneVal := datePieces[4]
// GMT time requires + prefix
// APC UPS fails to use the required +, so add it
if timeZoneVal == "00:00" {
timeZoneVal = "+" + timeZoneVal
}
// known APC UPS format strings
dateFormatVal := ""
switch formatUPSVal {
case "mm/dd/yyyy":
dateFormatVal = "01/02/2006"
case "dd.mm.yyyy":
dateFormatVal = "02.01.2006"
case "mmm-dd-yy":
dateFormatVal = "Jan-02-06"
case "dd-mmm-yy":
dateFormatVal = "02-Jan-06"
case "yyyy-mm-dd":
dateFormatVal = "2006-01-02"
default:
return time.Time{}, fmt.Errorf("apcssh: failed to get time (ups returned unknown format string (%s)", formatUPSVal)
}
// convert to time.Time
t, err := time.Parse(dateFormatVal+" 15:04:05 -07:00", dateVal+" "+timeVal+" "+timeZoneVal)
if err != nil {
return time.Time{}, fmt.Errorf("apcssh: failed to get time (time parse failed: %s)", err)
}
return t, nil
}

View file

@ -12,7 +12,7 @@ import (
)
const (
appVersion = "1.2.3"
appVersion = "1.1.0-b1"
)
// struct for receivers to use common app pieces

View file

@ -2,18 +2,11 @@ package app
import (
"apc-p15-tool/pkg/apcssh"
"bytes"
"context"
"crypto/tls"
"encoding/pem"
"errors"
"fmt"
"strconv"
"time"
)
const timeLoggingFormat = time.RFC1123Z
// cmdInstall is the app's command to create apc p15 file content from key and cert
// pem files and upload the p15 to the specified APC UPS
func (app *app) cmdInstall(cmdCtx context.Context, args []string) error {
@ -43,9 +36,7 @@ func (app *app) cmdInstall(cmdCtx context.Context, args []string) error {
}
// host to install on must be specified
if app.config.install.hostname == nil || *app.config.install.hostname == "" ||
app.config.install.sshport == nil || *app.config.install.sshport == 0 {
if app.config.install.hostAndPort == nil || *app.config.install.hostAndPort == "" {
return errors.New("install: failed, apc host not specified")
}
@ -64,7 +55,7 @@ func (app *app) cmdInstall(cmdCtx context.Context, args []string) error {
// make APC SSH client
cfg := &apcssh.Config{
Hostname: *app.config.install.hostname + ":" + strconv.Itoa(*app.config.install.sshport),
Hostname: *app.config.install.hostAndPort,
Username: *app.config.install.username,
Password: *app.config.install.password,
ServerFingerprint: *app.config.install.fingerprint,
@ -77,16 +68,6 @@ func (app *app) cmdInstall(cmdCtx context.Context, args []string) error {
}
app.stdLogger.Println("install: connected to ups ssh, installing ssl key and cert...")
// check time - don't fail it time is no good, just do logging here
upsT, err := client.GetTime()
if err != nil {
app.errLogger.Printf("warn: install: failed to fetch UPS time (%s), you should manually verify the time is correct on the UPS", err)
} else if upsT.After(time.Now().Add(1*time.Hour)) || upsT.Before(time.Now().Add(-1*time.Hour)) {
app.errLogger.Printf("warn: install: UPS clock skew detected (this system's time is %s vs. UPS time %s", time.Now().Format(timeLoggingFormat), upsT.Format(timeLoggingFormat))
} else {
app.stdLogger.Printf("install: UPS clock appears correct (%s)", upsT.Format(timeLoggingFormat))
}
// install SSL Cert
err = client.InstallSSLCert(keyP15, certPem, keyCertP15)
if err != nil {
@ -94,7 +75,7 @@ func (app *app) cmdInstall(cmdCtx context.Context, args []string) error {
}
// installed
app.stdLogger.Printf("install: apc p15 file installed on %s", *app.config.install.hostname)
app.stdLogger.Printf("install: apc p15 file installed on %s", *app.config.install.hostAndPort)
// restart UPS webUI
if app.config.install.restartWebUI != nil && *app.config.install.restartWebUI {
@ -108,48 +89,5 @@ func (app *app) cmdInstall(cmdCtx context.Context, args []string) error {
app.stdLogger.Println("install: sent webui restart command")
}
// check the new certificate is installed
if app.config.install.skipVerify != nil && !*app.config.install.skipVerify &&
app.config.install.webUISSLPort != nil && *app.config.install.webUISSLPort != 0 {
app.stdLogger.Println("install: attempting to verify certificate install...")
// sleep for UPS to finish anything it might be doing
time.Sleep(5 * time.Second)
// if UPS web UI was restarted, sleep longer
if app.config.install.restartWebUI != nil && *app.config.install.restartWebUI {
app.stdLogger.Println("install: waiting for ups webui restart...")
time.Sleep(25 * time.Second)
}
// connect to the web UI to get the current certificate
conf := &tls.Config{
InsecureSkipVerify: true,
}
conn, err := tls.Dial("tcp", *app.config.install.hostname+":"+strconv.Itoa(*app.config.install.webUISSLPort), conf)
if err != nil {
return fmt.Errorf("install: failed to dial webui for verification (%s)", err)
}
defer conn.Close()
// get top cert
leafCert := conn.ConnectionState().PeerCertificates[0]
if leafCert == nil {
return fmt.Errorf("install: failed to get web ui leaf cert for verification (%s)", err)
}
// convert pem to DER for comparison
pemBlock, _ := pem.Decode(certPem)
// verify cert is the correct one
certVerified := bytes.Equal(leafCert.Raw, pemBlock.Bytes)
if !certVerified {
return errors.New("install: web ui leaf cert does not match new cert")
}
app.stdLogger.Println("install: ups web ui cert verified")
}
return nil
}

View file

@ -33,14 +33,11 @@ type config struct {
}
install struct {
keyCertPemCfg
hostname *string
sshport *int
hostAndPort *string
fingerprint *string
username *string
password *string
restartWebUI *bool
webUISSLPort *int
skipVerify *bool
insecureCipher *bool
}
}
@ -95,19 +92,16 @@ func (app *app) getConfig(args []string) error {
cfg.install.certPemFilePath = installFlags.StringLong("certfile", "", "path and filename of the certificate in pem format")
cfg.install.keyPem = installFlags.StringLong("keypem", "", "string of the key in pem format")
cfg.install.certPem = installFlags.StringLong("certpem", "", "string of the certificate in pem format")
cfg.install.hostname = installFlags.StringLong("hostname", "", "hostname of the apc ups to install the certificate on")
cfg.install.sshport = installFlags.IntLong("sshport", 22, "apc ups ssh port number")
cfg.install.hostAndPort = installFlags.StringLong("apchost", "", "hostname:port of the apc ups to install the certificate on")
cfg.install.fingerprint = installFlags.StringLong("fingerprint", "", "the SHA256 fingerprint value of the ups' ssh server")
cfg.install.username = installFlags.StringLong("username", "", "username to login to the apc ups")
cfg.install.password = installFlags.StringLong("password", "", "password to login to the apc ups")
cfg.install.restartWebUI = installFlags.BoolLong("restartwebui", "some devices may need a webui restart to begin using the new cert, enabling this option sends the restart command after the p15 is installed")
cfg.install.webUISSLPort = installFlags.IntLong("sslport", 443, "apc ups ssl webui port number")
cfg.install.skipVerify = installFlags.BoolLong("skipverify", "the tool will try to connect to the UPS web UI to verify install success; this flag disables that check")
cfg.install.insecureCipher = installFlags.BoolLong("insecurecipher", "allows the use of insecure ssh ciphers (NOT recommended)")
installCmd := &ff.Command{
Name: "install",
Usage: "apc-p15-tool install --keyfile key.pem --certfile cert.pem --hostname example.com --fingerprint 123abc --username apc --password test",
Usage: "apc-p15-tool install --keyfile key.pem --certfile cert.pem --apchost example.com:22 --fingerprint 123abc --username apc --password test",
ShortHelp: "install the specified key and cert pem files on an apc ups (they will be converted to a comaptible p15 file)",
Flags: installFlags,
Exec: app.cmdInstall,