From 017a6b0a10c109403bf458a2ca109c4bf5934b25 Mon Sep 17 00:00:00 2001 From: "Sascha L. Teichmann" Date: Wed, 2 Aug 2023 21:02:58 +0200 Subject: [PATCH] Move cert handling into library and add option passphrase. Adjust uploader and checker. --- cmd/csaf_checker/config.go | 41 ++++++++++----------- cmd/csaf_uploader/main.go | 28 +++++++-------- docs/csaf_checker.md | 62 ++++++++++++++++---------------- docs/csaf_uploader.md | 9 +++-- internal/certs/certs.go | 74 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 140 insertions(+), 74 deletions(-) create mode 100644 internal/certs/certs.go diff --git a/cmd/csaf_checker/config.go b/cmd/csaf_checker/config.go index 396df81..6a04e00 100644 --- a/cmd/csaf_checker/config.go +++ b/cmd/csaf_checker/config.go @@ -14,6 +14,7 @@ import ( "fmt" "net/http" + "github.com/csaf-poc/csaf_distribution/v2/internal/certs" "github.com/csaf-poc/csaf_distribution/v2/internal/filter" "github.com/csaf-poc/csaf_distribution/v2/internal/models" "github.com/csaf-poc/csaf_distribution/v2/internal/options" @@ -29,17 +30,18 @@ const ( type config struct { Output string `short:"o" long:"output" description:"File name of the generated report" value-name:"REPORT-FILE" toml:"output"` //lint:ignore SA5008 We are using choice twice: json, html. - Format outputFormat `short:"f" long:"format" choice:"json" choice:"html" description:"Format of report" toml:"format"` - Insecure bool `long:"insecure" description:"Do not check TLS certificates from provider" toml:"insecure"` - ClientCert *string `long:"client-cert" description:"TLS client certificate file (PEM encoded data)" value-name:"CERT-FILE" toml:"client_cert"` - ClientKey *string `long:"client-key" description:"TLS client private key file (PEM encoded data)" value-name:"KEY-FILE" toml:"client_key"` - Version bool `long:"version" description:"Display version of the binary" toml:"-"` - Verbose bool `long:"verbose" short:"v" description:"Verbose output" toml:"verbose"` - Rate *float64 `long:"rate" short:"r" description:"The average upper limit of https operations per second (defaults to unlimited)" toml:"rate"` - Years *uint `long:"years" short:"y" description:"Number of years to look back from now" value-name:"YEARS" toml:"years"` - Range *models.TimeRange `long:"timerange" short:"t" description:"RANGE of time from which advisories to download" value-name:"RANGE" toml:"timerange"` - IgnorePattern []string `long:"ignorepattern" short:"i" description:"Dont download files if there URLs match any of the given PATTERNs" value-name:"PATTERN" toml:"ignorepattern"` - ExtraHeader http.Header `long:"header" short:"H" description:"One or more extra HTTP header fields" toml:"header"` + Format outputFormat `short:"f" long:"format" choice:"json" choice:"html" description:"Format of report" toml:"format"` + Insecure bool `long:"insecure" description:"Do not check TLS certificates from provider" toml:"insecure"` + ClientCert *string `long:"client-cert" description:"TLS client certificate file (PEM encoded data)" value-name:"CERT-FILE" toml:"client_cert"` + ClientKey *string `long:"client-key" description:"TLS client private key file (PEM encoded data)" value-name:"KEY-FILE" toml:"client_key"` + ClientPassphrase *string `long:"client-passphrase" description:"Optional passphrase for the client certificate" value-name:"PASSPHRASE" toml:"client_passphrase"` + Version bool `long:"version" description:"Display version of the binary" toml:"-"` + Verbose bool `long:"verbose" short:"v" description:"Verbose output" toml:"verbose"` + Rate *float64 `long:"rate" short:"r" description:"The average upper limit of https operations per second (defaults to unlimited)" toml:"rate"` + Years *uint `long:"years" short:"y" description:"Number of years to look back from now" value-name:"YEARS" toml:"years"` + Range *models.TimeRange `long:"timerange" short:"t" description:"RANGE of time from which advisories to download" value-name:"RANGE" toml:"timerange"` + IgnorePattern []string `long:"ignorepattern" short:"i" description:"Dont download files if there URLs match any of the given PATTERNs" value-name:"PATTERN" toml:"ignorepattern"` + ExtraHeader http.Header `long:"header" short:"H" description:"One or more extra HTTP header fields" toml:"header"` RemoteValidator string `long:"validator" description:"URL to validate documents remotely" value-name:"URL" toml:"validator"` RemoteValidatorCache string `long:"validatorcache" description:"FILE to cache remote validations" value-name:"FILE" toml:"validator_cache"` @@ -139,19 +141,12 @@ func (cfg *config) compileIgnorePatterns() error { // prepareCertificates loads the client side certificates used by the HTTP client. func (cfg *config) prepareCertificates() error { - - switch hasCert, hasKey := cfg.ClientCert != nil, cfg.ClientKey != nil; { - - case hasCert && !hasKey || !hasCert && hasKey: - return errors.New("both client-key and client-cert options must be set for the authentication") - - case hasCert: - cert, err := tls.LoadX509KeyPair(*cfg.ClientCert, *cfg.ClientKey) - if err != nil { - return err - } - cfg.clientCerts = []tls.Certificate{cert} + cert, err := certs.LoadCertificate( + cfg.ClientCert, cfg.ClientKey, cfg.ClientPassphrase) + if err != nil { + return err } + cfg.clientCerts = cert return nil } diff --git a/cmd/csaf_uploader/main.go b/cmd/csaf_uploader/main.go index a48f52e..48844a7 100644 --- a/cmd/csaf_uploader/main.go +++ b/cmd/csaf_uploader/main.go @@ -28,6 +28,7 @@ import ( "github.com/ProtonMail/gopenpgp/v2/constants" "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/csaf-poc/csaf_distribution/v2/csaf" + "github.com/csaf-poc/csaf_distribution/v2/internal/certs" "github.com/csaf-poc/csaf_distribution/v2/util" "github.com/jessevdk/go-flags" "github.com/mitchellh/go-homedir" @@ -43,11 +44,12 @@ type options struct { ExternalSigned bool `short:"x" long:"external-signed" description:"CSAF files are signed externally. Assumes .asc files beside CSAF files."` NoSchemaCheck bool `short:"s" long:"no-schema-check" description:"Do not check files against CSAF JSON schema locally."` - Key *string `short:"k" long:"key" description:"OpenPGP key to sign the CSAF files" value-name:"KEY-FILE"` - Password *string `short:"p" long:"password" description:"Authentication password for accessing the CSAF provider" value-name:"PASSWORD"` - Passphrase *string `short:"P" long:"passphrase" description:"Passphrase to unlock the OpenPGP key" value-name:"PASSPHRASE"` - ClientCert *string `long:"client-cert" description:"TLS client certificate file (PEM encoded data)" value-name:"CERT-FILE.crt"` - ClientKey *string `long:"client-key" description:"TLS client private key file (PEM encoded data)" value-name:"KEY-FILE.pem"` + Key *string `short:"k" long:"key" description:"OpenPGP key to sign the CSAF files" value-name:"KEY-FILE"` + Password *string `short:"p" long:"password" description:"Authentication password for accessing the CSAF provider" value-name:"PASSWORD"` + Passphrase *string `short:"P" long:"passphrase" description:"Passphrase to unlock the OpenPGP key" value-name:"PASSPHRASE"` + ClientCert *string `long:"client-cert" description:"TLS client certificate file (PEM encoded data)" value-name:"CERT-FILE.crt"` + ClientKey *string `long:"client-key" description:"TLS client private key file (PEM encoded data)" value-name:"KEY-FILE.pem"` + ClientPassphrase *string `long:"client-passphrase" description:"Optional passphrase for the client certificate" value-name:"PASSPHRASE"` PasswordInteractive bool `short:"i" long:"password-interactive" description:"Enter password interactively" no-ini:"true"` PassphraseInteractive bool `short:"I" long:"passphrase-interactive" description:"Enter OpenPGP key passphrase interactively" no-ini:"true"` @@ -75,18 +77,12 @@ var iniPaths = []string{ func (o *options) prepare() error { // Load client certs. - switch hasCert, hasKey := o.ClientCert != nil, o.ClientKey != nil; { - - case hasCert && !hasKey || !hasCert && hasKey: - return errors.New("both client-key and client-cert options must be set for the authentication") - - case hasCert: - cert, err := tls.LoadX509KeyPair(*o.ClientCert, *o.ClientKey) - if err != nil { - return err - } - o.clientCerts = []tls.Certificate{cert} + cert, err := certs.LoadCertificate( + o.ClientCert, o.ClientKey, o.ClientPassphrase) + if err != nil { + return err } + o.clientCerts = cert return nil } diff --git a/docs/csaf_checker.md b/docs/csaf_checker.md index 4bc62ef..7c32af0 100644 --- a/docs/csaf_checker.md +++ b/docs/csaf_checker.md @@ -7,25 +7,26 @@ Usage: csaf_checker [OPTIONS] domain... Application Options: - -o, --output=REPORT-FILE File name of the generated report - -f, --format=[json|html] Format of report (default: json) - --insecure Do not check TLS certificates from provider - --client-cert=CERT-FILE TLS client certificate file (PEM encoded data) - --client-key=KEY-FILE TLS client private key file (PEM encoded data) - --version Display version of the binary - -v, --verbose Verbose output - -r, --rate= The average upper limit of https operations per second (defaults to unlimited) - -y, --years=YEARS Number of years to look back from now - -t, --timerange=RANGE RANGE of time from which advisories to download - -i, --ignorepattern=PATTERN Dont download files if there URLs match any of the given PATTERNs - -H, --header= One or more extra HTTP header fields - --validator=URL URL to validate documents remotely - --validatorcache=FILE FILE to cache remote validations - --validatorpreset= One or more presets to validate remotely (default: [mandatory]) - -c, --config=TOML-FILE Path to config TOML file + -o, --output=REPORT-FILE File name of the generated report + -f, --format=[json|html] Format of report (default: json) + --insecure Do not check TLS certificates from provider + --client-cert=CERT-FILE TLS client certificate file (PEM encoded data) + --client-key=KEY-FILE TLS client private key file (PEM encoded data) + --client-passphrase=PASSPHRASE Optional passphrase for the client certificate + --version Display version of the binary + -v, --verbose Verbose output + -r, --rate= The average upper limit of https operations per second (defaults to unlimited) + -y, --years=YEARS Number of years to look back from now + -t, --timerange=RANGE RANGE of time from which advisories to download + -i, --ignorepattern=PATTERN Dont download files if there URLs match any of the given PATTERNs + -H, --header= One or more extra HTTP header fields + --validator=URL URL to validate documents remotely + --validatorcache=FILE FILE to cache remote validations + --validatorpreset= One or more presets to validate remotely (default: [mandatory]) + -c, --config=TOML-FILE Path to config TOML file Help Options: - -h, --help Show this help message + -h, --help Show this help message ``` Will check all given _domains_, by trying each as a CSAF provider. @@ -42,19 +43,20 @@ csaf_checker.toml with `~` expanding to `$HOME` on unixoid systems and `%HOMEPATH` on Windows systems. Supported options in config files: ``` -output = "" -format = "json" -insecure = false -# client_cert # not set by default -# client_key # not set by default -verbose = false -# rate # not set by default -# years # not set by default -# timerange # not set by default -# header # not set by default -# validator # not set by default -# validatorcache # not set by default -validatorpreset = ["mandatory"] +output = "" +format = "json" +insecure = false +# client_cert # not set by default +# client_key # not set by default +# client_passphrase # not set by default +verbose = false +# rate # not set by default +# years # not set by default +# timerange # not set by default +# header # not set by default +# validator # not set by default +# validatorcache # not set by default +validatorpreset = ["mandatory"] ``` Usage example: diff --git a/docs/csaf_uploader.md b/docs/csaf_uploader.md index 9998351..6bb3213 100644 --- a/docs/csaf_uploader.md +++ b/docs/csaf_uploader.md @@ -7,19 +7,18 @@ Application Options: -a, --action=[upload|create] Action to perform (default: upload) - -u, --url=URL URL of the CSAF provider (default: - https://localhost/cgi-bin/csaf_provider.go) + -u, --url=URL URL of the CSAF provider (default: https://localhost/cgi-bin/csaf_provider.go) -t, --tlp=[csaf|white|green|amber|red] TLP of the feed (default: csaf) - -x, --external-signed CSAF files are signed externally. Assumes .asc files - beside CSAF files. + -x, --external-signed CSAF files are signed externally. Assumes .asc files beside CSAF files. -s, --no-schema-check Do not check files against CSAF JSON schema locally. -k, --key=KEY-FILE OpenPGP key to sign the CSAF files -p, --password=PASSWORD Authentication password for accessing the CSAF provider -P, --passphrase=PASSPHRASE Passphrase to unlock the OpenPGP key --client-cert=CERT-FILE.crt TLS client certificate file (PEM encoded data) --client-key=KEY-FILE.pem TLS client private key file (PEM encoded data) + --client-passphrase=PASSPHRASE Optional passphrase for the client certificate -i, --password-interactive Enter password interactively - -I, --passphrase-interactive Enter passphrase interactively + -I, --passphrase-interactive Enter OpenPGP key passphrase interactively --insecure Do not check TLS certificates from provider -c, --config=INI-FILE Path to config ini file --version Display version of the binary diff --git a/internal/certs/certs.go b/internal/certs/certs.go new file mode 100644 index 0000000..dcbf7ef --- /dev/null +++ b/internal/certs/certs.go @@ -0,0 +1,74 @@ +// This file is Free Software under the MIT License +// without warranty, see README.md and LICENSES/MIT.txt for details. +// +// SPDX-License-Identifier: MIT +// +// SPDX-FileCopyrightText: 2023 German Federal Office for Information Security (BSI) +// Software-Engineering: 2023 Intevation GmbH + +// Package certs implement helpers for the tools to handle client side certifacates. +package certs + +import ( + "crypto/tls" + "crypto/x509" + "encoding/pem" + "errors" + "os" +) + +// LoadCertificate loads an client certificate from file with an optional passphrase. +// Returns nil if no certificate was loaded. +func LoadCertificate(certFile, keyFile, passphrase *string) ([]tls.Certificate, error) { + + switch hasCert, hasKey := certFile != nil, keyFile != nil; { + + case hasCert && !hasKey || !hasCert && hasKey: + return nil, errors.New( + "both client-key and client-cert options must be set for the authentication") + + case hasCert: + // No passphrase + if passphrase == nil { + cert, err := tls.LoadX509KeyPair(*certFile, *keyFile) + if err != nil { + return nil, err + } + return []tls.Certificate{cert}, nil + } + + // With passphrase + keyFile, err := os.ReadFile(*keyFile) + if err != nil { + return nil, err + } + keyBlock, _ := pem.Decode(keyFile) + + //lint:ignore SA1019 This is insecure by design. + keyDER, err := x509.DecryptPEMBlock(keyBlock, []byte(*passphrase)) + if err != nil { + return nil, err + } + // Update keyBlock with the plaintext bytes and clear the now obsolete + // headers. + keyBlock.Bytes = keyDER + keyBlock.Headers = nil + + // Turn the key back into PEM format so we can leverage tls.X509KeyPair, + // which will deal with the intricacies of error handling, different key + // types, certificate chains, etc + keyPEM := pem.EncodeToMemory(keyBlock) + + certPEMBlock, err := os.ReadFile(*certFile) + if err != nil { + return nil, err + } + cert, err := tls.X509KeyPair(certPEMBlock, keyPEM) + if err != nil { + return nil, err + } + return []tls.Certificate{cert}, nil + } + + return nil, nil +}