diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 083ca92..89d012b 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -29,3 +29,6 @@ jobs: - name: golint uses: Jerome1337/golint-action@v1.0.2 + + - name: Tests + run: go test -v ./... diff --git a/README.md b/README.md index 76a6757..6749feb 100644 --- a/README.md +++ b/README.md @@ -2,21 +2,19 @@ **WIP**: A proof of concept for a CSAF trusted provider, checker and aggregator. - ## Setup - 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 ` -- Build Go components - Makefile supplies the following targets: +- Build Go components Makefile supplies the following targets: - Build For GNU/Linux System: `make build_linux` - Build For Windows System (cross build): `make build_win` - 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. + - 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` Binaries will be placed in directories named like `bin-linux-amd64/` and `bin-windows-amd64/`. @@ -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) ## csaf_uploader + csaf_uploader is a command line tool that uploads CSAF documents to the trusted provider (CSAF_Provider). Following options are supported: @@ -35,13 +34,14 @@ Following options are supported: | -a, --action=[upload\|create] | Action to perform (default: upload) | | -u, --url=URL | URL of the CSAF provider (default:https://localhost/cgi-bin/csaf_provider.go) | | -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 | | -p, --password=PASSWORD | Authentication password for accessing the CSAF provider | | -P, --passphrase=PASSPHRASE | Passphrase to unlock the OpenPGP key | | -i, --password-interactive | Enter password interactively | | -I, --passphrase-interacive | Enter passphrase interactively | | -c, --config=INI-FILE | Path to config ini file | +| --insecure | Do not check TSL certificates from provider | | -h, --help | Show help | E.g. creating the initial directiories and files @@ -71,6 +71,12 @@ action=create 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 - csaf_distribution is licensed as Free Software under MIT License. diff --git a/cmd/csaf_checker/main.go b/cmd/csaf_checker/main.go index 58493ed..aeff022 100644 --- a/cmd/csaf_checker/main.go +++ b/cmd/csaf_checker/main.go @@ -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 { enc := json.NewEncoder(w) enc.SetIndent("", " ") @@ -48,6 +50,8 @@ func writeJSON(report *Report, w io.WriteCloser) error { 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 { tmpl, err := template.New("Report HTML").Parse(reportHTML) if err != nil { @@ -72,6 +76,8 @@ type nopCloser struct{ io.Writer } 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 { var w io.WriteCloser @@ -98,6 +104,8 @@ func writeReport(report *Report, opts *options) error { 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 { return []reporter{ &tlsReporter{baseReporter{num: 3, description: "TLS"}}, @@ -112,7 +120,7 @@ func buildReporters() []reporter { &directoryListingsReporter{baseReporter{num: 14, description: "Directory listings"}}, &integrityReporter{baseReporter{num: 18, description: "Integrity"}}, &signaturesReporter{baseReporter{num: 19, description: "Signatures"}}, - &publicPGPKeyReporter{baseReporter{num: 20, description: "Public PGP Key"}}, + &publicPGPKeyReporter{baseReporter{num: 20, description: "Public OpenPGP Key"}}, } } diff --git a/cmd/csaf_checker/processor.go b/cmd/csaf_checker/processor.go index 3e8d403..1ebcf47 100644 --- a/cmd/csaf_checker/processor.go +++ b/cmd/csaf_checker/processor.go @@ -11,7 +11,6 @@ package main import ( "bufio" "bytes" - "context" "crypto/sha256" "crypto/sha512" "crypto/tls" @@ -20,6 +19,7 @@ import ( "errors" "fmt" "io" + "log" "net/http" "net/url" "regexp" @@ -28,8 +28,6 @@ import ( "strings" "time" - "github.com/PaesslerAG/gval" - "github.com/PaesslerAG/jsonpath" "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/csaf-poc/csaf_distribution/csaf" @@ -43,6 +41,7 @@ type processor struct { redirects map[string]string noneTLS map[string]struct{} alreadyChecked map[string]whereType + pmdURL string pmd256 []byte pmd interface{} keys []*crypto.KeyRing @@ -56,10 +55,12 @@ type processor struct { badChanges []string badFolders []string - builder gval.Language - exprs map[string]gval.Evaluable + expr *util.PathEval } +// 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 { 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 { return &processor{ opts: opts, alreadyChecked: map[string]whereType{}, - builder: gval.Full(jsonpath.Language()), - exprs: map[string]gval.Evaluable{}, + expr: util.NewPathEval(), } } +// clean clears the fields values of the given processor. func (p *processor) clean() { p.redirects = nil p.noneTLS = nil for k := range p.alreadyChecked { delete(p.alreadyChecked, k) } + p.pmdURL = "" p.pmd256 = nil p.pmd = nil p.keys = nil @@ -131,6 +135,10 @@ func (p *processor) clean() { p.badIndices = 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) { var report Report @@ -174,21 +182,8 @@ func (p *processor) checkDomain(domain string) error { return nil } -func (p *processor) jsonPath(expr string, doc interface{}) (interface{}, error) { - if doc == nil { - 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) -} - +// checkTLS parses the given URL to check its schema, as a result it sets +// the value of "noneTLS" field if it is not HTTPS. func (p *processor) checkTLS(u string) { if p.noneTLS == nil { p.noneTLS = map[string]struct{}{} @@ -247,6 +242,7 @@ func (p *processor) httpClient() *http.Client { return p.client } +// use checks the given array and initializes an empty array if its nil. func use(s *[]string) { if *s == nil { *s = []string{} @@ -257,34 +253,50 @@ func used(s []string) bool { 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{}) { 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{}) { 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{}) { 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{}) { 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{}) { 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{}) { 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{}) { 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{}) { 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. use(&p.badFolders) - if date, err := p.jsonPath( + if date, err := p.expr.Eval( `$.document.tracking.initial_release_date`, doc); err != nil { p.badFolder( "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. var files []string - for _, f := range rfeed.Entry { + for _, f := range rfeed.Feed.Entry { for i := range f.Link { files = append(files, f.Link[i].HRef) } @@ -508,6 +520,9 @@ func (p *processor) processROLIEFeed(feed string) error { 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 { client := p.httpClient() 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) } +// 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 { client := p.httpClient() 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 { - base, err := url.Parse("https://" + domain + "/.well-known/csaf/") + base, err := url.Parse(p.pmdURL) if err != nil { return err } @@ -634,7 +653,7 @@ func (p *processor) processROLIEFeeds(domain string, feeds [][]csaf.Feed) error func (p *processor) checkCSAFs(domain string) error { // 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 { return err } @@ -654,7 +673,10 @@ func (p *processor) checkCSAFs(domain string) error { } // 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 { return err @@ -701,53 +723,156 @@ func (p *processor) checkMissing(string) error { 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() - 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) - - res, err := client.Get(url) - if err != nil { - p.badProviderMetadata("Fetching %s: %v.", url, err) - return errStop + if err := func() error { + defer res.Body.Close() + return found(url, res.Body) + }(); err != nil { + return false, err + } + return true, nil } - if res.StatusCode != http.StatusOK { - p.badProviderMetadata("Fetching %s failed. Status code: %d (%s)", - url, res.StatusCode, res.Status) - return errStop + for _, loc := range providerMetadataLocations { + url := "https://" + domain + "/" + loc + ok, err := tryURL(url) + if err != nil { + if err == errContinue { + continue + } + return err + } + if ok { + return nil + } } - // Calculate checksum for later comparison. - hash := sha256.New() + // Read from security.txt - if err := func() error { - defer res.Body.Close() - 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) + path := "https://" + domain + "/.well-known/security.txt" + res, err := client.Get(path) if err != nil { return err } - if len(errors) > 0 { - p.badProviderMetadata("Validating against JSON schema failed:") - for _, msg := range errors { - p.badProviderMetadata(strings.ReplaceAll(msg, `%`, `%%`)) + + if res.StatusCode != http.StatusOK { + return err + } + + 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. + 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 } +// 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 { client := p.httpClient() @@ -826,24 +951,28 @@ func (p *processor) checkSecurity(domain string) error { 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 { use(&p.badPGPs) - src, err := p.jsonPath("$.pgp_keys", p.pmd) + src, err := p.expr.Eval("$.pgp_keys", p.pmd) if err != nil { - p.badPGP("No PGP keys found: %v.", err) + p.badPGP("No public OpenPGP keys found: %v.", err) return errContinue } var keys []csaf.PGPKey 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 } if len(keys) == 0 { - p.badPGP("No PGP keys found.") + p.badPGP("No public OpenPGP keys found.") return errContinue } @@ -851,7 +980,7 @@ func (p *processor) checkPGPKeys(domain string) error { client := p.httpClient() - base, err := url.Parse("https://" + domain + "/.well-known/csaf/provider-metadata.json") + base, err := url.Parse(p.pmdURL) if err != nil { return err } @@ -873,11 +1002,11 @@ func (p *processor) checkPGPKeys(domain string) error { res, err := client.Get(u) 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 } 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) continue } @@ -888,24 +1017,24 @@ func (p *processor) checkPGPKeys(domain string) error { }() 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 } 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 } keyring, err := crypto.NewKeyRing(ckey) 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 } p.keys = append(p.keys, keyring) } if len(p.keys) == 0 { - p.badPGP("No PGP keys loaded.") + p.badPGP("No OpenPGP keys loaded.") } return nil } diff --git a/cmd/csaf_checker/reporters.go b/cmd/csaf_checker/reporters.go index 173c84b..be79926 100644 --- a/cmd/csaf_checker/reporters.go +++ b/cmd/csaf_checker/reporters.go @@ -3,8 +3,8 @@ // // SPDX-License-Identifier: MIT // -// SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) -// Software-Engineering: 2021 Intevation GmbH +// SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) +// Software-Engineering: 2022 Intevation GmbH package main @@ -42,6 +42,9 @@ func (bc *baseReporter) requirement(domain *Domain) *Requirement { 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) { req := r.requirement(domain) if p.noneTLS == nil { @@ -49,7 +52,7 @@ func (r *tlsReporter) report(p *processor, domain *Domain) { return } if len(p.noneTLS) == 0 { - req.message("All tested URLs were https.") + req.message("All tested URLs were HTTPS.") return } @@ -60,10 +63,12 @@ func (r *tlsReporter) report(p *processor, domain *Domain) { i++ } sort.Strings(urls) - req.message("Following none https URLs were used:") + req.message("Following non-HTTPS URLs were used:") 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) { req := r.requirement(domain) if len(p.redirects) == 0 { @@ -84,6 +89,8 @@ func (r *redirectsReporter) report(p *processor, domain *Domain) { 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) { req := r.requirement(domain) if !used(p.badProviderMetadatas) { @@ -91,12 +98,14 @@ func (r *providerMetadataReport) report(p *processor, domain *Domain) { return } if len(p.badProviderMetadatas) == 0 { - req.message("No problems with provider metadata.") + req.message("Found good provider metadata.") return } 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) { req := r.requirement(domain) if !used(p.badSecurities) { @@ -104,7 +113,7 @@ func (r *securityReporter) report(p *processor, domain *Domain) { return } if len(p.badSecurities) == 0 { - req.message("No problems with security.txt found.") + req.message("Found good security.txt.") return } req.Messages = p.badSecurities @@ -113,19 +122,19 @@ func (r *securityReporter) report(p *processor, domain *Domain) { func (r *wellknownMetadataReporter) report(_ *processor, domain *Domain) { // TODO: Implement me! req := r.requirement(domain) - _ = req + req.message("(Not checked, missing implementation.)") } func (r *dnsPathReporter) report(_ *processor, domain *Domain) { // TODO: Implement me! req := r.requirement(domain) - _ = req + req.message("(Not checked, missing implementation.)") } func (r *oneFolderPerYearReport) report(p *processor, domain *Domain) { req := r.requirement(domain) 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 } if len(p.badFolders) == 0 { @@ -142,7 +151,7 @@ func (r *indexReporter) report(p *processor, domain *Domain) { return } if len(p.badIndices) == 0 { - req.message("No problems with index.txt found.") + req.message("Found good index.txt.") return } req.Messages = p.badIndices @@ -155,7 +164,7 @@ func (r *changesReporter) report(p *processor, domain *Domain) { return } if len(p.badChanges) == 0 { - req.message("No problems with changes.csv found.") + req.message("Found good changes.csv.") return } req.Messages = p.badChanges @@ -195,11 +204,11 @@ func (r *signaturesReporter) report(p *processor, domain *Domain) { func (r *publicPGPKeyReporter) report(p *processor, domain *Domain) { req := r.requirement(domain) if !used(p.badPGPs) { - req.message("No PGP keys loaded.") + req.message("No public OpenPGP keys loaded.") return } req.Messages = p.badPGPs 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))) } } diff --git a/cmd/csaf_provider/actions.go b/cmd/csaf_provider/actions.go index ccb85d8..41a91b9 100644 --- a/cmd/csaf_provider/actions.go +++ b/cmd/csaf_provider/actions.go @@ -29,6 +29,8 @@ import ( 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 { s = strings.ReplaceAll(s, `/`, ``) s = strings.ReplaceAll(s, `\`, ``) @@ -37,6 +39,10 @@ func cleanFileName(s string) string { 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) { file, handler, err := r.FormFile("csaf") if err != nil { @@ -123,6 +129,8 @@ func (c *controller) tlpParam(r *http.Request) (tlp, error) { 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) { if err := ensureFolders(c.cfg); err != nil { 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 { return nil, err } @@ -171,8 +179,9 @@ func (c *controller) upload(r *http.Request) (interface{}, error) { // Extract real TLP from document. if t == tlpCSAF { - if t = tlp(strings.ToLower(ex.tlpLabel)); !t.valid() || t == tlpCSAF { - return nil, fmt.Errorf("not a valid TL: %s", ex.tlpLabel) + if t = tlp(strings.ToLower(ex.TLPLabel)); !t.valid() || t == tlpCSAF { + 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. if rolie == nil { rolie = &csaf.ROLIEFeed{ - ID: "csaf-feed-tlp-" + ts, - Title: "CSAF feed (TLP:" + string(tlpLabel) + ")", - Link: []csaf.Link{{ - Rel: "rel", - HRef: string(feedURL), - }}, + Feed: csaf.FeedData{ + ID: "csaf-feed-tlp-" + ts, + Title: "CSAF feed (TLP:" + string(tlpLabel) + ")", + Link: []csaf.Link{{ + Rel: "rel", + 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 + "/.well-known/csaf/" + ts + "/" + year + "/" + newCSAF - e := rolie.EntryByID(ex.id) + e := rolie.EntryByID(ex.ID) if e == nil { - e = &csaf.Entry{ID: ex.id} - rolie.Entry = append(rolie.Entry, e) + e = &csaf.Entry{ID: ex.ID} + rolie.Feed.Entry = append(rolie.Feed.Entry, e) } - e.Titel = ex.title - e.Published = csaf.TimeStamp(ex.initialReleaseDate) - e.Updated = csaf.TimeStamp(ex.currentReleaseDate) + e.Titel = ex.Title + e.Published = csaf.TimeStamp(ex.InitialReleaseDate) + e.Updated = csaf.TimeStamp(ex.CurrentReleaseDate) e.Link = []csaf.Link{{ Rel: "self", HRef: csafURL, @@ -254,8 +265,8 @@ func (c *controller) upload(r *http.Request) (interface{}, error) { Type: "application/json", Src: csafURL, } - if ex.summary != "" { - e.Summary = &csaf.Summary{Content: ex.summary} + if ex.Summary != "" { + e.Summary = &csaf.Summary{Content: ex.Summary} } else { e.Summary = nil } @@ -291,7 +302,7 @@ func (c *controller) upload(r *http.Request) (interface{}, error) { if err := updateIndices( folder, filepath.Join(year, newCSAF), - ex.currentReleaseDate, + ex.CurrentReleaseDate, ); err != nil { 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?") if c.cfg.DynamicProviderMetaData { 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.") } @@ -323,7 +334,7 @@ func (c *controller) upload(r *http.Request) (interface{}, error) { Error error `json:"-"` }{ Name: newCSAF, - ReleaseDate: ex.currentReleaseDate.Format(dateFormat), + ReleaseDate: ex.CurrentReleaseDate.Format(dateFormat), Warnings: warnings, } diff --git a/cmd/csaf_provider/config.go b/cmd/csaf_provider/config.go index def8c26..cd9e056 100644 --- a/cmd/csaf_provider/config.go +++ b/cmd/csaf_provider/config.go @@ -21,14 +21,16 @@ import ( ) const ( + // The environment name, that contains the path to the config file. configEnv = "CSAF_CONFIG" - defaultConfigPath = "/usr/lib/casf/config.toml" - defaultFolder = "/var/www/" - defaultWeb = "/var/www/html" - defaultOpenPGPURL = "https://openpgp.circl.lu/pks/lookup?op=get&search=${FINGERPRINT}" - defaultUploadLimit = 50 * 1024 * 1024 + defaultConfigPath = "/usr/lib/csaf/config.toml" // Default path to the config file. + defaultFolder = "/var/www/" // Default folder path. + defaultWeb = "/var/www/html" // Default web path. + defaultOpenPGPURL = "https://openpgp.circl.lu/pks/lookup?op=get&search=${FINGERPRINT}" // Default OpenPGP URL. + defaultUploadLimit = 50 * 1024 * 1024 // Default limit size of the uploaded file. ) +// configs contains the config values for the provider. type config struct { Password *string `toml:"password"` Key string `toml:"key"` @@ -57,6 +59,7 @@ const ( tlpRed tlp = "red" ) +// valid returns true if the checked tlp matches one of the defined tlps. func (t tlp) valid() bool { switch t { 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)) } +// 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 { // Zero or less means no upload limit. if cfg.UploadLimit == nil || *cfg.UploadLimit < 1 { @@ -101,6 +106,8 @@ func (cfg *config) modelTLPs() []csaf.TLPLabel { 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) { f, err := os.Open(cfg.Key) if err != nil { @@ -110,11 +117,18 @@ func (cfg *config) loadCryptoKey() (*crypto.Key, error) { 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 { return 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) { path := os.Getenv(configEnv) if path == "" { diff --git a/cmd/csaf_provider/controller.go b/cmd/csaf_provider/controller.go index 74d7953..11330ee 100644 --- a/cmd/csaf_provider/controller.go +++ b/cmd/csaf_provider/controller.go @@ -38,11 +38,14 @@ func asMultiError(err error) multiError { return multiError([]string{err.Error()}) } +// controller contains the config values and the html templates. type controller struct { cfg *config 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) { c := controller{cfg: cfg} @@ -57,6 +60,8 @@ func newController(cfg *config) (*controller, error) { 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) { if !c.cfg.NoWebUI { 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))) } +// 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( fn 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{}) { rw.Header().Set("Content-type", "text/html; charset=utf-8") 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) { result := map[string]interface{}{"Error": asMultiError(err)} 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) { c.render(rw, "index.html", map[string]interface{}{ "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( fn func(*http.Request) (interface{}, error), 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) { rw.Header().Set("Content-type", "application/json; charset=utf-8") rw.Header().Set("X-Content-Type-Options", "nosniff") diff --git a/cmd/csaf_provider/create.go b/cmd/csaf_provider/create.go index 7507dfd..b4bf281 100644 --- a/cmd/csaf_provider/create.go +++ b/cmd/csaf_provider/create.go @@ -18,6 +18,8 @@ import ( "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 { wellknown := filepath.Join(c.Web, ".well-known") @@ -38,6 +40,8 @@ func ensureFolders(c *config) error { 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 { st, err := os.Stat(wellknown) if err != nil { @@ -52,6 +56,10 @@ func createWellknown(wellknown string) error { 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 { for _, t := range c.TLPs { if t == tlpCSAF { @@ -75,6 +83,8 @@ func createFeedFolders(c *config, wellknown string) error { 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 { security := filepath.Join(wellknown, "security.txt") if _, err := os.Stat(security); err != nil { @@ -93,6 +103,7 @@ func createSecurity(c *config, wellknown string) error { return nil } +// createProviderMetadata creates the provider-metadata.json file if does not exist. func createProviderMetadata(c *config, wellknownCSAF string) error { path := filepath.Join(wellknownCSAF, "provider-metadata.json") _, err := os.Stat(path) diff --git a/cmd/csaf_uploader/main.go b/cmd/csaf_uploader/main.go index 32179c5..b7b95b6 100644 --- a/cmd/csaf_uploader/main.go +++ b/cmd/csaf_uploader/main.go @@ -3,10 +3,10 @@ // // SPDX-License-Identifier: MIT // -// SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) -// Software-Engineering: 2021 Intevation GmbH +// SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) +// Software-Engineering: 2022 Intevation GmbH -// 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 import ( @@ -34,7 +34,7 @@ type options struct { 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"` 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."` Key *string `short:"k" long:"key" description:"OpenPGP key to sign the CSAF files" value-name:"KEY-FILE"` diff --git a/csaf/models.go b/csaf/models.go index 86ebffe..4faea86 100644 --- a/csaf/models.go +++ b/csaf/models.go @@ -3,8 +3,8 @@ // // SPDX-License-Identifier: MIT // -// SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) -// Software-Engineering: 2021 Intevation GmbH +// SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) +// Software-Engineering: 2022 Intevation GmbH package csaf @@ -72,13 +72,13 @@ type Distribution struct { type TimeStamp time.Time // Fingerprint is the fingerprint of a OpenPGP key used to sign -// the CASF documents. +// the CSAF documents. type Fingerprint string var fingerprintPattern = patternUnmarshal(`^[0-9a-fA-F]{40,}$`) // PGPKey is location and the fingerprint of the key -// used to sign the CASF documents. +// used to sign the CSAF documents. type PGPKey struct { Fingerprint Fingerprint `json:"fingerprint,omitempty"` URL *string `json:"url"` // required diff --git a/csaf/rolie.go b/csaf/rolie.go index 12e6beb..4c4a5af 100644 --- a/csaf/rolie.go +++ b/csaf/rolie.go @@ -58,8 +58,8 @@ type Entry struct { Format Format `json:"format"` } -// ROLIEFeed is a ROLIE feed. -type ROLIEFeed struct { +// FeedData is the content of the ROLIE feed. +type FeedData struct { ID string `json:"id"` Title string `json:"title"` Link []Link `json:"link,omitempty"` @@ -68,6 +68,11 @@ type ROLIEFeed struct { 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. func LoadROLIEFeed(r io.Reader) (*ROLIEFeed, error) { 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. // Returns nil if no such entry was found. func (rf *ROLIEFeed) EntryByID(id string) *Entry { - for _, entry := range rf.Entry { + for _, entry := range rf.Feed.Entry { if entry.ID == id { return entry } @@ -101,7 +106,7 @@ func (rf *ROLIEFeed) EntryByID(id string) *Entry { // SortEntriesByUpdated sorts all the entries in the feed // by their update times. func (rf *ROLIEFeed) SortEntriesByUpdated() { - entries := rf.Entry + entries := rf.Feed.Entry sort.Slice(entries, func(i, j int) bool { return time.Time(entries[j].Updated).Before(time.Time(entries[i].Updated)) }) diff --git a/cmd/csaf_provider/extract.go b/csaf/summary.go similarity index 63% rename from cmd/csaf_provider/extract.go rename to csaf/summary.go index 922b16a..5ae4630 100644 --- a/cmd/csaf_provider/extract.go +++ b/csaf/summary.go @@ -6,17 +6,12 @@ // SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) // Software-Engineering: 2021 Intevation GmbH -package main +package csaf import ( - "context" "errors" "time" - "github.com/PaesslerAG/gval" - "github.com/PaesslerAG/jsonpath" - - "github.com/csaf-poc/csaf_distribution/csaf" "github.com/csaf-poc/csaf_distribution/util" ) @@ -30,39 +25,39 @@ const ( summaryExpr = `$.document.notes[? @.category=="summary" || @.type=="summary"].text` ) -type extraction struct { - id string - title string - publisher *csaf.Publisher - initialReleaseDate time.Time - currentReleaseDate time.Time - summary string - tlpLabel string +// AdvisorySummary is a summary of some essentials of an CSAF advisory. +type AdvisorySummary struct { + ID string + Title string + Publisher *Publisher + InitialReleaseDate time.Time + CurrentReleaseDate time.Time + Summary string + TLPLabel string } 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) { - eval, err := builder.NewEvaluable(expr) - if err != nil { - return nil, err - } - return eval(context.Background(), content) + path := func(s string) (interface{}, error) { + return expr.Eval(s, doc) } - e := new(extraction) - for _, fn := range []func(extractFunc) error{ - extractText(idExpr, &e.id), - extractText(titleExpr, &e.title), - extractTime(currentReleaseDateExpr, &e.currentReleaseDate), - extractTime(initialReleaseDateExpr, &e.initialReleaseDate), - extractText(summaryExpr, &e.summary), - extractText(tlpLabelExpr, &e.tlpLabel), + extractText(idExpr, &e.ID), + extractText(titleExpr, &e.Title), + extractTime(currentReleaseDateExpr, &e.CurrentReleaseDate), + extractTime(initialReleaseDateExpr, &e.InitialReleaseDate), + extractText(summaryExpr, &e.Summary), + extractText(tlpLabelExpr, &e.TLPLabel), e.extractPublisher, } { if err := fn(path); err != nil { @@ -95,7 +90,7 @@ func extractTime(expr string, store *time.Time) func(extractFunc) error { if !ok { return errors.New("not a string") } - date, err := time.Parse(dateFormat, text) + date, err := time.Parse(time.RFC3339, text) if err == nil { *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) if err != nil { return err @@ -111,13 +106,13 @@ func (e *extraction) extractPublisher(path extractFunc) error { // XXX: It's a bit cumbersome to serialize and deserialize // it into our own structure. - publisher := new(csaf.Publisher) + publisher := new(Publisher) if err := util.ReMarshalJSON(publisher, p); err != nil { return err } if err := publisher.Validate(); err != nil { return err } - e.publisher = publisher + e.Publisher = publisher return nil } diff --git a/docs/development-ca.md b/docs/development-ca.md new file mode 100644 index 0000000..81ca4d7 --- /dev/null +++ b/docs/development-ca.md @@ -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 diff --git a/docs/install-server-certificate.md b/docs/install-server-certificate.md index 94b0340..eb2c092 100644 --- a/docs/install-server-certificate.md +++ b/docs/install-server-certificate.md @@ -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. 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. + 3. [Run your own little CA](development-ca.md). + Which has the major drawback that someone + 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 efforts and costs of the level of extended validation against diff --git a/docs/provider-setup.md b/docs/provider-setup.md index 699cff5..c0da7f3 100644 --- a/docs/provider-setup.md +++ b/docs/provider-setup.md @@ -62,7 +62,7 @@ server { location / { # Other config - # ... + # ... # For atomic directory switches disable_symlinks off; @@ -76,6 +76,7 @@ server { 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`. Make sure `/usr/lib/cgi-bin/` exists. @@ -90,8 +91,10 @@ key = "/usr/lib/csaf/private.asc" domain = "http://192.168.56.102" #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: ```(shell) diff --git a/util/file_test.go b/util/file_test.go new file mode 100644 index 0000000..f94d4ac --- /dev/null +++ b/util/file_test.go @@ -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) + } +} diff --git a/util/json.go b/util/json.go index d7f6cf6..df3b9f7 100644 --- a/util/json.go +++ b/util/json.go @@ -9,7 +9,12 @@ package util import ( + "context" "encoding/json" + "errors" + + "github.com/PaesslerAG/gval" + "github.com/PaesslerAG/jsonpath" ) // 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) } + +// 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) +}