diff --git a/.github/workflows/build_releases.yml b/.github/workflows/build_releases.yml index 5b41dbe..c054287 100644 --- a/.github/workflows/build_releases.yml +++ b/.github/workflows/build_releases.yml @@ -8,83 +8,9 @@ on: env: GITHUB_REF: ${{ github.ref }} - GO_VERSION: '1.24.2' jobs: - build-common: - runs-on: ubuntu-24.04 - - 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-24.04-arm - steps: - - name: Checkout 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 -v ./cmd/tool - env: - GOOS: linux - GOARCH: arm64 - 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: + build-all: runs-on: ubuntu-24.04 steps: - name: Checkout Backend Repo @@ -97,375 +23,14 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '${{ env.GO_VERSION }}' + go-version-file: 'go.mod' - - name: Build Tool - run: go build -o ./apc-p15-tool -v ./cmd/tool - env: - GOOS: linux - GOARCH: amd64 - CGO_ENABLED: 0 + - name: Build All + run: | + python ./build_release.py - - name: Save Compiled Binary + - name: Save Zip of all targets uses: actions/upload-artifact@v4 with: - 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 - - build-darwin-arm64: - runs-on: macos-15 - 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 -v ./cmd/tool - env: - GOOS: darwin - GOARCH: arm64 - CGO_ENABLED: 0 - - - name: Save Compiled Binary - uses: actions/upload-artifact@v4 - with: - name: apc-p15-tool-darwin-arm64 - path: ./apc-p15-tool - - - name: Build Install Only - run: go build -o ./apc-p15-install -v ./cmd/install_only - env: - GOOS: darwin - GOARCH: arm64 - CGO_ENABLED: 0 - - - name: Save Compiled Binary - uses: actions/upload-artifact@v4 - with: - name: apc-p15-install-darwin-arm64 - path: ./apc-p15-install - - build-darwin-amd64: - runs-on: macos-13 - 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 -v ./cmd/tool - env: - GOOS: darwin - GOARCH: amd64 - CGO_ENABLED: 0 - - - name: Save Compiled Binary - uses: actions/upload-artifact@v4 - with: - name: apc-p15-tool-darwin-amd64 - path: ./apc-p15-tool - - - name: Build Install Only - run: go build -o ./apc-p15-install -v ./cmd/install_only - env: - GOOS: darwin - GOARCH: amd64 - CGO_ENABLED: 0 - - - name: Save Compiled Binary - uses: actions/upload-artifact@v4 - with: - name: apc-p15-install-darwin-amd64 - path: ./apc-p15-install - -### - - release-file-linux-arm64: - needs: [build-common, build-linux-arm64] - runs-on: ubuntu-24.04 - - 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-24.04 - - 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-24.04 - - 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 - - release-file-darwin-arm64: - needs: [build-common, build-darwin-arm64] - runs-on: ubuntu-24.04 - - steps: - - name: Make release directory - run: mkdir ./release - - - name: Download Tool Binary - uses: actions/download-artifact@v4 - with: - name: apc-p15-tool-darwin-arm64 - path: ./release - - - name: Download Install Binary - uses: actions/download-artifact@v4 - with: - name: apc-p15-install-darwin-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_darwin_arm64 - path: ./release - - release-file-darwin-amd64: - needs: [build-common, build-darwin-amd64] - runs-on: ubuntu-24.04 - - steps: - - name: Make release directory - run: mkdir ./release - - - name: Download Tool Binary - uses: actions/download-artifact@v4 - with: - name: apc-p15-tool-darwin-amd64 - path: ./release - - - name: Download Install Binary - uses: actions/download-artifact@v4 - with: - name: apc-p15-install-darwin-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_darwin_amd64 - path: ./release + name: apc-p15-tool-release + path: ./_out/_release diff --git a/CHANGELOG.md b/CHANGELOG.md index cc534a6..c8a6ff5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # APC P15 Tool Changelog +## [v1.3.0] - 2025-06-23 + +This release attempts to detect and warn of possible incompatibilies with a +spcecified certificate. NMCs do not warn or error when a bad file is installed, +instead they silently fail and generally just generate a new self-signed +certificate. This release checks some properties of the specified certificate +and produces warning messages that can be referenced if the cert installation +appears to work but ultimately doesn't prododuce the expected result. + +- Add warnings based on key type, signature algorithm, validity dates, and + extensions. +- Minor lint. + + +## [v1.2.3] - 2025-06-19 + +Minor updates to the application. Large updates to the build process to +improve building, releasing, and maintainability. + +- 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. diff --git a/README.md b/README.md index 1d494ed..9286344 100644 --- a/README.md +++ b/README.md @@ -53,13 +53,14 @@ This project aims to solve all of these problems by accepting the most common key and cert file format (PEM) and by being 100% open source and licensed under the GPL-3.0 license. -## Compatibility Notice - -Both NMC2 and NMC3 devices should be fully supported. However, I have one -NMC2 device in a home lab and have no way to guarantee success in all cases. - ### Key Types and Sizes +Ensure you select an appropriate key! + +NMC2 is extremely picky about the key type and size it supports. NMC3 is a bit +more flexible. Beware, some ACME clients will generate an ECDSA key by default +which is NOT supported by NMC2. + NMC2: - RSA 1,024, 2,048, 3,072* bit lengths. @@ -80,22 +81,60 @@ NMC3*: this size if possible. Most (all?) public ACME services won't accept keys of this size anyway. -### General Troubleshooting +### Compatibility Notice + +Both NMC2 and NMC3 devices should be fully supported. However, I have one +NMC2 device in a home lab and have no way to guarantee success in all cases. My setup (and therefore the testing setup) is: - APC Smart-UPS 1500VA RM 2U SUA1500RM2U (Firmware Revision 667.18.D) - AP9631 NMC2 Hardware Revision 05 running AOS v7.1.2 and Boot Monitor v1.0.9. -If you have trouble, your first step should be to update your NMC's firmware. -Many issues with this tool will be resolved simply by updating to the newest -firmware. +Generally, if there is a compatibility issue, there is a good chance you will +not see an error. Rather, the NMC will silently fail and you'll only know +something went wrong because the NMC's certificate didn't update, or it regenerated +a self-signed certificate that you'll see upon your next connection attempt. +I've tried to add some `WARNING` messages to the tool to indicate what might +be going wrong, but the list is definitely not exhaustive. -If you have a problem after that, please post the log in an issue and I can -try to fix it but it may be difficult without your particular hardware to -test with. +### Troubleshooting -In particular, if you are experiencing `ssh: handshake failed:` first try +Suggested troubleshooting steps: +- Review the `Key Types and Sizes` and `Compatibility Notice` sections of this + README. +- Update your NMC's firmware to the latest version. +- Read this tool's output, look specifically for any `WARNING` messages and + adjust your certificate accordingly. +- Test using an RSA 2048 bit key to obtain a certificate from Let's Encrypt. + Their certificates are known to work with NMC. +- Use the official NMC Security Wizard to verify you can create a working + certificate and load it into your NMC. If the official tool does not work + switching to this tool won't help. + +If you have tried all of these steps and are still experiencing a problem, +you may open an Issue on GitHub. + +Include: +- The full command you are running that is causing the problem. +- The full log of this tool's output when you run the command. Append the + `--debug` flag to your command to get the debug output. + +Keep in mind, I am one person with one specific hardware setup. I may not +be able to help you. + +#### NMC3 Install `ssh: parse error in message type 53` Error + +Configuring a `System Message` on an NMC3 breaks the install function. I do +not have an NMC3 and after doing some code review it is highly unlikely I'll +be able to fix this. Don't use a `System Message` if the install feature is +important to you. + +see: https://github.com/gregtwallace/apc-p15-tool/issues/14 + +#### Install `ssh: handshake failed` Error + +If you are experiencing `ssh: handshake failed:` first try using the `--insecurecipher` flag. If this works, you should upgrade your NMC to a newer firmware which includes secure ciphers. You should NOT automate your environment using this flag as SSH over these ciphers is broken and @@ -171,6 +210,20 @@ separate script.  +## 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 diff --git a/build.ps1 b/build.ps1 deleted file mode 100644 index 0c8774b..0000000 --- a/build.ps1 +++ /dev/null @@ -1,58 +0,0 @@ -# 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 - -# Darwin (MacOS) amd64 -$env:GOARCH = "amd64" -$env:GOOS = "darwin" -$env:CGO_ENABLED = 0 -go build -o $outDir/apc-p15-tool-darwin-amd64 ./cmd/tool - -$env:GOARCH = "amd64" -$env:GOOS = "darwin" -$env:CGO_ENABLED = 0 -go build -o $outDir/apc-p15-install-darwin-amd64 ./cmd/install_only - -# Darwin (MacOS) arm64 -$env:GOARCH = "arm64" -$env:GOOS = "darwin" -$env:CGO_ENABLED = 0 -go build -o $outDir/apc-p15-tool-darwin-arm64 ./cmd/tool - -$env:GOARCH = "arm64" -$env:GOOS = "darwin" -$env:CGO_ENABLED = 0 -go build -o $outDir/apc-p15-install-darwin-arm64 ./cmd/install_only diff --git a/build_release.py b/build_release.py new file mode 100644 index 0000000..d3892e7 --- /dev/null +++ b/build_release.py @@ -0,0 +1,89 @@ +#!/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) diff --git a/go.mod b/go.mod index 76eb3ca..4848582 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,14 @@ module apc-p15-tool -go 1.24.2 +go 1.24.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.37.0 + golang.org/x/crypto v0.39.0 ) -require golang.org/x/sys v0.32.0 // indirect +require golang.org/x/sys v0.33.0 // indirect replace apc-p15-tool/cmd/install_only => /cmd/install_only diff --git a/go.sum b/go.sum index 0b94b0b..cd5913b 100644 --- a/go.sum +++ b/go.sum @@ -4,11 +4,11 @@ github.com/peterbourgon/ff/v4 v4.0.0-alpha.4 h1:aiqS8aBlF9PsAKeMddMSfbwp3smONCn3 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.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= -golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +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= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/pkg/app/app.go b/pkg/app/app.go index 052a00e..151236e 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -12,7 +12,7 @@ import ( ) const ( - appVersion = "1.2.2" + appVersion = "1.3.0" ) // struct for receivers to use common app pieces diff --git a/pkg/app/cmd_install.go b/pkg/app/cmd_install.go index eacda53..d94c00c 100644 --- a/pkg/app/cmd_install.go +++ b/pkg/app/cmd_install.go @@ -145,7 +145,7 @@ func (app *app) cmdInstall(cmdCtx context.Context, args []string) error { // 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") + return errors.New("install: web ui leaf cert does not match new cert (your cert may not be compatible with NMC; check for WARNINGs in this tool's output)") } app.stdLogger.Println("install: ups web ui cert verified") diff --git a/pkg/app/pem_to_p15.go b/pkg/app/pem_to_p15.go index d48d4a8..f6a6094 100644 --- a/pkg/app/pem_to_p15.go +++ b/pkg/app/pem_to_p15.go @@ -2,8 +2,11 @@ package app import ( "apc-p15-tool/pkg/pkcs15" + "crypto/x509" + "encoding/asn1" "fmt" "slices" + "time" ) // list of keys supported by the NMC2 @@ -13,6 +16,32 @@ var nmc2SupportedKeyTypes = []pkcs15.KeyType{ pkcs15.KeyTypeRSA3072, // officially not supported but works } +// known good signing algorithms +var knownSupportedNMC2SigningAlgs = []x509.SignatureAlgorithm{ + x509.SHA256WithRSA, +} + +var knownSupportedNMC3SigningAlgs = append(knownSupportedNMC2SigningAlgs, []x509.SignatureAlgorithm{ + x509.ECDSAWithSHA384, +}...) + +// known supported cert extensions +var knownSupportedCriticalOIDs = []asn1.ObjectIdentifier{ + {2, 5, 29, 15}, // keyUsage + {2, 5, 29, 19}, // basicConstraints + {2, 5, 29, 17}, // subjectAltName +} + +var knownSupportedOIDs = append(knownSupportedCriticalOIDs, []asn1.ObjectIdentifier{ + {2, 5, 29, 37}, // extKeyUsage + {2, 5, 29, 14}, // subjectKeyIdentifier + {2, 5, 29, 35}, // authorityKeyIdentifier + {1, 3, 6, 1, 5, 5, 7, 1, 1}, // authorityInfoAccess + {2, 5, 29, 32}, // certificatePolicies + {1, 3, 6, 1, 4, 1, 11129, 2, 4, 2}, // googleSignedCertificateTimestamp + {2, 5, 29, 31}, // cRLDistributionPoints +}...) + // pemToAPCP15 reads the specified pem files and returns the apc p15 file(s). If the // key type of the key is not supported by NMC2, the combined key+cert file is not // generated and nil is returned instead for that file. If the key IS supported by @@ -37,7 +66,10 @@ func (app *app) pemToAPCP15(keyPem, certPem []byte, parentCmdName string) (keyFi app.stdLogger.Printf("%s: successfully generated p15 key file content", parentCmdName) // check key type for compat with NMC2 + nmc2KeyType := false if slices.Contains(nmc2SupportedKeyTypes, p15.KeyType()) { + nmc2KeyType = true + app.stdLogger.Printf("%s: key type is supported by NMC2, generating p15 key+cert file content...", parentCmdName) // make file bytes @@ -54,11 +86,84 @@ func (app *app) pemToAPCP15(keyPem, certPem []byte, parentCmdName string) (keyFi // combine header with file apcKeyCertFile = append(apcHeader, keyCertFile...) - } else { - // NMC2 unsupported - app.stdLogger.Printf("%s: key type is not supported by NMC2, skipping p15 key+cert file content", parentCmdName) } + // check various parts of cert and log compatibility warnings + warned := false + + // key not supported for NMC2 + if !nmc2KeyType { + app.stdLogger.Printf("WARNING: %s: key type is %s and is not supported by NMC2.", parentCmdName, p15.KeyType().String()) + warned = true + } + + // signature algorithm (see: https://github.com/gregtwallace/apc-p15-tool/issues/18) + if !nmc2KeyType { + // definitely not for NMC2 + if !slices.Contains(knownSupportedNMC3SigningAlgs, p15.Cert.SignatureAlgorithm) { + app.stdLogger.Printf("WARNING: %s: Certificate signing algorithm is %s and it is not known if NMC3 supports this algorithm.", parentCmdName, p15.Cert.SignatureAlgorithm.String()) + warned = true + } + } else { + // could be for either NMC2 or NMC3 + if !slices.Contains(knownSupportedNMC2SigningAlgs, p15.Cert.SignatureAlgorithm) { + if !slices.Contains(knownSupportedNMC3SigningAlgs, p15.Cert.SignatureAlgorithm) { + // not in NMC2 or NMC3 list + app.stdLogger.Printf("WARNING: %s: Certificate signing algorithm is %s and is not supported by NMC2. It is also not known if NMC3 supports this algorithm.", parentCmdName, p15.Cert.SignatureAlgorithm.String()) + } else { + // not in NMC2 list, but is in NMC3 list + app.stdLogger.Printf("WARNING: %s: Certificate signing algorithm is %s and it does not support NMC2.", parentCmdName, p15.Cert.SignatureAlgorithm.String()) + } + warned = true + } + } + + // check validity dates + if time.Now().Before(p15.Cert.NotBefore) { + app.stdLogger.Printf("WARNING: %s: Current time (%s) is before certificate's NotBefore time (%s).", + parentCmdName, time.Now().Format(timeLoggingFormat), p15.Cert.NotBefore.Format(timeLoggingFormat)) + warned = true + } + + if time.Now().After(p15.Cert.NotAfter) { + app.stdLogger.Printf("WARNING: %s: Current time (%s) is after certificate's NotAfter time (%s).", + parentCmdName, time.Now().Format(timeLoggingFormat), p15.Cert.NotAfter.Format(timeLoggingFormat)) + warned = true + } + + // check extensions against known working extensions + for _, extension := range p15.Cert.Extensions { + // critical or not? + okOIDs := knownSupportedCriticalOIDs + criticalLogMsg := "Critical " + if !extension.Critical { + okOIDs = knownSupportedOIDs + criticalLogMsg = "" + } + + // validate OIDs + ok := false + for _, okOID := range okOIDs { + if okOID.Equal(extension.Id) { + ok = true + break + } + } + + if !ok { + app.stdLogger.Printf("WARNING: %s: %sExtension %s may not be supported by NMC.", parentCmdName, criticalLogMsg, extension.Id.String()) + } + } + + // log a message about possible failure + if warned { + app.stdLogger.Printf("WARNING: %s: Possible certificate compatibility issues were detected. If the resulting p15 file "+ + "does not work with your NMC (e.g., a self-signed certificate is regenerated after you try to install the p15), "+ + "modify your certificate to resolve the warnings and try again.", parentCmdName) + } + + // end compatibility warnings + app.stdLogger.Printf("%s: apc p15 file(s) data succesfully generated", parentCmdName) return keyFile, apcKeyCertFile, nil diff --git a/pkg/pkcs15/encrypted_envelope.go b/pkg/pkcs15/encrypted_envelope.go index 71433d1..a6dc525 100644 --- a/pkg/pkcs15/encrypted_envelope.go +++ b/pkg/pkcs15/encrypted_envelope.go @@ -25,7 +25,7 @@ const ( // params expected in the APC file. func (p15 *pkcs15KeyCert) computeEncryptedKeyEnvelope() error { // if computation already performed, this is a no-op (keep existing envelope) - if p15.envelopedPrivateKey != nil && len(p15.envelopedPrivateKey) != 0 { + if len(p15.envelopedPrivateKey) > 0 { return nil } diff --git a/pkg/pkcs15/keyid.go b/pkg/pkcs15/keyid.go index 08a3ce4..cac7301 100644 --- a/pkg/pkcs15/keyid.go +++ b/pkg/pkcs15/keyid.go @@ -15,7 +15,7 @@ func (p15 *pkcs15KeyCert) keyId() []byte { // SHA-1 Hash hasher := sha1.New() - _, err := hasher.Write(p15.cert.RawSubjectPublicKeyInfo) + _, err := hasher.Write(p15.Cert.RawSubjectPublicKeyInfo) if err != nil { panic(err) } @@ -46,9 +46,9 @@ func (p15 *pkcs15KeyCert) keyIdInt3() []byte { // object to hash hashObj := asn1obj.Sequence([][]byte{ // issuerDistinguishedName - p15.cert.RawIssuer, + p15.Cert.RawIssuer, // serialNumber - asn1obj.Integer(p15.cert.SerialNumber), + asn1obj.Integer(p15.Cert.SerialNumber), }) // SHA-1 Hash @@ -74,7 +74,7 @@ func (p15 *pkcs15KeyCert) keyIdInt6() []byte { // SHA-1 Hash hasher := sha1.New() - _, err := hasher.Write(p15.cert.RawIssuer) + _, err := hasher.Write(p15.Cert.RawIssuer) if err != nil { panic(err) } @@ -95,7 +95,7 @@ func (p15 *pkcs15KeyCert) keyIdInt7() []byte { // SHA-1 Hash hasher := sha1.New() - _, err := hasher.Write(p15.cert.RawSubject) + _, err := hasher.Write(p15.Cert.RawSubject) if err != nil { panic(err) } @@ -168,7 +168,7 @@ func (p15 *pkcs15KeyCert) keyIdInt9() []byte { // to be ~ 1 hour ish BEFORE the cert was even created. Key would also // obviously have to be created prior to the cert creation. time := make([]byte, 4) - binary.BigEndian.PutUint32(time, uint32(p15.cert.NotBefore.Unix())) + binary.BigEndian.PutUint32(time, uint32(p15.Cert.NotBefore.Unix())) publicKeyPacket = append(publicKeyPacket, time...) // the next part is key type specific diff --git a/pkg/pkcs15/pem_parse.go b/pkg/pkcs15/pem_parse.go index 19e44f1..9df0ecd 100644 --- a/pkg/pkcs15/pem_parse.go +++ b/pkg/pkcs15/pem_parse.go @@ -10,8 +10,8 @@ import ( // pkcs15KeyCert holds the data for a key and certificate pair; it provides // various methods to transform pkcs15 data type pkcs15KeyCert struct { + Cert *x509.Certificate key crypto.PrivateKey - cert *x509.Certificate // store the encrypted enveloped Private Key for re-use envelopedPrivateKey []byte } @@ -32,6 +32,31 @@ const ( KeyTypeUnknown ) +// String returns the private key type in a log friendly string format. +func (keyType KeyType) String() string { + switch keyType { + case KeyTypeRSA1024: + return "RSA 1024-bit" + case KeyTypeRSA2048: + return "RSA 2048-bit" + case KeyTypeRSA3072: + return "RSA 3072-bit" + case KeyTypeRSA4096: + return "RSA 4096-bit" + + case KeyTypeECP256: + return "ECDSA P-256" + case KeyTypeECP384: + return "ECDSA P-384" + case KeyTypeECP521: + return "ECDSA P-521" + + default: + } + + return "unknown key type" +} + // KeyType returns the private key type func (p15 *pkcs15KeyCert) KeyType() KeyType { switch pKey := p15.key.(type) { @@ -85,7 +110,7 @@ func ParsePEMToPKCS15(keyPem, certPem []byte) (*pkcs15KeyCert, error) { // create p15 struct p15 := &pkcs15KeyCert{ key: key, - cert: cert, + Cert: cert, } // pre-calculate encrypted envelope diff --git a/pkg/pkcs15/pem_to_p15.go b/pkg/pkcs15/pem_to_p15.go index 0c2214d..a68aba2 100644 --- a/pkg/pkcs15/pem_to_p15.go +++ b/pkg/pkcs15/pem_to_p15.go @@ -43,9 +43,9 @@ func (p15 *pkcs15KeyCert) ToP15KeyCert() (keyCert []byte, err error) { // CommonKeyAttributes - accessFlags (trailing 0s will drop) asn1obj.BitString([]byte{byte(0b10110000)}), // CommonKeyAttributes - startDate - asn1obj.GeneralizedTime(p15.cert.NotBefore), + asn1obj.GeneralizedTime(p15.Cert.NotBefore), // CommonKeyAttributes - [0] endDate - asn1obj.GeneralizedTimeExplicitValue(0, p15.cert.NotAfter), + asn1obj.GeneralizedTimeExplicitValue(0, p15.Cert.NotAfter), }), // ObjectValue - indirect-protected asn1obj.ExplicitCompound(1, [][]byte{ @@ -74,9 +74,9 @@ func (p15 *pkcs15KeyCert) ToP15KeyCert() (keyCert []byte, err error) { // CommonKeyAttributes - accessFlags (trailing 0s will drop) asn1obj.BitString([]byte{byte(0b10110000)}), // CommonKeyAttributes - startDate - asn1obj.GeneralizedTime(p15.cert.NotBefore), + asn1obj.GeneralizedTime(p15.Cert.NotBefore), // CommonKeyAttributes - [0] endDate - asn1obj.GeneralizedTimeExplicitValue(0, p15.cert.NotAfter), + asn1obj.GeneralizedTimeExplicitValue(0, p15.Cert.NotAfter), }), // ObjectValue - indirect-protected asn1obj.ExplicitCompound(1, [][]byte{ @@ -114,15 +114,15 @@ func (p15 *pkcs15KeyCert) ToP15KeyCert() (keyCert []byte, err error) { p15.keyIdInt9(), }), // CommonKeyAttributes - startDate - asn1obj.GeneralizedTime(p15.cert.NotBefore), + asn1obj.GeneralizedTime(p15.Cert.NotBefore), // CommonKeyAttributes - [4] endDate - asn1obj.GeneralizedTimeExplicitValue(4, p15.cert.NotAfter), + asn1obj.GeneralizedTimeExplicitValue(4, p15.Cert.NotAfter), }), // actual certificate itself asn1obj.ExplicitCompound(1, [][]byte{ asn1obj.Sequence([][]byte{ asn1obj.ExplicitCompound(0, [][]byte{ - p15.cert.Raw, + p15.Cert.Raw, }), }), }),