mirror of
https://github.com/gocsaf/csaf.git
synced 2025-12-22 11:55:40 +01:00
Merge branch 'main' into improve_tlp_error_message
This commit is contained in:
commit
cffc7aaa66
10 changed files with 345 additions and 49 deletions
57
Makefile
Normal file
57
Makefile
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
# 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: 2021 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
|
||||||
|
# Software-Engineering: 2021 Intevation GmbH <https://intevation.de>
|
||||||
|
#
|
||||||
|
# Makefile to build csaf_distribution components
|
||||||
|
|
||||||
|
SHELL = /bin/bash
|
||||||
|
BUILD = go build
|
||||||
|
MKDIR = mkdir -p
|
||||||
|
|
||||||
|
.PHONY: build build_linux build_win tag_checked_out mostlyclean
|
||||||
|
|
||||||
|
all:
|
||||||
|
@echo choose a target from: build build_linux build_win mostlyclean
|
||||||
|
@echo prepend \`make BUILDTAG=1\` to checkout the highest git tag before building
|
||||||
|
@echo or set BUILDTAG to a specific tag
|
||||||
|
|
||||||
|
# Build all binaries
|
||||||
|
build: build_linux build_win
|
||||||
|
|
||||||
|
# if BUILDTAG == 1 set it to the highest git tag
|
||||||
|
ifeq ($(strip $(BUILDTAG)),1)
|
||||||
|
override BUILDTAG = $(shell git tag --sort=-version:refname | head -n 1)
|
||||||
|
endif
|
||||||
|
|
||||||
|
ifdef BUILDTAG
|
||||||
|
# add the git tag checkout to the requirements of our build targets
|
||||||
|
build_linux build_win: tag_checked_out
|
||||||
|
endif
|
||||||
|
|
||||||
|
tag_checked_out:
|
||||||
|
$(if $(strip $(BUILDTAG)),,$(error no git tag found))
|
||||||
|
git checkout -q tags/${BUILDTAG}
|
||||||
|
@echo Don\'t forget that we are in checked out tag $(BUILDTAG) now.
|
||||||
|
|
||||||
|
|
||||||
|
# Build binaries and place them under bin-$(GOOS)-$(GOARCH)
|
||||||
|
# Using 'Target-specific Variable Values' to specify the build target system
|
||||||
|
|
||||||
|
GOARCH = amd64
|
||||||
|
build_linux: GOOS = linux
|
||||||
|
build_win: GOOS = windows
|
||||||
|
|
||||||
|
build_linux build_win:
|
||||||
|
$(eval BINDIR = bin-$(GOOS)-$(GOARCH)/ )
|
||||||
|
$(MKDIR) $(BINDIR)
|
||||||
|
env GOARCH=$(GOARCH) GOOS=$(GOOS) $(BUILD) -o $(BINDIR) -v ./cmd/...
|
||||||
|
|
||||||
|
|
||||||
|
# Remove bin-*-* directories
|
||||||
|
mostlyclean:
|
||||||
|
rm -rf ./bin-*-*
|
||||||
|
@echo Files in \`go env GOCACHE\` remain.
|
||||||
23
README.md
23
README.md
|
|
@ -2,23 +2,29 @@
|
||||||
|
|
||||||
**WIP**: A proof of concept for a CSAF trusted provider, checker and aggregator.
|
**WIP**: A proof of concept for a CSAF trusted provider, checker and aggregator.
|
||||||
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
- A recent version of **Go** (1.17+) should be installed. [Go installation](https://go.dev/doc/install)
|
- A recent version of **Go** (1.17+) should be installed. [Go installation](https://go.dev/doc/install)
|
||||||
|
|
||||||
- Clone the repository `git clone https://github.com/csaf-poc/csaf_distribution.git `
|
- Clone the repository `git clone https://github.com/csaf-poc/csaf_distribution.git `
|
||||||
|
|
||||||
- Build Go components
|
- Build Go components Makefile supplies the following targets:
|
||||||
``` bash
|
- Build For GNU/Linux System: `make build_linux`
|
||||||
cd csaf_distribution
|
- Build For Windows System (cross build): `make build_win`
|
||||||
go build -v ./cmd/...
|
- Build For both linux and windows: `make build`
|
||||||
```
|
- Build from a specific github tag by passing the intended tag to the `BUILDTAG` variable.
|
||||||
|
E.g. `make BUILDTAG=v1.0.0 build` or `make BUILDTAG=1 build_linux`.
|
||||||
|
The special value `1` means checking out the highest github tag for the build.
|
||||||
|
- Remove the generated binaries und their directories: `make mostlyclean`
|
||||||
|
|
||||||
- [Install](http://nginx.org/en/docs/install.html) **nginx**
|
Binaries will be placed in directories named like `bin-linux-amd64/` and `bin-windows-amd64/`.
|
||||||
|
|
||||||
|
- [Install](https://nginx.org/en/docs/install.html) **nginx**
|
||||||
|
- To install server certificate on nginx see [docs/install-server-certificate.md](docs/install-server-certificate.md)
|
||||||
- To configure nginx see [docs/provider-setup.md](docs/provider-setup.md)
|
- To configure nginx see [docs/provider-setup.md](docs/provider-setup.md)
|
||||||
|
|
||||||
## csaf_uploader
|
## csaf_uploader
|
||||||
|
|
||||||
csaf_uploader is a command line tool that uploads CSAF documents to the trusted provider (CSAF_Provider).
|
csaf_uploader is a command line tool that uploads CSAF documents to the trusted provider (CSAF_Provider).
|
||||||
Following options are supported:
|
Following options are supported:
|
||||||
|
|
||||||
|
|
@ -27,13 +33,14 @@ Following options are supported:
|
||||||
| -a, --action=[upload\|create] | Action to perform (default: upload) |
|
| -a, --action=[upload\|create] | Action to perform (default: upload) |
|
||||||
| -u, --url=URL | URL of the CSAF provider (default:https:<span></span>//localhost/cgi-bin/csaf_provider.go) |
|
| -u, --url=URL | URL of the CSAF provider (default:https:<span></span>//localhost/cgi-bin/csaf_provider.go) |
|
||||||
| -t, --tlp=[csaf\|white\|green\|amber\|red] | TLP of the feed (default: csaf) |
|
| -t, --tlp=[csaf\|white\|green\|amber\|red] | TLP of the feed (default: csaf) |
|
||||||
| -x, --external-signed | CASF files are signed externally. |
|
| -x, --external-signed | CASF files are signed externally. Assumes .asc files beside CSAF files |
|
||||||
| -k, --key=KEY-FILE | OpenPGP key to sign the CSAF files |
|
| -k, --key=KEY-FILE | OpenPGP key to sign the CSAF files |
|
||||||
| -p, --password=PASSWORD | Authentication password for accessing the CSAF provider |
|
| -p, --password=PASSWORD | Authentication password for accessing the CSAF provider |
|
||||||
| -P, --passphrase=PASSPHRASE | Passphrase to unlock the OpenPGP key |
|
| -P, --passphrase=PASSPHRASE | Passphrase to unlock the OpenPGP key |
|
||||||
| -i, --password-interactive | Enter password interactively |
|
| -i, --password-interactive | Enter password interactively |
|
||||||
| -I, --passphrase-interacive | Enter passphrase interactively |
|
| -I, --passphrase-interacive | Enter passphrase interactively |
|
||||||
| -c, --config=INI-FILE | Path to config ini file |
|
| -c, --config=INI-FILE | Path to config ini file |
|
||||||
|
| --insecure | Do not check TSL certificates from provider |
|
||||||
| -h, --help | Show help |
|
| -h, --help | Show help |
|
||||||
|
|
||||||
E.g. creating the initial directiories and files
|
E.g. creating the initial directiories and files
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
@ -43,6 +44,7 @@ type processor struct {
|
||||||
redirects map[string]string
|
redirects map[string]string
|
||||||
noneTLS map[string]struct{}
|
noneTLS map[string]struct{}
|
||||||
alreadyChecked map[string]whereType
|
alreadyChecked map[string]whereType
|
||||||
|
pmdURL string
|
||||||
pmd256 []byte
|
pmd256 []byte
|
||||||
pmd interface{}
|
pmd interface{}
|
||||||
keys []*crypto.KeyRing
|
keys []*crypto.KeyRing
|
||||||
|
|
@ -119,6 +121,7 @@ func (p *processor) clean() {
|
||||||
for k := range p.alreadyChecked {
|
for k := range p.alreadyChecked {
|
||||||
delete(p.alreadyChecked, k)
|
delete(p.alreadyChecked, k)
|
||||||
}
|
}
|
||||||
|
p.pmdURL = ""
|
||||||
p.pmd256 = nil
|
p.pmd256 = nil
|
||||||
p.pmd = nil
|
p.pmd = nil
|
||||||
p.keys = nil
|
p.keys = nil
|
||||||
|
|
@ -131,6 +134,7 @@ func (p *processor) clean() {
|
||||||
p.badIndices = nil
|
p.badIndices = nil
|
||||||
p.badChanges = nil
|
p.badChanges = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) run(reporters []reporter, domains []string) (*Report, error) {
|
func (p *processor) run(reporters []reporter, domains []string) (*Report, error) {
|
||||||
|
|
||||||
var report Report
|
var report Report
|
||||||
|
|
@ -607,7 +611,7 @@ func (p *processor) checkChanges(base string, mask whereType) error {
|
||||||
|
|
||||||
func (p *processor) processROLIEFeeds(domain string, feeds [][]csaf.Feed) error {
|
func (p *processor) processROLIEFeeds(domain string, feeds [][]csaf.Feed) error {
|
||||||
|
|
||||||
base, err := url.Parse("https://" + domain + "/.well-known/csaf/")
|
base, err := url.Parse(p.pmdURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -654,7 +658,10 @@ func (p *processor) checkCSAFs(domain string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// No rolie feeds
|
// No rolie feeds
|
||||||
base := "https://" + domain + "/.well-known/csaf"
|
base, err := basePath(p.pmdURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := p.checkIndex(base, indexMask); err != nil && err != errContinue {
|
if err := p.checkIndex(base, indexMask); err != nil && err != errContinue {
|
||||||
return err
|
return err
|
||||||
|
|
@ -701,50 +708,144 @@ func (p *processor) checkMissing(string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) checkProviderMetadata(domain string) error {
|
var providerMetadataLocations = [...]string{
|
||||||
|
".well-known/csaf",
|
||||||
|
"security/data/csaf",
|
||||||
|
"advisories/csaf",
|
||||||
|
"security/csaf",
|
||||||
|
}
|
||||||
|
|
||||||
|
// locateProviderMetadata searches for provider-metadata.json at various
|
||||||
|
// locations mentioned in "7.1.7 Requirement 7: provider-metadata.json".
|
||||||
|
func (p *processor) locateProviderMetadata(
|
||||||
|
domain string,
|
||||||
|
found func(string, io.Reader) error,
|
||||||
|
) error {
|
||||||
|
|
||||||
client := p.httpClient()
|
client := p.httpClient()
|
||||||
|
|
||||||
url := "https://" + domain + "/.well-known/csaf/provider-metadata.json"
|
tryURL := func(url string) (bool, error) {
|
||||||
|
res, err := client.Get(url)
|
||||||
|
if err != nil || res.StatusCode != http.StatusOK ||
|
||||||
|
res.Header.Get("Content-Type") != "application/json" {
|
||||||
|
// ignore this as it is expected.
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
use(&p.badProviderMetadatas)
|
if err := func() error {
|
||||||
|
defer res.Body.Close()
|
||||||
res, err := client.Get(url)
|
return found(url, res.Body)
|
||||||
if err != nil {
|
}(); err != nil {
|
||||||
p.badProviderMetadata("Fetching %s: %v.", url, err)
|
return false, err
|
||||||
return errStop
|
}
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
for _, loc := range providerMetadataLocations {
|
||||||
p.badProviderMetadata("Fetching %s failed. Status code: %d (%s)",
|
url := "https://" + domain + "/" + loc
|
||||||
url, res.StatusCode, res.Status)
|
ok, err := tryURL(url)
|
||||||
return errStop
|
if err != nil {
|
||||||
|
if err == errContinue {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate checksum for later comparison.
|
// Read from security.txt
|
||||||
hash := sha256.New()
|
|
||||||
|
|
||||||
if err := func() error {
|
path := "https://" + domain + "/.well-known/security.txt"
|
||||||
defer res.Body.Close()
|
res, err := client.Get(path)
|
||||||
tee := io.TeeReader(res.Body, hash)
|
|
||||||
return json.NewDecoder(tee).Decode(&p.pmd)
|
|
||||||
}(); err != nil {
|
|
||||||
p.badProviderMetadata("Decoding JSON failed: %v", err)
|
|
||||||
return errStop
|
|
||||||
}
|
|
||||||
|
|
||||||
p.pmd256 = hash.Sum(nil)
|
|
||||||
|
|
||||||
errors, err := csaf.ValidateProviderMetadata(p.pmd)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(errors) > 0 {
|
|
||||||
p.badProviderMetadata("Validating against JSON schema failed:")
|
if res.StatusCode != http.StatusOK {
|
||||||
for _, msg := range errors {
|
return err
|
||||||
p.badProviderMetadata(strings.ReplaceAll(msg, `%`, `%%`))
|
}
|
||||||
|
|
||||||
|
loc, err := func() (string, error) {
|
||||||
|
defer res.Body.Close()
|
||||||
|
return extractProviderURL(res.Body)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error: %v\n", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if loc != "" {
|
||||||
|
if _, err = tryURL(loc); err == errContinue {
|
||||||
|
err = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractProviderURL(r io.Reader) (string, error) {
|
||||||
|
sc := bufio.NewScanner(r)
|
||||||
|
const csaf = "CSAF:"
|
||||||
|
|
||||||
|
for sc.Scan() {
|
||||||
|
line := sc.Text()
|
||||||
|
if strings.HasPrefix(line, csaf) {
|
||||||
|
line = strings.TrimSpace(line[len(csaf):])
|
||||||
|
if !strings.HasPrefix(line, "https://") {
|
||||||
|
return "", errors.New("CASF: found in security.txt, but does not start with https://")
|
||||||
|
}
|
||||||
|
return line, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := sc.Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *processor) checkProviderMetadata(domain string) error {
|
||||||
|
|
||||||
|
use(&p.badProviderMetadatas)
|
||||||
|
|
||||||
|
found := func(url string, content io.Reader) error {
|
||||||
|
|
||||||
|
// Calculate checksum for later comparison.
|
||||||
|
hash := sha256.New()
|
||||||
|
|
||||||
|
tee := io.TeeReader(content, hash)
|
||||||
|
if err := json.NewDecoder(tee).Decode(&p.pmd); err != nil {
|
||||||
|
p.badProviderMetadata("%s: Decoding JSON failed: %v", url, err)
|
||||||
|
return errContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
p.pmd256 = hash.Sum(nil)
|
||||||
|
|
||||||
|
errors, err := csaf.ValidateProviderMetadata(p.pmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(errors) > 0 {
|
||||||
|
p.badProviderMetadata("%s: Validating against JSON schema failed:", url)
|
||||||
|
for _, msg := range errors {
|
||||||
|
p.badProviderMetadata(strings.ReplaceAll(msg, `%`, `%%`))
|
||||||
|
}
|
||||||
|
return errStop
|
||||||
|
}
|
||||||
|
p.pmdURL = url
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.locateProviderMetadata(domain, found); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.pmdURL == "" {
|
||||||
|
p.badProviderMetadata("No provider-metadata.json found.")
|
||||||
|
return errStop
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -851,7 +952,7 @@ func (p *processor) checkPGPKeys(domain string) error {
|
||||||
|
|
||||||
client := p.httpClient()
|
client := p.httpClient()
|
||||||
|
|
||||||
base, err := url.Parse("https://" + domain + "/.well-known/csaf/provider-metadata.json")
|
base, err := url.Parse(p.pmdURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ import (
|
||||||
|
|
||||||
const dateFormat = time.RFC3339
|
const dateFormat = time.RFC3339
|
||||||
|
|
||||||
|
// cleanFileName removes the "/" "\" charachters and replace the two or more
|
||||||
|
// occurences of "." with only one from the passed string.
|
||||||
func cleanFileName(s string) string {
|
func cleanFileName(s string) string {
|
||||||
s = strings.ReplaceAll(s, `/`, ``)
|
s = strings.ReplaceAll(s, `/`, ``)
|
||||||
s = strings.ReplaceAll(s, `\`, ``)
|
s = strings.ReplaceAll(s, `\`, ``)
|
||||||
|
|
@ -37,6 +39,10 @@ func cleanFileName(s string) string {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadCSAF loads the csaf file from the request, calls the "UploadLimter" function to
|
||||||
|
// set the upload limit size of the file and the "cleanFileName" to refine
|
||||||
|
// the filename. It returns the filename, file content in a buffer of bytes
|
||||||
|
// and an error.
|
||||||
func (c *controller) loadCSAF(r *http.Request) (string, []byte, error) {
|
func (c *controller) loadCSAF(r *http.Request) (string, []byte, error) {
|
||||||
file, handler, err := r.FormFile("csaf")
|
file, handler, err := r.FormFile("csaf")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -123,6 +129,8 @@ func (c *controller) tlpParam(r *http.Request) (tlp, error) {
|
||||||
return "", fmt.Errorf("unsupported TLP type '%s'", t)
|
return "", fmt.Errorf("unsupported TLP type '%s'", t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create calls the "ensureFolders" functions to create the directories and files.
|
||||||
|
// It returns a struct by success, otherwise an error.
|
||||||
func (c *controller) create(*http.Request) (interface{}, error) {
|
func (c *controller) create(*http.Request) (interface{}, error) {
|
||||||
if err := ensureFolders(c.cfg); err != nil {
|
if err := ensureFolders(c.cfg); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -21,14 +21,16 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// The environment name, that contains the path to the config file.
|
||||||
configEnv = "CSAF_CONFIG"
|
configEnv = "CSAF_CONFIG"
|
||||||
defaultConfigPath = "/usr/lib/casf/config.toml"
|
defaultConfigPath = "/usr/lib/casf/config.toml" // Default path to the config file.
|
||||||
defaultFolder = "/var/www/"
|
defaultFolder = "/var/www/" // Default folder path.
|
||||||
defaultWeb = "/var/www/html"
|
defaultWeb = "/var/www/html" // Default web path.
|
||||||
defaultOpenPGPURL = "https://openpgp.circl.lu/pks/lookup?op=get&search=${FINGERPRINT}"
|
defaultOpenPGPURL = "https://openpgp.circl.lu/pks/lookup?op=get&search=${FINGERPRINT}" // Default OpenPGP URL.
|
||||||
defaultUploadLimit = 50 * 1024 * 1024
|
defaultUploadLimit = 50 * 1024 * 1024 // Default limit size of the uploaded file.
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// configs contains the config values for the provider.
|
||||||
type config struct {
|
type config struct {
|
||||||
Password *string `toml:"password"`
|
Password *string `toml:"password"`
|
||||||
Key string `toml:"key"`
|
Key string `toml:"key"`
|
||||||
|
|
@ -56,6 +58,7 @@ const (
|
||||||
tlpRed tlp = "red"
|
tlpRed tlp = "red"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// valid returns true if the checked tlp matches one of the defined tlps.
|
||||||
func (t tlp) valid() bool {
|
func (t tlp) valid() bool {
|
||||||
switch t {
|
switch t {
|
||||||
case tlpCSAF, tlpWhite, tlpGreen, tlpAmber, tlpRed:
|
case tlpCSAF, tlpWhite, tlpGreen, tlpAmber, tlpRed:
|
||||||
|
|
@ -73,6 +76,8 @@ func (t *tlp) UnmarshalText(text []byte) error {
|
||||||
return fmt.Errorf("invalid config TLP value: %v", string(text))
|
return fmt.Errorf("invalid config TLP value: %v", string(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// uploadLimiter returns a reader that reads from a given r reader but stops
|
||||||
|
// with EOF after the defined bytes in the "UploadLimit" config option.
|
||||||
func (cfg *config) uploadLimiter(r io.Reader) io.Reader {
|
func (cfg *config) uploadLimiter(r io.Reader) io.Reader {
|
||||||
// Zero or less means no upload limit.
|
// Zero or less means no upload limit.
|
||||||
if cfg.UploadLimit == nil || *cfg.UploadLimit < 1 {
|
if cfg.UploadLimit == nil || *cfg.UploadLimit < 1 {
|
||||||
|
|
@ -100,6 +105,8 @@ func (cfg *config) modelTLPs() []csaf.TLPLabel {
|
||||||
return tlps
|
return tlps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadCryptoKey loads the armored data into the key stored in the file specified by the
|
||||||
|
// "key" config value and return it with nil, otherwise an error.
|
||||||
func (cfg *config) loadCryptoKey() (*crypto.Key, error) {
|
func (cfg *config) loadCryptoKey() (*crypto.Key, error) {
|
||||||
f, err := os.Open(cfg.Key)
|
f, err := os.Open(cfg.Key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -109,11 +116,18 @@ func (cfg *config) loadCryptoKey() (*crypto.Key, error) {
|
||||||
return crypto.NewKeyFromArmoredReader(f)
|
return crypto.NewKeyFromArmoredReader(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkPassword compares the given hashed password with the plaintext in the "password" config value.
|
||||||
|
// It returns true if these matches or if the "password" config value is not set, otherwise false.
|
||||||
func (cfg *config) checkPassword(hash string) bool {
|
func (cfg *config) checkPassword(hash string) bool {
|
||||||
return cfg.Password == nil ||
|
return cfg.Password == nil ||
|
||||||
bcrypt.CompareHashAndPassword([]byte(hash), []byte(*cfg.Password)) == nil
|
bcrypt.CompareHashAndPassword([]byte(hash), []byte(*cfg.Password)) == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadConfig extracts the config values from the config file. The path to the
|
||||||
|
// file is taken either from environment variable "CSAF_CONFIG" or from the
|
||||||
|
// defined default path in "defaultConfigPath".
|
||||||
|
// Default values are set in case some are missing in the file.
|
||||||
|
// It returns these values in a struct and nil if there is no error.
|
||||||
func loadConfig() (*config, error) {
|
func loadConfig() (*config, error) {
|
||||||
path := os.Getenv(configEnv)
|
path := os.Getenv(configEnv)
|
||||||
if path == "" {
|
if path == "" {
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,14 @@ func asMultiError(err error) multiError {
|
||||||
return multiError([]string{err.Error()})
|
return multiError([]string{err.Error()})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// controller contains the config values and the html templates.
|
||||||
type controller struct {
|
type controller struct {
|
||||||
cfg *config
|
cfg *config
|
||||||
tmpl *template.Template
|
tmpl *template.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newController assigns the given configs to a controller variable and parses the html template
|
||||||
|
// if the config value "NoWebUI" is true. It returns the controller variable and nil, otherwise error.
|
||||||
func newController(cfg *config) (*controller, error) {
|
func newController(cfg *config) (*controller, error) {
|
||||||
|
|
||||||
c := controller{cfg: cfg}
|
c := controller{cfg: cfg}
|
||||||
|
|
@ -56,6 +59,8 @@ func newController(cfg *config) (*controller, error) {
|
||||||
return &c, nil
|
return &c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bind binds the paths with the corresponding http.handler and wraps it with the respective middleware,
|
||||||
|
// according to the "NoWebUI" config value.
|
||||||
func (c *controller) bind(pim *pathInfoMux) {
|
func (c *controller) bind(pim *pathInfoMux) {
|
||||||
if !c.cfg.NoWebUI {
|
if !c.cfg.NoWebUI {
|
||||||
pim.handleFunc("/", c.auth(c.index))
|
pim.handleFunc("/", c.auth(c.index))
|
||||||
|
|
@ -66,6 +71,9 @@ func (c *controller) bind(pim *pathInfoMux) {
|
||||||
pim.handleFunc("/api/create", c.auth(api(c.create)))
|
pim.handleFunc("/api/create", c.auth(api(c.create)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// auth wraps the given http.HandlerFunc and returns an new one after authenticating the
|
||||||
|
// password contained in the header "X-CSAF-PROVIDER-AUTH" with the "password" config value
|
||||||
|
// if set, otherwise returns the given http.HandlerFunc.
|
||||||
func (c *controller) auth(
|
func (c *controller) auth(
|
||||||
fn func(http.ResponseWriter, *http.Request),
|
fn func(http.ResponseWriter, *http.Request),
|
||||||
) func(http.ResponseWriter, *http.Request) {
|
) func(http.ResponseWriter, *http.Request) {
|
||||||
|
|
@ -83,6 +91,9 @@ func (c *controller) auth(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// render sets the headers for the response. It applies the given template "tmpl" to
|
||||||
|
// the given object "arg" and writes the output to http.ResponseWriter.
|
||||||
|
// It logs a warning in case of error.
|
||||||
func (c *controller) render(rw http.ResponseWriter, tmpl string, arg interface{}) {
|
func (c *controller) render(rw http.ResponseWriter, tmpl string, arg interface{}) {
|
||||||
rw.Header().Set("Content-type", "text/html; charset=utf-8")
|
rw.Header().Set("Content-type", "text/html; charset=utf-8")
|
||||||
rw.Header().Set("X-Content-Type-Options", "nosniff")
|
rw.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
|
|
@ -91,17 +102,24 @@ func (c *controller) render(rw http.ResponseWriter, tmpl string, arg interface{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// failed constructs the error messages by calling "asMultiError" and calls "render"
|
||||||
|
// function to render the passed template and error object.
|
||||||
func (c *controller) failed(rw http.ResponseWriter, tmpl string, err error) {
|
func (c *controller) failed(rw http.ResponseWriter, tmpl string, err error) {
|
||||||
result := map[string]interface{}{"Error": asMultiError(err)}
|
result := map[string]interface{}{"Error": asMultiError(err)}
|
||||||
c.render(rw, tmpl, result)
|
c.render(rw, tmpl, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// index calls the "render" function and passes the "index.html" and c.cfg to it.
|
||||||
func (c *controller) index(rw http.ResponseWriter, r *http.Request) {
|
func (c *controller) index(rw http.ResponseWriter, r *http.Request) {
|
||||||
c.render(rw, "index.html", map[string]interface{}{
|
c.render(rw, "index.html", map[string]interface{}{
|
||||||
"Config": c.cfg,
|
"Config": c.cfg,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// web executes the given function "fn", calls the "render" function and passes
|
||||||
|
// the result content from "fn", the given template and the http.ResponseWriter to it
|
||||||
|
// in case of no error occurred, otherwise calls the "failed" function and passes the given
|
||||||
|
// template and the error from "fn".
|
||||||
func (c *controller) web(
|
func (c *controller) web(
|
||||||
fn func(*http.Request) (interface{}, error),
|
fn func(*http.Request) (interface{}, error),
|
||||||
tmpl string,
|
tmpl string,
|
||||||
|
|
@ -116,6 +134,8 @@ func (c *controller) web(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// writeJSON sets the header for the response and writes the JSON encoding of the given "content".
|
||||||
|
// It logs out an error message in case of an error.
|
||||||
func writeJSON(rw http.ResponseWriter, content interface{}, code int) {
|
func writeJSON(rw http.ResponseWriter, content interface{}, code int) {
|
||||||
rw.Header().Set("Content-type", "application/json; charset=utf-8")
|
rw.Header().Set("Content-type", "application/json; charset=utf-8")
|
||||||
rw.Header().Set("X-Content-Type-Options", "nosniff")
|
rw.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ import (
|
||||||
"github.com/csaf-poc/csaf_distribution/util"
|
"github.com/csaf-poc/csaf_distribution/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ensureFolders initializes the paths and call functions to create
|
||||||
|
// the directories and files.
|
||||||
func ensureFolders(c *config) error {
|
func ensureFolders(c *config) error {
|
||||||
|
|
||||||
wellknown := filepath.Join(c.Web, ".well-known")
|
wellknown := filepath.Join(c.Web, ".well-known")
|
||||||
|
|
@ -38,6 +40,8 @@ func ensureFolders(c *config) error {
|
||||||
return createSecurity(c, wellknown)
|
return createSecurity(c, wellknown)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createWellknown creates ".well-known" directory if not exist and returns nil.
|
||||||
|
// An error is returned if the it is not a directory.
|
||||||
func createWellknown(wellknown string) error {
|
func createWellknown(wellknown string) error {
|
||||||
st, err := os.Stat(wellknown)
|
st, err := os.Stat(wellknown)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -52,6 +56,10 @@ func createWellknown(wellknown string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createFeedFolders creates the feed folders according to the tlp values
|
||||||
|
// in the "tlps" config option if they do not already exist.
|
||||||
|
// No creation for the "csaf" option will be done.
|
||||||
|
// It creates also symbolic links to feed folders.
|
||||||
func createFeedFolders(c *config, wellknown string) error {
|
func createFeedFolders(c *config, wellknown string) error {
|
||||||
for _, t := range c.TLPs {
|
for _, t := range c.TLPs {
|
||||||
if t == tlpCSAF {
|
if t == tlpCSAF {
|
||||||
|
|
@ -75,6 +83,8 @@ func createFeedFolders(c *config, wellknown string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createSecurity creats the "security.txt" file if does not exist
|
||||||
|
// and writes the CSAF field inside the file.
|
||||||
func createSecurity(c *config, wellknown string) error {
|
func createSecurity(c *config, wellknown string) error {
|
||||||
security := filepath.Join(wellknown, "security.txt")
|
security := filepath.Join(wellknown, "security.txt")
|
||||||
if _, err := os.Stat(security); err != nil {
|
if _, err := os.Stat(security); err != nil {
|
||||||
|
|
@ -93,6 +103,7 @@ func createSecurity(c *config, wellknown string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createProviderMetadata creates the provider-metadata.json file if does not exist.
|
||||||
func createProviderMetadata(c *config, wellknownCSAF string) error {
|
func createProviderMetadata(c *config, wellknownCSAF string) error {
|
||||||
path := filepath.Join(wellknownCSAF, "provider-metadata.json")
|
path := filepath.Join(wellknownCSAF, "provider-metadata.json")
|
||||||
_, err := os.Stat(path)
|
_, err := os.Stat(path)
|
||||||
|
|
|
||||||
|
|
@ -121,9 +121,15 @@ func (cs *compiledSchema) validate(doc interface{}) ([]string, error) {
|
||||||
res := make([]string, 0, len(errs))
|
res := make([]string, 0, len(errs))
|
||||||
|
|
||||||
for i := range errs {
|
for i := range errs {
|
||||||
if e := &errs[i]; e.InstanceLocation != "" && e.Error != "" {
|
e := &errs[i]
|
||||||
res = append(res, e.InstanceLocation+": "+e.Error)
|
if e.Error == "" {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
loc := e.InstanceLocation
|
||||||
|
if loc == "" {
|
||||||
|
loc = e.AbsoluteKeywordLocation
|
||||||
|
}
|
||||||
|
res = append(res, loc+": "+e.Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
|
|
|
||||||
72
docs/install-server-certificate.md
Normal file
72
docs/install-server-certificate.md
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
# Configure TLS Certificate for HTTPS
|
||||||
|
|
||||||
|
## Get a webserver TLS certificate
|
||||||
|
|
||||||
|
There are three ways to get a TLS certificate for your HTTPS server:
|
||||||
|
1. Get it from a certificate provider who will run a certificate
|
||||||
|
authority (CA) and also offers
|
||||||
|
[extended validation](https://en.wikipedia.org/wiki/Extended_Validation_Certificate) (EV)
|
||||||
|
for the certificate. This will cost a fee.
|
||||||
|
If possible, create the private key yourself,
|
||||||
|
then send a Certificate Signing Request (CSR).
|
||||||
|
Overall follow the documentation of the CA operator.
|
||||||
|
2. Get a domain validated TLS certificate via
|
||||||
|
[Let's encrypt](https://letsencrypt.org/) without a fee.
|
||||||
|
See their instruction, e.g.
|
||||||
|
[certbot for nignx on Ubuntu](https://certbot.eff.org/instructions?ws=nginx&os=ubuntufocal).
|
||||||
|
3. Run your own little CA. Which has the major drawback that someone
|
||||||
|
will have to import the root certificate in the webbrowsers manually.
|
||||||
|
Suitable for development purposes.
|
||||||
|
|
||||||
|
To decide between 1. and 2. you will need to weight the extra
|
||||||
|
efforts and costs of the level of extended validation against
|
||||||
|
a bit of extra trust for the security advisories
|
||||||
|
that will be served under the domain.
|
||||||
|
|
||||||
|
|
||||||
|
## Install the files for ngnix
|
||||||
|
|
||||||
|
Place the certificates on the server machine.
|
||||||
|
This includes the certificate for your webserver, the intermediate
|
||||||
|
certificates and the root certificate. The latter may already be on your
|
||||||
|
machine as part of the trust anchors for webbrowsers.
|
||||||
|
|
||||||
|
Follow the [nginx documentation](https://docs.nginx.com/nginx/admin-guide/security-controls/terminating-ssl-http/)
|
||||||
|
to further configure TLS with your private key and the certificates.
|
||||||
|
|
||||||
|
We recommend to
|
||||||
|
* restrict the TLS protocol version and ciphers following a current
|
||||||
|
recommendation (e.g. [BSI-TR-02102-2](https://www.bsi.bund.de/SharedDocs/Downloads/EN/BSI/Publications/TechGuidelines/TG02102/BSI-TR-02102-2.html)).
|
||||||
|
|
||||||
|
|
||||||
|
### Example configuration
|
||||||
|
|
||||||
|
Assuming the relevant server block is in `/etc/nginx/sites-enabled/default`,
|
||||||
|
change the `listen` configuration and add options so nginx
|
||||||
|
finds your your private key and the certificate chain.
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2 default_server; # ipv4
|
||||||
|
listen [::]:443 ssl http2 default_server; # ipv6
|
||||||
|
server_name www.example.com
|
||||||
|
|
||||||
|
ssl_certificate /etc/ssl/{domainName}.pem; # or bundle.crt
|
||||||
|
ssl_certificate_key /etc/ssl/{domainName}.key";
|
||||||
|
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
# Other Config
|
||||||
|
# ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `{domainName}` with the name for your certificate in the example.
|
||||||
|
|
||||||
|
Reload or restart nginx to apply the changes (e.g. `systemctl reload nginx`
|
||||||
|
on Debian or Ubuntu.)
|
||||||
|
|
||||||
|
Technical hints:
|
||||||
|
* When allowing or requiring `TLSv1.3` webbrowsers like
|
||||||
|
Chromium (seen with version 98) may have higher requirements
|
||||||
|
on the server certificates they allow,
|
||||||
|
otherwise they do not connect with `ERR_SSL_KEY_USAGE_INCOMPATIBLE`.
|
||||||
|
|
@ -7,7 +7,7 @@ The following instructions are for an Debian 11 server setup.
|
||||||
```(shell)
|
```(shell)
|
||||||
apt-get install nginx fcgiwrap
|
apt-get install nginx fcgiwrap
|
||||||
cp /usr/share/doc/fcgiwrap/examples/nginx.conf /etc/nginx/fcgiwrap.conf
|
cp /usr/share/doc/fcgiwrap/examples/nginx.conf /etc/nginx/fcgiwrap.conf
|
||||||
systemctl status fcgiwrap.servic
|
systemctl status fcgiwrap.service
|
||||||
systemctl status fcgiwrap.socket
|
systemctl status fcgiwrap.socket
|
||||||
systemctl is-enabled fcgiwrap.service
|
systemctl is-enabled fcgiwrap.service
|
||||||
systemctl is-enabled fcgiwrap.socket
|
systemctl is-enabled fcgiwrap.socket
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue