1
0
Fork 0
mirror of https://github.com/gocsaf/csaf.git synced 2025-12-22 05:40:11 +01:00

Merge branch 'main' into client-certificate

This commit is contained in:
Bernhard Reiter 2022-03-23 20:12:36 +01:00
commit 3753f08370
No known key found for this signature in database
GPG key ID: 2B7BA3BF9BC3A554
18 changed files with 537 additions and 168 deletions

View file

@ -29,3 +29,6 @@ jobs:
- name: golint - name: golint
uses: Jerome1337/golint-action@v1.0.2 uses: Jerome1337/golint-action@v1.0.2
- name: Tests
run: go test -v ./...

View file

@ -2,15 +2,13 @@
**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:
Makefile supplies the following targets:
- Build For GNU/Linux System: `make build_linux` - Build For GNU/Linux System: `make build_linux`
- Build For Windows System (cross build): `make build_win` - Build For Windows System (cross build): `make build_win`
- Build For both linux and windows: `make build` - Build For both linux and windows: `make build`
@ -27,6 +25,7 @@ Binaries will be placed in directories named like `bin-linux-amd64/` and `bin-wi
- To configure nginx for client certificate authentication see [docs/client-certificate-setup.md](docs/client-certificate-setup.md) - To configure nginx for client certificate authentication see [docs/client-certificate-setup.md](docs/client-certificate-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:
@ -35,13 +34,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 | CSAF 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
@ -71,6 +71,12 @@ action=create
u=http://localhost/cgi-bin/csaf_provider.go u=http://localhost/cgi-bin/csaf_provider.go
``` ```
## csaf_checker
Provider checker is a tool for testing a CSAF trusted provider according to [Section 7 of the CSAF standard](https://docs.oasis-open.org/csaf/csaf/v2.0/csaf-v2.0.html#7-distributing-csaf-documents).
Usage example:
``` ./csaf_checker example.com -f html -o check-results.html```
## License ## License
- csaf_distribution is licensed as Free Software under MIT License. - csaf_distribution is licensed as Free Software under MIT License.

View file

@ -38,6 +38,8 @@ func errCheck(err error) {
} }
} }
// writeJSON writes the JSON encoding of the given report to the given stream.
// It returns nil, otherwise an error.
func writeJSON(report *Report, w io.WriteCloser) error { func writeJSON(report *Report, w io.WriteCloser) error {
enc := json.NewEncoder(w) enc := json.NewEncoder(w)
enc.SetIndent("", " ") enc.SetIndent("", " ")
@ -48,6 +50,8 @@ func writeJSON(report *Report, w io.WriteCloser) error {
return err return err
} }
// writeHTML writes the given report to the given writer, it uses the template
// in the "reportHTML" variable. It returns nil, otherwise an error.
func writeHTML(report *Report, w io.WriteCloser) error { func writeHTML(report *Report, w io.WriteCloser) error {
tmpl, err := template.New("Report HTML").Parse(reportHTML) tmpl, err := template.New("Report HTML").Parse(reportHTML)
if err != nil { if err != nil {
@ -72,6 +76,8 @@ type nopCloser struct{ io.Writer }
func (nc *nopCloser) Close() error { return nil } func (nc *nopCloser) Close() error { return nil }
// writeReport defines where to write the report according to the "output" flag option.
// It calls also the "writeJSON" or "writeHTML" function according to the "format" flag option.
func writeReport(report *Report, opts *options) error { func writeReport(report *Report, opts *options) error {
var w io.WriteCloser var w io.WriteCloser
@ -98,6 +104,8 @@ func writeReport(report *Report, opts *options) error {
return writer(report, w) return writer(report, w)
} }
// buildReporters initializes each report by assigning a number and description to it.
// It returns an array of the reporter interface type.
func buildReporters() []reporter { func buildReporters() []reporter {
return []reporter{ return []reporter{
&tlsReporter{baseReporter{num: 3, description: "TLS"}}, &tlsReporter{baseReporter{num: 3, description: "TLS"}},
@ -112,7 +120,7 @@ func buildReporters() []reporter {
&directoryListingsReporter{baseReporter{num: 14, description: "Directory listings"}}, &directoryListingsReporter{baseReporter{num: 14, description: "Directory listings"}},
&integrityReporter{baseReporter{num: 18, description: "Integrity"}}, &integrityReporter{baseReporter{num: 18, description: "Integrity"}},
&signaturesReporter{baseReporter{num: 19, description: "Signatures"}}, &signaturesReporter{baseReporter{num: 19, description: "Signatures"}},
&publicPGPKeyReporter{baseReporter{num: 20, description: "Public PGP Key"}}, &publicPGPKeyReporter{baseReporter{num: 20, description: "Public OpenPGP Key"}},
} }
} }

View file

@ -11,7 +11,6 @@ package main
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"context"
"crypto/sha256" "crypto/sha256"
"crypto/sha512" "crypto/sha512"
"crypto/tls" "crypto/tls"
@ -20,6 +19,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log"
"net/http" "net/http"
"net/url" "net/url"
"regexp" "regexp"
@ -28,8 +28,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/PaesslerAG/gval"
"github.com/PaesslerAG/jsonpath"
"github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/csaf-poc/csaf_distribution/csaf" "github.com/csaf-poc/csaf_distribution/csaf"
@ -43,6 +41,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
@ -56,10 +55,12 @@ type processor struct {
badChanges []string badChanges []string
badFolders []string badFolders []string
builder gval.Language expr *util.PathEval
exprs map[string]gval.Evaluable
} }
// reporter is implemented by any value that has a report method.
// The implementation of the report controls how to test
// the respective requirement and generate the report.
type reporter interface { type reporter interface {
report(*processor, *Domain) report(*processor, *Domain)
} }
@ -104,21 +105,24 @@ func (wt whereType) String() string {
} }
} }
// newProcessor returns a processor structure after assigning the given options to the opts attribute
// and initializing the "alreadyChecked" and "expr" fields.
func newProcessor(opts *options) *processor { func newProcessor(opts *options) *processor {
return &processor{ return &processor{
opts: opts, opts: opts,
alreadyChecked: map[string]whereType{}, alreadyChecked: map[string]whereType{},
builder: gval.Full(jsonpath.Language()), expr: util.NewPathEval(),
exprs: map[string]gval.Evaluable{},
} }
} }
// clean clears the fields values of the given processor.
func (p *processor) clean() { func (p *processor) clean() {
p.redirects = nil p.redirects = nil
p.noneTLS = nil p.noneTLS = nil
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 +135,10 @@ func (p *processor) clean() {
p.badIndices = nil p.badIndices = nil
p.badChanges = nil p.badChanges = nil
} }
// run calls checkDomain function for each domain in the given "domains" parameter.
// Then it calls the report method on each report from the given "reporters" paramerter for each domain.
// It returns a pointer to the report and nil, otherwise an error.
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
@ -174,21 +182,8 @@ func (p *processor) checkDomain(domain string) error {
return nil return nil
} }
func (p *processor) jsonPath(expr string, doc interface{}) (interface{}, error) { // checkTLS parses the given URL to check its schema, as a result it sets
if doc == nil { // the value of "noneTLS" field if it is not HTTPS.
return nil, errors.New("no document to extract data from")
}
eval := p.exprs[expr]
if eval == nil {
var err error
if eval, err = p.builder.NewEvaluable(expr); err != nil {
return nil, err
}
p.exprs[expr] = eval
}
return eval(context.Background(), doc)
}
func (p *processor) checkTLS(u string) { func (p *processor) checkTLS(u string) {
if p.noneTLS == nil { if p.noneTLS == nil {
p.noneTLS = map[string]struct{}{} p.noneTLS = map[string]struct{}{}
@ -247,6 +242,7 @@ func (p *processor) httpClient() *http.Client {
return p.client return p.client
} }
// use checks the given array and initializes an empty array if its nil.
func use(s *[]string) { func use(s *[]string) {
if *s == nil { if *s == nil {
*s = []string{} *s = []string{}
@ -257,34 +253,50 @@ func used(s []string) bool {
return s != nil return s != nil
} }
// badIntegrity appends a message to the value of "badIntegrity" field of
// the "processor" struct according to the given format and parameters.
func (p *processor) badIntegrity(format string, args ...interface{}) { func (p *processor) badIntegrity(format string, args ...interface{}) {
p.badIntegrities = append(p.badIntegrities, fmt.Sprintf(format, args...)) p.badIntegrities = append(p.badIntegrities, fmt.Sprintf(format, args...))
} }
// badSignature appends a message to the value of "badSignature" field of
// the "processor" struct according to the given format and parameters.
func (p *processor) badSignature(format string, args ...interface{}) { func (p *processor) badSignature(format string, args ...interface{}) {
p.badSignatures = append(p.badSignatures, fmt.Sprintf(format, args...)) p.badSignatures = append(p.badSignatures, fmt.Sprintf(format, args...))
} }
// badProviderMetadata appends a message to the value of "badProviderMetadatas" field of
// the "processor" struct according to the given format and parameters.
func (p *processor) badProviderMetadata(format string, args ...interface{}) { func (p *processor) badProviderMetadata(format string, args ...interface{}) {
p.badProviderMetadatas = append(p.badProviderMetadatas, fmt.Sprintf(format, args...)) p.badProviderMetadatas = append(p.badProviderMetadatas, fmt.Sprintf(format, args...))
} }
// badPGP appends a message to the value of "badPGPs" field of
// the "processor" struct according to the given format and parameters.
func (p *processor) badPGP(format string, args ...interface{}) { func (p *processor) badPGP(format string, args ...interface{}) {
p.badPGPs = append(p.badPGPs, fmt.Sprintf(format, args...)) p.badPGPs = append(p.badPGPs, fmt.Sprintf(format, args...))
} }
// badSecurity appends a message to the value of "badSecurity" field of
// the "processor" struct according to the given format and parameters.
func (p *processor) badSecurity(format string, args ...interface{}) { func (p *processor) badSecurity(format string, args ...interface{}) {
p.badSecurities = append(p.badSecurities, fmt.Sprintf(format, args...)) p.badSecurities = append(p.badSecurities, fmt.Sprintf(format, args...))
} }
// badIndex appends a message to the value of "badIndices" field of
// the "processor" struct according to the given format and parameters.
func (p *processor) badIndex(format string, args ...interface{}) { func (p *processor) badIndex(format string, args ...interface{}) {
p.badIndices = append(p.badIndices, fmt.Sprintf(format, args...)) p.badIndices = append(p.badIndices, fmt.Sprintf(format, args...))
} }
// badChange appends a message to the value of "badChanges" field of
// the "processor" struct according to the given format and parameters.
func (p *processor) badChange(format string, args ...interface{}) { func (p *processor) badChange(format string, args ...interface{}) {
p.badChanges = append(p.badChanges, fmt.Sprintf(format, args...)) p.badChanges = append(p.badChanges, fmt.Sprintf(format, args...))
} }
// badFolder appends a message to the value of "badFolders" field of
// the "processor" struct according to the given format and parameters.
func (p *processor) badFolder(format string, args ...interface{}) { func (p *processor) badFolder(format string, args ...interface{}) {
p.badFolders = append(p.badFolders, fmt.Sprintf(format, args...)) p.badFolders = append(p.badFolders, fmt.Sprintf(format, args...))
} }
@ -355,7 +367,7 @@ func (p *processor) integrity(
// Check if file is in the right folder. // Check if file is in the right folder.
use(&p.badFolders) use(&p.badFolders)
if date, err := p.jsonPath( if date, err := p.expr.Eval(
`$.document.tracking.initial_release_date`, doc); err != nil { `$.document.tracking.initial_release_date`, doc); err != nil {
p.badFolder( p.badFolder(
"Extracting 'initial_release_date' from %s failed: %v", u, err) "Extracting 'initial_release_date' from %s failed: %v", u, err)
@ -486,7 +498,7 @@ func (p *processor) processROLIEFeed(feed string) error {
// Extract the CSAF files from feed. // Extract the CSAF files from feed.
var files []string var files []string
for _, f := range rfeed.Entry { for _, f := range rfeed.Feed.Entry {
for i := range f.Link { for i := range f.Link {
files = append(files, f.Link[i].HRef) files = append(files, f.Link[i].HRef)
} }
@ -508,6 +520,9 @@ func (p *processor) processROLIEFeed(feed string) error {
return nil return nil
} }
// checkIndex fetches the "index.txt" and calls "checkTLS" method for HTTPS checks.
// It extracts the file names from the file and passes them to "integrity" function.
// It returns error if fetching/reading the file(s) fails, otherwise nil.
func (p *processor) checkIndex(base string, mask whereType) error { func (p *processor) checkIndex(base string, mask whereType) error {
client := p.httpClient() client := p.httpClient()
index := base + "/index.txt" index := base + "/index.txt"
@ -546,6 +561,10 @@ func (p *processor) checkIndex(base string, mask whereType) error {
return p.integrity(files, base, mask, p.badIndex) return p.integrity(files, base, mask, p.badIndex)
} }
// checkChanges fetches the "changes.csv" and calls the "checkTLS" method for HTTPs checks.
// It extracts the file content, tests the column number and the validity of the time format
// of the fields' values and if they are sorted properly. Then it passes the files to the
// "integrity" functions. It returns error if some test fails, otherwise nil.
func (p *processor) checkChanges(base string, mask whereType) error { func (p *processor) checkChanges(base string, mask whereType) error {
client := p.httpClient() client := p.httpClient()
changes := base + "/changes.csv" changes := base + "/changes.csv"
@ -607,7 +626,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
} }
@ -634,7 +653,7 @@ func (p *processor) processROLIEFeeds(domain string, feeds [][]csaf.Feed) error
func (p *processor) checkCSAFs(domain string) error { func (p *processor) checkCSAFs(domain string) error {
// Check for ROLIE // Check for ROLIE
rolie, err := p.jsonPath("$.distributions[*].rolie.feeds", p.pmd) rolie, err := p.expr.Eval("$.distributions[*].rolie.feeds", p.pmd)
if err != nil { if err != nil {
return err return err
} }
@ -654,7 +673,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,36 +723,121 @@ 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) {
use(&p.badProviderMetadatas)
res, err := client.Get(url) 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
}
if err := func() error {
defer res.Body.Close()
return found(url, res.Body)
}(); err != nil {
return false, err
}
return true, nil
}
for _, loc := range providerMetadataLocations {
url := "https://" + domain + "/" + loc
ok, err := tryURL(url)
if err != nil { if err != nil {
p.badProviderMetadata("Fetching %s: %v.", url, err) if err == errContinue {
return errStop continue
}
return err
}
if ok {
return nil
}
}
// Read from security.txt
path := "https://" + domain + "/.well-known/security.txt"
res, err := client.Get(path)
if err != nil {
return err
} }
if res.StatusCode != http.StatusOK { if res.StatusCode != http.StatusOK {
p.badProviderMetadata("Fetching %s failed. Status code: %d (%s)", return err
url, res.StatusCode, res.Status)
return errStop
} }
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("CSAF: found in security.txt, but does not start with https://")
}
return line, nil
}
}
if err := sc.Err(); err != nil {
return "", err
}
return "", nil
}
// checkProviderMetadata checks the provider-metatdata if exists, decodes,
// and validates against the JSON schema. According to the result the respective
// error messages are passed to the badProviderMetadatas method in case of errors.
// It returns nil if all checks are passed.
func (p *processor) checkProviderMetadata(domain string) error {
use(&p.badProviderMetadatas)
found := func(url string, content io.Reader) error {
// Calculate checksum for later comparison. // Calculate checksum for later comparison.
hash := sha256.New() hash := sha256.New()
if err := func() error { tee := io.TeeReader(content, hash)
defer res.Body.Close() if err := json.NewDecoder(tee).Decode(&p.pmd); err != nil {
tee := io.TeeReader(res.Body, hash) p.badProviderMetadata("%s: Decoding JSON failed: %v", url, err)
return json.NewDecoder(tee).Decode(&p.pmd) return errContinue
}(); err != nil {
p.badProviderMetadata("Decoding JSON failed: %v", err)
return errStop
} }
p.pmd256 = hash.Sum(nil) p.pmd256 = hash.Sum(nil)
@ -740,14 +847,32 @@ func (p *processor) checkProviderMetadata(domain string) error {
return err return err
} }
if len(errors) > 0 { if len(errors) > 0 {
p.badProviderMetadata("Validating against JSON schema failed:") p.badProviderMetadata("%s: Validating against JSON schema failed:", url)
for _, msg := range errors { for _, msg := range errors {
p.badProviderMetadata(strings.ReplaceAll(msg, `%`, `%%`)) 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
} }
// checkSecurity checks the security.txt file by making HTTP request to fetch it.
// It checks the existence of the CSAF field in the file content and tries to fetch
// the value of this field. As a result of these a respective error messages are
// passed to the badSecurity method in case of errors.
// It returns nil if all checks are passed.
func (p *processor) checkSecurity(domain string) error { func (p *processor) checkSecurity(domain string) error {
client := p.httpClient() client := p.httpClient()
@ -826,24 +951,28 @@ func (p *processor) checkSecurity(domain string) error {
return nil return nil
} }
// checkPGPKeys checks if the OpenPGP keys are available and valid, fetches
// the the remotely keys and compares the fingerprints.
// As a result of these a respective error messages are passed to badPGP method
// in case of errors. It returns nil if all checks are passed.
func (p *processor) checkPGPKeys(domain string) error { func (p *processor) checkPGPKeys(domain string) error {
use(&p.badPGPs) use(&p.badPGPs)
src, err := p.jsonPath("$.pgp_keys", p.pmd) src, err := p.expr.Eval("$.pgp_keys", p.pmd)
if err != nil { if err != nil {
p.badPGP("No PGP keys found: %v.", err) p.badPGP("No public OpenPGP keys found: %v.", err)
return errContinue return errContinue
} }
var keys []csaf.PGPKey var keys []csaf.PGPKey
if err := util.ReMarshalJSON(&keys, src); err != nil { if err := util.ReMarshalJSON(&keys, src); err != nil {
p.badPGP("PGP keys invalid: %v.", err) p.badPGP("Invalid public OpenPGP keys: %v.", err)
return errContinue return errContinue
} }
if len(keys) == 0 { if len(keys) == 0 {
p.badPGP("No PGP keys found.") p.badPGP("No public OpenPGP keys found.")
return errContinue return errContinue
} }
@ -851,7 +980,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
} }
@ -873,11 +1002,11 @@ func (p *processor) checkPGPKeys(domain string) error {
res, err := client.Get(u) res, err := client.Get(u)
if err != nil { if err != nil {
p.badPGP("Fetching PGP key %s failed: %v.", u, err) p.badPGP("Fetching public OpenPGP key %s failed: %v.", u, err)
continue continue
} }
if res.StatusCode != http.StatusOK { if res.StatusCode != http.StatusOK {
p.badPGP("Fetching PGP key %s status code: %d (%s)", p.badPGP("Fetching public OpenPGP key %s status code: %d (%s)",
u, res.StatusCode, res.Status) u, res.StatusCode, res.Status)
continue continue
} }
@ -888,24 +1017,24 @@ func (p *processor) checkPGPKeys(domain string) error {
}() }()
if err != nil { if err != nil {
p.badPGP("Reading PGP key %s failed: %v", u, err) p.badPGP("Reading public OpenPGP key %s failed: %v", u, err)
continue continue
} }
if ckey.GetFingerprint() != string(key.Fingerprint) { if ckey.GetFingerprint() != string(key.Fingerprint) {
p.badPGP("Fingerprint of PGP key %s do not match remotely loaded.", u) p.badPGP("Fingerprint of public OpenPGP key %s does not match remotely loaded.", u)
continue continue
} }
keyring, err := crypto.NewKeyRing(ckey) keyring, err := crypto.NewKeyRing(ckey)
if err != nil { if err != nil {
p.badPGP("Creating key ring for %s failed: %v.", u, err) p.badPGP("Creating store for public OpenPGP key %s failed: %v.", u, err)
continue continue
} }
p.keys = append(p.keys, keyring) p.keys = append(p.keys, keyring)
} }
if len(p.keys) == 0 { if len(p.keys) == 0 {
p.badPGP("No PGP keys loaded.") p.badPGP("No OpenPGP keys loaded.")
} }
return nil return nil
} }

View file

@ -3,8 +3,8 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// //
// SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de> // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
// Software-Engineering: 2021 Intevation GmbH <https://intevation.de> // Software-Engineering: 2022 Intevation GmbH <https://intevation.de>
package main package main
@ -42,6 +42,9 @@ func (bc *baseReporter) requirement(domain *Domain) *Requirement {
return req return req
} }
// report tests if the URLs are HTTPS and sets the "message" field value
// of the "Requirement" struct as a result of that.
// A list of non HTTPS URLs is included in the value of the "message" field.
func (r *tlsReporter) report(p *processor, domain *Domain) { func (r *tlsReporter) report(p *processor, domain *Domain) {
req := r.requirement(domain) req := r.requirement(domain)
if p.noneTLS == nil { if p.noneTLS == nil {
@ -49,7 +52,7 @@ func (r *tlsReporter) report(p *processor, domain *Domain) {
return return
} }
if len(p.noneTLS) == 0 { if len(p.noneTLS) == 0 {
req.message("All tested URLs were https.") req.message("All tested URLs were HTTPS.")
return return
} }
@ -60,10 +63,12 @@ func (r *tlsReporter) report(p *processor, domain *Domain) {
i++ i++
} }
sort.Strings(urls) sort.Strings(urls)
req.message("Following none https URLs were used:") req.message("Following non-HTTPS URLs were used:")
req.message(urls...) req.message(urls...)
} }
// report tests if redirects are used and sets the "message" field value
// of the "Requirement" struct as a result of that.
func (r *redirectsReporter) report(p *processor, domain *Domain) { func (r *redirectsReporter) report(p *processor, domain *Domain) {
req := r.requirement(domain) req := r.requirement(domain)
if len(p.redirects) == 0 { if len(p.redirects) == 0 {
@ -84,6 +89,8 @@ func (r *redirectsReporter) report(p *processor, domain *Domain) {
req.Messages = keys req.Messages = keys
} }
// report tests if an provider-metatdata.json are available and sets the
// "message" field value of the "Requirement" struct as a result of that.
func (r *providerMetadataReport) report(p *processor, domain *Domain) { func (r *providerMetadataReport) report(p *processor, domain *Domain) {
req := r.requirement(domain) req := r.requirement(domain)
if !used(p.badProviderMetadatas) { if !used(p.badProviderMetadatas) {
@ -91,12 +98,14 @@ func (r *providerMetadataReport) report(p *processor, domain *Domain) {
return return
} }
if len(p.badProviderMetadatas) == 0 { if len(p.badProviderMetadatas) == 0 {
req.message("No problems with provider metadata.") req.message("Found good provider metadata.")
return return
} }
req.Messages = p.badProviderMetadatas req.Messages = p.badProviderMetadatas
} }
// report tests the "security.txt" file and sets the "message" field value
// of the "Requirement" struct as a result of that.
func (r *securityReporter) report(p *processor, domain *Domain) { func (r *securityReporter) report(p *processor, domain *Domain) {
req := r.requirement(domain) req := r.requirement(domain)
if !used(p.badSecurities) { if !used(p.badSecurities) {
@ -104,7 +113,7 @@ func (r *securityReporter) report(p *processor, domain *Domain) {
return return
} }
if len(p.badSecurities) == 0 { if len(p.badSecurities) == 0 {
req.message("No problems with security.txt found.") req.message("Found good security.txt.")
return return
} }
req.Messages = p.badSecurities req.Messages = p.badSecurities
@ -113,19 +122,19 @@ func (r *securityReporter) report(p *processor, domain *Domain) {
func (r *wellknownMetadataReporter) report(_ *processor, domain *Domain) { func (r *wellknownMetadataReporter) report(_ *processor, domain *Domain) {
// TODO: Implement me! // TODO: Implement me!
req := r.requirement(domain) req := r.requirement(domain)
_ = req req.message("(Not checked, missing implementation.)")
} }
func (r *dnsPathReporter) report(_ *processor, domain *Domain) { func (r *dnsPathReporter) report(_ *processor, domain *Domain) {
// TODO: Implement me! // TODO: Implement me!
req := r.requirement(domain) req := r.requirement(domain)
_ = req req.message("(Not checked, missing implementation.)")
} }
func (r *oneFolderPerYearReport) report(p *processor, domain *Domain) { func (r *oneFolderPerYearReport) report(p *processor, domain *Domain) {
req := r.requirement(domain) req := r.requirement(domain)
if !used(p.badFolders) { if !used(p.badFolders) {
req.message("No checks if file are in right folders were performed.") req.message("No checks if files are in right folders were performed.")
return return
} }
if len(p.badFolders) == 0 { if len(p.badFolders) == 0 {
@ -142,7 +151,7 @@ func (r *indexReporter) report(p *processor, domain *Domain) {
return return
} }
if len(p.badIndices) == 0 { if len(p.badIndices) == 0 {
req.message("No problems with index.txt found.") req.message("Found good index.txt.")
return return
} }
req.Messages = p.badIndices req.Messages = p.badIndices
@ -155,7 +164,7 @@ func (r *changesReporter) report(p *processor, domain *Domain) {
return return
} }
if len(p.badChanges) == 0 { if len(p.badChanges) == 0 {
req.message("No problems with changes.csv found.") req.message("Found good changes.csv.")
return return
} }
req.Messages = p.badChanges req.Messages = p.badChanges
@ -195,11 +204,11 @@ func (r *signaturesReporter) report(p *processor, domain *Domain) {
func (r *publicPGPKeyReporter) report(p *processor, domain *Domain) { func (r *publicPGPKeyReporter) report(p *processor, domain *Domain) {
req := r.requirement(domain) req := r.requirement(domain)
if !used(p.badPGPs) { if !used(p.badPGPs) {
req.message("No PGP keys loaded.") req.message("No public OpenPGP keys loaded.")
return return
} }
req.Messages = p.badPGPs req.Messages = p.badPGPs
if len(p.keys) > 0 { if len(p.keys) > 0 {
req.message(fmt.Sprintf("%d PGP key(s) loaded successfully.", len(p.keys))) req.message(fmt.Sprintf("%d public OpenPGP key(s) loaded.", len(p.keys)))
} }
} }

View file

@ -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
@ -159,7 +167,7 @@ func (c *controller) upload(r *http.Request) (interface{}, error) {
} }
} }
ex, err := newExtraction(content) ex, err := csaf.NewAdvisorySummary(util.NewPathEval(), content)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -171,8 +179,9 @@ func (c *controller) upload(r *http.Request) (interface{}, error) {
// Extract real TLP from document. // Extract real TLP from document.
if t == tlpCSAF { if t == tlpCSAF {
if t = tlp(strings.ToLower(ex.tlpLabel)); !t.valid() || t == tlpCSAF { if t = tlp(strings.ToLower(ex.TLPLabel)); !t.valid() || t == tlpCSAF {
return nil, fmt.Errorf("not a valid TL: %s", ex.tlpLabel) return nil, fmt.Errorf(
"valid TLP label missing in document (found '%s')", t)
} }
} }
@ -217,31 +226,33 @@ func (c *controller) upload(r *http.Request) (interface{}, error) {
// Create new if does not exists. // Create new if does not exists.
if rolie == nil { if rolie == nil {
rolie = &csaf.ROLIEFeed{ rolie = &csaf.ROLIEFeed{
Feed: csaf.FeedData{
ID: "csaf-feed-tlp-" + ts, ID: "csaf-feed-tlp-" + ts,
Title: "CSAF feed (TLP:" + string(tlpLabel) + ")", Title: "CSAF feed (TLP:" + string(tlpLabel) + ")",
Link: []csaf.Link{{ Link: []csaf.Link{{
Rel: "rel", Rel: "rel",
HRef: string(feedURL), HRef: string(feedURL),
}}, }},
},
} }
} }
rolie.Updated = csaf.TimeStamp(time.Now()) rolie.Feed.Updated = csaf.TimeStamp(time.Now())
year := strconv.Itoa(ex.initialReleaseDate.Year()) year := strconv.Itoa(ex.InitialReleaseDate.Year())
csafURL := c.cfg.Domain + csafURL := c.cfg.Domain +
"/.well-known/csaf/" + ts + "/" + year + "/" + newCSAF "/.well-known/csaf/" + ts + "/" + year + "/" + newCSAF
e := rolie.EntryByID(ex.id) e := rolie.EntryByID(ex.ID)
if e == nil { if e == nil {
e = &csaf.Entry{ID: ex.id} e = &csaf.Entry{ID: ex.ID}
rolie.Entry = append(rolie.Entry, e) rolie.Feed.Entry = append(rolie.Feed.Entry, e)
} }
e.Titel = ex.title e.Titel = ex.Title
e.Published = csaf.TimeStamp(ex.initialReleaseDate) e.Published = csaf.TimeStamp(ex.InitialReleaseDate)
e.Updated = csaf.TimeStamp(ex.currentReleaseDate) e.Updated = csaf.TimeStamp(ex.CurrentReleaseDate)
e.Link = []csaf.Link{{ e.Link = []csaf.Link{{
Rel: "self", Rel: "self",
HRef: csafURL, HRef: csafURL,
@ -254,8 +265,8 @@ func (c *controller) upload(r *http.Request) (interface{}, error) {
Type: "application/json", Type: "application/json",
Src: csafURL, Src: csafURL,
} }
if ex.summary != "" { if ex.Summary != "" {
e.Summary = &csaf.Summary{Content: ex.summary} e.Summary = &csaf.Summary{Content: ex.Summary}
} else { } else {
e.Summary = nil e.Summary = nil
} }
@ -291,7 +302,7 @@ func (c *controller) upload(r *http.Request) (interface{}, error) {
if err := updateIndices( if err := updateIndices(
folder, filepath.Join(year, newCSAF), folder, filepath.Join(year, newCSAF),
ex.currentReleaseDate, ex.CurrentReleaseDate,
); err != nil { ); err != nil {
return err return err
} }
@ -302,9 +313,9 @@ func (c *controller) upload(r *http.Request) (interface{}, error) {
warn("Publisher in provider metadata is not initialized. Forgot to configure?") warn("Publisher in provider metadata is not initialized. Forgot to configure?")
if c.cfg.DynamicProviderMetaData { if c.cfg.DynamicProviderMetaData {
warn("Taking publisher from CSAF") warn("Taking publisher from CSAF")
pmd.Publisher = ex.publisher pmd.Publisher = ex.Publisher
} }
case !pmd.Publisher.Equals(ex.publisher): case !pmd.Publisher.Equals(ex.Publisher):
warn("Publishers in provider metadata and CSAF do not match.") warn("Publishers in provider metadata and CSAF do not match.")
} }
@ -323,7 +334,7 @@ func (c *controller) upload(r *http.Request) (interface{}, error) {
Error error `json:"-"` Error error `json:"-"`
}{ }{
Name: newCSAF, Name: newCSAF,
ReleaseDate: ex.currentReleaseDate.Format(dateFormat), ReleaseDate: ex.CurrentReleaseDate.Format(dateFormat),
Warnings: warnings, Warnings: warnings,
} }

View file

@ -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/csaf/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"`
@ -57,6 +59,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:
@ -74,6 +77,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 {
@ -101,6 +106,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 {
@ -110,11 +117,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 == "" {

View file

@ -38,11 +38,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}
@ -57,6 +60,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))
@ -67,6 +72,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) {
@ -93,6 +101,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")
@ -101,17 +112,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,
@ -126,6 +144,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")

View file

@ -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)

