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
uses: Jerome1337/golint-action@v1.0.2
- name: Tests
run: go test -v ./...

View file

@ -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:<span></span>//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.

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 {
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"}},
}
}

View file

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

View file

@ -3,8 +3,8 @@
//
// SPDX-License-Identifier: MIT
//
// SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
// Software-Engineering: 2021 Intevation GmbH <https://intevation.de>
// SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
// Software-Engineering: 2022 Intevation GmbH <https://intevation.de>
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)))
}
}

View file

@ -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,
}

View file

@ -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 == "" {

View file

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

View file

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

View file

@ -3,10 +3,10 @@
//
// SPDX-License-Identifier: MIT
//
// SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
// Software-Engineering: 2021 Intevation GmbH <https://intevation.de>
// SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) <https://www.bsi.bund.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
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"`

View file

@ -3,8 +3,8 @@
//
// SPDX-License-Identifier: MIT
//
// SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
// Software-Engineering: 2021 Intevation GmbH <https://intevation.de>
// SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
// Software-Engineering: 2022 Intevation GmbH <https://intevation.de>
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

View file

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

View file

@ -6,17 +6,12 @@
// SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
// Software-Engineering: 2021 Intevation GmbH <https://intevation.de>
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
}

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.
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

View file

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

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