View file

@ -3,10 +3,10 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// //
// SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de> // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
// Software-Engineering: 2021 Intevation GmbH <https://intevation.de> // Software-Engineering: 2022 Intevation GmbH <https://intevation.de>
// Implemnts a command line tool that loads csaf documents to a trusted provider // Implements a command line tool that uploads csaf documents to csaf_provider.
package main package main
import ( import (
@ -34,7 +34,7 @@ type options struct {
Action string `short:"a" long:"action" choice:"upload" choice:"create" default:"upload" description:"Action to perform"` Action string `short:"a" long:"action" choice:"upload" choice:"create" default:"upload" description:"Action to perform"`
URL string `short:"u" long:"url" description:"URL of the CSAF provider" default:"https://localhost/cgi-bin/csaf_provider.go" value-name:"URL"` URL string `short:"u" long:"url" description:"URL of the CSAF provider" default:"https://localhost/cgi-bin/csaf_provider.go" value-name:"URL"`
TLP string `short:"t" long:"tlp" choice:"csaf" choice:"white" choice:"green" choice:"amber" choice:"red" default:"csaf" description:"TLP of the feed"` TLP string `short:"t" long:"tlp" choice:"csaf" choice:"white" choice:"green" choice:"amber" choice:"red" default:"csaf" description:"TLP of the feed"`
ExternalSigned bool `short:"x" long:"external-signed" description:"CASF files are signed externally. Assumes .asc files beside CSAF files."` 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."` 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"` Key *string `short:"k" long:"key" description:"OpenPGP key to sign the CSAF files" value-name:"KEY-FILE"`

View file

@ -3,8 +3,8 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// //
// SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de> // SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
// Software-Engineering: 2021 Intevation GmbH <https://intevation.de> // Software-Engineering: 2022 Intevation GmbH <https://intevation.de>
package csaf package csaf
@ -72,13 +72,13 @@ type Distribution struct {
type TimeStamp time.Time type TimeStamp time.Time
// Fingerprint is the fingerprint of a OpenPGP key used to sign // Fingerprint is the fingerprint of a OpenPGP key used to sign
// the CASF documents. // the CSAF documents.
type Fingerprint string type Fingerprint string
var fingerprintPattern = patternUnmarshal(`^[0-9a-fA-F]{40,}$`) var fingerprintPattern = patternUnmarshal(`^[0-9a-fA-F]{40,}$`)
// PGPKey is location and the fingerprint of the key // PGPKey is location and the fingerprint of the key
// used to sign the CASF documents. // used to sign the CSAF documents.
type PGPKey struct { type PGPKey struct {
Fingerprint Fingerprint `json:"fingerprint,omitempty"` Fingerprint Fingerprint `json:"fingerprint,omitempty"`
URL *string `json:"url"` // required URL *string `json:"url"` // required

View file

@ -58,8 +58,8 @@ type Entry struct {
Format Format `json:"format"` Format Format `json:"format"`
} }
// ROLIEFeed is a ROLIE feed. // FeedData is the content of the ROLIE feed.
type ROLIEFeed struct { type FeedData struct {
ID string `json:"id"` ID string `json:"id"`
Title string `json:"title"` Title string `json:"title"`
Link []Link `json:"link,omitempty"` Link []Link `json:"link,omitempty"`
@ -68,6 +68,11 @@ type ROLIEFeed struct {
Entry []*Entry `json:"entry,omitempty"` Entry []*Entry `json:"entry,omitempty"`
} }
// ROLIEFeed is a ROLIE feed.
type ROLIEFeed struct {
Feed FeedData `json:"feed"`
}
// LoadROLIEFeed loads a ROLIE feed from a reader. // LoadROLIEFeed loads a ROLIE feed from a reader.
func LoadROLIEFeed(r io.Reader) (*ROLIEFeed, error) { func LoadROLIEFeed(r io.Reader) (*ROLIEFeed, error) {
dec := json.NewDecoder(r) dec := json.NewDecoder(r)
@ -90,7 +95,7 @@ func (rf *ROLIEFeed) WriteTo(w io.Writer) (int64, error) {
// EntryByID looks up an entry by its ID. // EntryByID looks up an entry by its ID.
// Returns nil if no such entry was found. // Returns nil if no such entry was found.
func (rf *ROLIEFeed) EntryByID(id string) *Entry { func (rf *ROLIEFeed) EntryByID(id string) *Entry {
for _, entry := range rf.Entry { for _, entry := range rf.Feed.Entry {
if entry.ID == id { if entry.ID == id {
return entry return entry
} }
@ -101,7 +106,7 @@ func (rf *ROLIEFeed) EntryByID(id string) *Entry {
// SortEntriesByUpdated sorts all the entries in the feed // SortEntriesByUpdated sorts all the entries in the feed
// by their update times. // by their update times.
func (rf *ROLIEFeed) SortEntriesByUpdated() { func (rf *ROLIEFeed) SortEntriesByUpdated() {
entries := rf.Entry entries := rf.Feed.Entry
sort.Slice(entries, func(i, j int) bool { sort.Slice(entries, func(i, j int) bool {
return time.Time(entries[j].Updated).Before(time.Time(entries[i].Updated)) return time.Time(entries[j].Updated).Before(time.Time(entries[i].Updated))
}) })

View file

@ -6,17 +6,12 @@
// SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de> // SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
// Software-Engineering: 2021 Intevation GmbH <https://intevation.de> // Software-Engineering: 2021 Intevation GmbH <https://intevation.de>
package main package csaf
import ( import (
"context"
"errors" "errors"
"time" "time"
"github.com/PaesslerAG/gval"
"github.com/PaesslerAG/jsonpath"
"github.com/csaf-poc/csaf_distribution/csaf"
"github.com/csaf-poc/csaf_distribution/util" "github.com/csaf-poc/csaf_distribution/util"
) )
@ -30,39 +25,39 @@ const (
summaryExpr = `$.document.notes[? @.category=="summary" || @.type=="summary"].text` summaryExpr = `$.document.notes[? @.category=="summary" || @.type=="summary"].text`
) )
type extraction struct { // AdvisorySummary is a summary of some essentials of an CSAF advisory.
id string type AdvisorySummary struct {
title string ID string
publisher *csaf.Publisher Title string
initialReleaseDate time.Time Publisher *Publisher
currentReleaseDate time.Time InitialReleaseDate time.Time
summary string CurrentReleaseDate time.Time
tlpLabel string Summary string
TLPLabel string
} }
type extractFunc func(string) (interface{}, error) type extractFunc func(string) (interface{}, error)
func newExtraction(content interface{}) (*extraction, error) { // NewAdvisorySummary creates a summary from an advisory doc
// with the help of an expression evaluator expr.
func NewAdvisorySummary(
expr *util.PathEval,
doc interface{},
) (*AdvisorySummary, error) {
builder := gval.Full(jsonpath.Language()) e := new(AdvisorySummary)
path := func(expr string) (interface{}, error) { path := func(s string) (interface{}, error) {
eval, err := builder.NewEvaluable(expr) return expr.Eval(s, doc)
if err != nil {
return nil, err
} }
return eval(context.Background(), content)
}
e := new(extraction)
for _, fn := range []func(extractFunc) error{ for _, fn := range []func(extractFunc) error{
extractText(idExpr, &e.id), extractText(idExpr, &e.ID),
extractText(titleExpr, &e.title), extractText(titleExpr, &e.Title),
extractTime(currentReleaseDateExpr, &e.currentReleaseDate), extractTime(currentReleaseDateExpr, &e.CurrentReleaseDate),
extractTime(initialReleaseDateExpr, &e.initialReleaseDate), extractTime(initialReleaseDateExpr, &e.InitialReleaseDate),
extractText(summaryExpr, &e.summary), extractText(summaryExpr, &e.Summary),
extractText(tlpLabelExpr, &e.tlpLabel), extractText(tlpLabelExpr, &e.TLPLabel),
e.extractPublisher, e.extractPublisher,
} { } {
if err := fn(path); err != nil { if err := fn(path); err != nil {
@ -95,7 +90,7 @@ func extractTime(expr string, store *time.Time) func(extractFunc) error {
if !ok { if !ok {
return errors.New("not a string") return errors.New("not a string")
} }
date, err := time.Parse(dateFormat, text) date, err := time.Parse(time.RFC3339, text)
if err == nil { if err == nil {
*store = date.UTC() *store = date.UTC()
} }
@ -103,7 +98,7 @@ func extractTime(expr string, store *time.Time) func(extractFunc) error {
} }
} }
func (e *extraction) extractPublisher(path extractFunc) error { func (e *AdvisorySummary) extractPublisher(path extractFunc) error {
p, err := path(publisherExpr) p, err := path(publisherExpr)
if err != nil { if err != nil {
return err return err
@ -111,13 +106,13 @@ func (e *extraction) extractPublisher(path extractFunc) error {
// XXX: It's a bit cumbersome to serialize and deserialize // XXX: It's a bit cumbersome to serialize and deserialize
// it into our own structure. // it into our own structure.
publisher := new(csaf.Publisher) publisher := new(Publisher)
if err := util.ReMarshalJSON(publisher, p); err != nil { if err := util.ReMarshalJSON(publisher, p); err != nil {
return err return err
} }
if err := publisher.Validate(); err != nil { if err := publisher.Validate(); err != nil {
return err return err
} }
e.publisher = publisher e.Publisher = publisher
return nil return nil
} }

87
docs/development-ca.md Normal file
View file

@ -0,0 +1,87 @@
# Certificate Authority for development purposes
A bare bones development certificate authority (CA) can be set up
to create certs for serving TLS connections.
Install GnuTLS, E.g. with `apt install gnutls-bin` (3.7.1-5) on Debian Bullseye.
All the private keys will be created without password protection,
which is suitable for testing in development setups.
## create root CA
```bash
mkdir devca1
cd devca1
certtool --generate-privkey --outfile rootca-key.pem
echo '
organization = "CSAF Tools Development (internal)"
country = DE
cn = "Tester"
ca
cert_signing_key
crl_signing_key
serial = 001
expiration_days = 100
' >gnutls-certtool.rootca.template
certtool --generate-self-signed --load-privkey rootca-key.pem --outfile rootca-cert.pem --template gnutls-certtool.rootca.template
```
## create webserver cert
```bash
#being in devca1/
certtool --generate-privkey --outfile testserver-key.pem
echo '
organization = "CSAF Tools Development (internal)"
country = DE
cn = "Service Testing"
tls_www_server
signing_key
encryption_key
non_repudiation
dns_name = "*.local"
dns_name = "localhost"
serial = 010
expiration_days = 50
' > gnutls-certtool.testserver.template
certtool --generate-certificate --load-privkey testserver-key.pem --outfile testserver.crt --load-ca-certificate rootca-cert.pem --load-ca-privkey rootca-key.pem --template gnutls-certtool.testserver.template
cat testserver.crt rootca-cert.pem >bundle.crt
echo Full path config options for nginx:
echo " ssl_certificate \"$PWD/bundle.crt\";"
echo " ssl_certificate_key \"$PWD/testserver-key.pem\";"
```
## Considerations and References
* The command line and template options are explained in the
GnuTLS documentation at the end of _certtool Invocation_, see the [section of the current stable documentation](https://gnutls.org/manual/html_node/certtool-Invocation.html), but be aware that it maybe newer than
the version you have installed.
* Using GnuTLS instead of OpenSSL, because GnuTLS is an implementation
with a long, good track record. Configuration is also slightly slimmer.
(Overall it is positive for the security of Open Standards
like TLS and CMS, that there are several competing compatible
implementations. Selecting a different implementation here and there helps
the ecosystem by fostering that competition.)
* Using the GnuTLS default algorithm (RSA 3072 at time for writing) is
good enough, as the goal is not to test ECC compatibility for client
certificates for servers, browser and tools.
* An example script for server certs:
https://gist.github.com/epcim/832cec2482a255e3f392
* An example for client certs as part of the libvirt setup instructions:
https://wiki.libvirt.org/page/TLSCreateClientCerts

View file

@ -14,9 +14,11 @@ There are three ways to get a TLS certificate for your HTTPS server:
[Let's encrypt](https://letsencrypt.org/) without a fee. [Let's encrypt](https://letsencrypt.org/) without a fee.
See their instruction, e.g. See their instruction, e.g.
[certbot for nignx on Ubuntu](https://certbot.eff.org/instructions?ws=nginx&os=ubuntufocal). [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 3. [Run your own little CA](development-ca.md).
will have to import the root certificate in the webbrowsers manually. Which has the major drawback that someone
Suitable for development purposes. will have to import the root certificate in the webbrowsers manually or
override warning on each connect.
Suitable for development purposes, must not be used for production servers.
To decide between 1. and 2. you will need to weight the extra To decide between 1. and 2. you will need to weight the extra
efforts and costs of the level of extended validation against efforts and costs of the level of extended validation against

View file

@ -76,6 +76,7 @@ server {
include fcgiwrap.conf; include fcgiwrap.conf;
} }
``` ```
Reload nginx to apply the changes (e.g. ```systemctl reload nginx``` on Debian or Ubuntu).
Place the binary under `/usr/lib/cgi-bin/csaf_provider.go`. Place the binary under `/usr/lib/cgi-bin/csaf_provider.go`.
Make sure `/usr/lib/cgi-bin/` exists. Make sure `/usr/lib/cgi-bin/` exists.
@ -90,8 +91,10 @@ key = "/usr/lib/csaf/private.asc"
domain = "http://192.168.56.102" domain = "http://192.168.56.102"
#no_passphrase = true #no_passphrase = true
``` ```
with suitable replacements
(This configurations-example assumes that the private/public keys are available under `/usr/lib/csaf/`).
with suitable replacements.
Create the folders: Create the folders:
```(shell) ```(shell)

30
util/file_test.go Normal file
View file

@ -0,0 +1,30 @@
package util
import (
"bytes"
"testing"
)
func TestNWriter(t *testing.T) {
msg := []byte("Gruß!\n")
first, second := msg[:len(msg)/2], msg[len(msg)/2:]
var buf bytes.Buffer
nw := NWriter{Writer: &buf, N: 0}
_, err1 := nw.Write(first)
_, err2 := nw.Write(second)
if err1 != nil || err2 != nil {
t.Error("Calling NWriter failed")
}
if n := int64(len(msg)); nw.N != n {
t.Errorf("Expected %d bytes, but counted %d.", n, nw.N)
}
if out := buf.Bytes(); !bytes.Equal(msg, out) {
t.Errorf("Expected %q, but got %q", msg, out)
}
}

View file

@ -9,7 +9,12 @@
package util package util
import ( import (
"context"
"encoding/json" "encoding/json"
"errors"
"github.com/PaesslerAG/gval"
"github.com/PaesslerAG/jsonpath"
) )
// ReMarshalJSON transforms data from src to dst via JSON marshalling. // ReMarshalJSON transforms data from src to dst via JSON marshalling.
@ -20,3 +25,34 @@ func ReMarshalJSON(dst, src interface{}) error {
} }
return json.Unmarshal(intermediate, dst) return json.Unmarshal(intermediate, dst)
} }
// PathEval is a helper to evaluate JSON paths on documents.
type PathEval struct {
builder gval.Language
exprs map[string]gval.Evaluable
}
// NewPathEval creates a new PathEval.
func NewPathEval() *PathEval {
return &PathEval{
builder: gval.Full(jsonpath.Language()),
exprs: map[string]gval.Evaluable{},
}
}
// Eval evalutes expression expr on document doc.
// Returns the result of the expression.
func (pe *PathEval) Eval(expr string, doc interface{}) (interface{}, error) {
if doc == nil {
return nil, errors.New("no document to extract data from")
}
eval := pe.exprs[expr]
if eval == nil {
var err error
if eval, err = pe.builder.NewEvaluable(expr); err != nil {
return nil, err
}
pe.exprs[expr] = eval
}
return eval(context.Background(), doc)
}