diff --git a/cmd/csaf_checker/checks.go b/cmd/csaf_checker/checks.go index ff4e361..3f0f8cc 100644 --- a/cmd/csaf_checker/checks.go +++ b/cmd/csaf_checker/checks.go @@ -9,362 +9,152 @@ package main import ( - "bufio" - "bytes" - "crypto/sha256" "fmt" - "io" - "net/http" - "net/url" "sort" - "strings" - - "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/csaf-poc/csaf_distribution/csaf" - "github.com/csaf-poc/csaf_distribution/util" ) -type baseCheck struct { - exec int - num int - description string - messages []string -} +type ( + baseReporter struct { + num int + description string + } + tlsReport struct{ baseReporter } + redirectsReport struct{ baseReporter } + providerMetadataReport struct{ baseReporter } + securityReport struct{ baseReporter } + wellknownMetadataReport struct{ baseReporter } + dnsPathReport struct{ baseReporter } + oneFolderPerYearReport struct{ baseReporter } + indexReport struct{ baseReporter } + changesReport struct{ baseReporter } + directoryListingsReport struct{ baseReporter } + integrityReport struct{ baseReporter } + signaturesReport struct{ baseReporter } + publicPGPKeyReport struct{ baseReporter } +) -type tlsCheck struct { - baseCheck -} - -type redirectsCheck struct { - baseCheck -} - -type providerMetadataCheck struct { - baseCheck -} - -type securityCheck struct { - baseCheck -} - -type wellknownMetadataCheck struct { - baseCheck -} - -type dnsPathCheck struct { - baseCheck -} - -type oneFolderPerYearCheck struct { - baseCheck -} - -type indexCheck struct { - baseCheck -} - -type changesCheck struct { - baseCheck -} - -type directoryListingsCheck struct { - baseCheck -} - -type integrityCheck struct { - baseCheck -} - -type signaturesCheck struct { - baseCheck -} - -type publicPGPKeyCheck struct { - baseCheck -} - -func (bc *baseCheck) executionOrder() int { - return bc.exec -} - -func (bc *baseCheck) run(*processor, string) error { - return nil -} - -func (bc *baseCheck) report(_ *processor, domain *Domain) { +func (bc *baseReporter) requirement(domain *Domain) *Requirement { req := &Requirement{ Num: bc.num, Description: bc.description, - Messages: bc.messages, } domain.Requirements = append(domain.Requirements, req) + return req } -func (bc *baseCheck) add(messages ...string) { - bc.messages = append(bc.messages, messages...) -} - -func (bc *baseCheck) sprintf(format string, args ...interface{}) { - msg := fmt.Sprintf(format, args...) - bc.messages = append(bc.messages, msg) -} - -func (bc *baseCheck) ok(message string) bool { - k := len(bc.messages) == 0 - if k { - bc.messages = []string{message} - } - return k -} - -func (tc *tlsCheck) run(p *processor, domain string) error { +func (r *tlsReport) report(p *processor, domain *Domain) { + req := r.requirement(domain) if len(p.noneTLS) == 0 { - tc.add("All tested URLs were https.") - } else { - urls := make([]string, len(p.noneTLS)) - var i int - for k := range p.noneTLS { - urls[i] = k - i++ - } - sort.Strings(urls) - tc.add("Following none https URLs were used:") - tc.add(urls...) + req.message("All tested URLs were https.") + return } - return nil + + urls := make([]string, len(p.noneTLS)) + var i int + for k := range p.noneTLS { + urls[i] = k + i++ + } + sort.Strings(urls) + req.message("Following none https URLs were used:") + req.message(urls...) } -func (rc *redirectsCheck) run(p *processor, domain string) error { +func (r *redirectsReport) report(p *processor, domain *Domain) { + req := r.requirement(domain) if len(p.redirects) == 0 { - rc.add("No redirections found.") - } else { - keys := make([]string, len(p.redirects)) - var i int - for k := range p.redirects { - keys[i] = k - i++ - } - sort.Strings(keys) - for i, k := range keys { - keys[i] = fmt.Sprintf("Redirect %s: %s", k, p.redirects[k]) - } - rc.baseCheck.messages = keys + req.message("No redirections found.") + return } - return nil + + keys := make([]string, len(p.redirects)) + var i int + for k := range p.redirects { + keys[i] = k + i++ + } + sort.Strings(keys) + for i, k := range keys { + keys[i] = fmt.Sprintf("Redirect %s: %s", k, p.redirects[k]) + } + req.Messages = keys } -func (pmdc *providerMetadataCheck) run(p *processor, domain string) error { - - if err := p.checkProviderMetadata(domain, pmdc.sprintf); err != nil { - return err +func (r *providerMetadataReport) report(p *processor, domain *Domain) { + req := r.requirement(domain) + if len(p.badProviderMetadatas) == 0 { + req.message("No problems with provider metadata.") + return } - - pmdc.ok("No problems with provider metadata.") - return nil + req.Messages = p.badProviderMetadatas } -func (sc *securityCheck) run(p *processor, domain string) error { - path := "https://" + domain + "/.well-known/security.txt" - client := p.httpClient() - res, err := client.Get(path) - if err != nil { - return err +func (r *securityReport) report(p *processor, domain *Domain) { + req := r.requirement(domain) + if len(p.badSecurity) == 0 { + req.message("No problems with security.txt.") + return } - if res.StatusCode != http.StatusOK { - sc.sprintf("Fetching security failed. Status code %d (%s)", res.StatusCode, res.Status) - return nil - } - u, err := func() (string, error) { - defer res.Body.Close() - lines := bufio.NewScanner(res.Body) - for lines.Scan() { - line := lines.Text() - if strings.HasPrefix(line, "CSAF:") { - return strings.TrimSpace(line[6:]), nil - } - } - return "", lines.Err() - }() - if err != nil { - sc.sprintf("Error while reading security.txt: %s", err.Error()) - } - if u == "" { - sc.add("No CSAF line found in security.txt.") - return nil - } - - // Try to load - up, err := url.Parse(u) - if err != nil { - sc.sprintf("CSAF URL '%s' invalid: %s.", u, err.Error()) - return nil - } - - base, err := url.Parse("https://" + domain + "/.well-known/") - if err != nil { - return err - } - ur := base.ResolveReference(up) - u = ur.String() - p.checkTLS(u) - if res, err = client.Get(u); err != nil { - sc.sprintf("Cannot fetch %s from security.txt: %v", u, err) - return nil - } - if res.StatusCode != http.StatusOK { - sc.sprintf("Fetching %s failed. Status code %d (%s).", - u, res.StatusCode, res.Status) - return nil - } - defer res.Body.Close() - // Compare checksums to already read provider-metadata.json. - h := sha256.New() - if _, err := io.Copy(h, res.Body); err != nil { - sc.sprintf("Reading %s failed: %v.", u, err) - return nil - } - - if !bytes.Equal(h.Sum(nil), p.pmd256) { - sc.sprintf( - "Content of %s from security.txt is not identical to .well-known/csaf/provider-metadata.json", u) - } - - sc.ok("Valid CSAF line in security.txt found.") - - return nil + req.Messages = p.badSecurity } -func (wmdc *wellknownMetadataCheck) run(*processor, string) error { +func (r *wellknownMetadataReport) report(_ *processor, domain *Domain) { // TODO: Implement me! - return nil + req := r.requirement(domain) + _ = req } -func (dpc *dnsPathCheck) run(*processor, string) error { +func (r *dnsPathReport) report(_ *processor, domain *Domain) { // TODO: Implement me! - return nil + req := r.requirement(domain) + _ = req } -func (ofpyc *oneFolderPerYearCheck) run(p *processor, domain string) error { - - // TODO: This does not belong here! - return p.checkCSAFs(domain, ofpyc.sprintf) -} - -func (ic *indexCheck) run(*processor, string) error { +func (r *oneFolderPerYearReport) report(_ *processor, domain *Domain) { // TODO: Implement me! - return nil + req := r.requirement(domain) + _ = req } -func (cc *changesCheck) run(*processor, string) error { +func (r *indexReport) report(_ *processor, domain *Domain) { // TODO: Implement me! - return nil + req := r.requirement(domain) + _ = req } -func (dlc *directoryListingsCheck) run(*processor, string) error { +func (r *changesReport) report(_ *processor, domain *Domain) { // TODO: Implement me! - return nil + req := r.requirement(domain) + _ = req } -func (ic *integrityCheck) run(p *processor, _ string) error { - if len(p.badHashes) > 0 { - ic.baseCheck.messages = p.badHashes - } else { - ic.add("All checksums match.") - } - return nil +func (r *directoryListingsReport) report(_ *processor, domain *Domain) { + // TODO: Implement me! + req := r.requirement(domain) + _ = req } -func (sc *signaturesCheck) run(p *processor, _ string) error { - if len(p.badSignatures) > 0 { - sc.baseCheck.messages = p.badSignatures - } else { - sc.add("All signatures verified.") +func (r *integrityReport) report(p *processor, domain *Domain) { + req := r.requirement(domain) + if len(p.badHashes) == 0 { + req.message("All checksums match.") + return } - return nil + req.Messages = p.badHashes } -func (ppkc *publicPGPKeyCheck) run(p *processor, domain string) error { - - src, err := p.jsonPath("$.pgp_keys") - if err != nil { - ppkc.sprintf("No PGP keys found: %v.", err) - return nil +func (r *signaturesReport) report(p *processor, domain *Domain) { + req := r.requirement(domain) + if len(p.badSignatures) == 0 { + req.message("All signatures verified.") + } + req.Messages = p.badSignatures +} + +func (r *publicPGPKeyReport) report(p *processor, domain *Domain) { + req := r.requirement(domain) + req.Messages = p.badPGPs + if len(p.keys) > 0 { + req.message(fmt.Sprintf("%d PGP key(s) loaded successfully.", len(p.keys))) } - - var keys []csaf.PGPKey - if err := util.ReMarshalJSON(&keys, src); err != nil { - ppkc.sprintf("PGP keys invalid: %v.", err) - return nil - } - - if len(keys) == 0 { - ppkc.add("No PGP keys found.") - return nil - } - - // Try to load - - client := p.httpClient() - - base, err := url.Parse("https://" + domain + "/.well-known/csaf/provider-metadata.json") - if err != nil { - return err - } - - for i := range keys { - key := &keys[i] - if key.URL == nil { - ppkc.sprintf("Missing URL for fingerprint %x.", key.Fingerprint) - continue - } - up, err := url.Parse(*key.URL) - if err != nil { - ppkc.sprintf("Invalid URL '%s': %v", *key.URL, err) - continue - } - - up = base.ResolveReference(up) - u := up.String() - p.checkTLS(u) - - res, err := client.Get(u) - if err != nil { - ppkc.sprintf("Fetching PGP key %s failed: %v.", u, err) - continue - } - if res.StatusCode != http.StatusOK { - ppkc.sprintf("Fetching PGP key %s status code: %d (%s)", u, res.StatusCode, res.Status) - continue - } - - ckey, err := func() (*crypto.Key, error) { - defer res.Body.Close() - return crypto.NewKeyFromArmoredReader(res.Body) - }() - - if err != nil { - ppkc.sprintf("Reading PGP key %s failed: %v", u, err) - continue - } - - if ckey.GetFingerprint() != string(key.Fingerprint) { - ppkc.sprintf("Fingerprint of PGP key %s do not match remotely loaded.", u) - continue - } - keyring, err := crypto.NewKeyRing(ckey) - if err != nil { - ppkc.sprintf("Creatin key ring for %s failed: %v.", u, err) - continue - } - p.keys = append(p.keys, keyring) - } - - if len(p.keys) == 0 { - ppkc.add("No PGP keys loaded.") - return nil - } - - ppkc.ok(fmt.Sprintf("%d PGP key(s) loaded successfully.", len(p.keys))) - - return nil } diff --git a/cmd/csaf_checker/main.go b/cmd/csaf_checker/main.go index 7f414a2..c3d7b3b 100644 --- a/cmd/csaf_checker/main.go +++ b/cmd/csaf_checker/main.go @@ -98,21 +98,21 @@ func writeReport(report *Report, opts *options) error { return writer(report, w) } -func buildChecks() []check { - return []check{ - &tlsCheck{baseCheck{exec: 13, num: 3, description: "TLS"}}, - &redirectsCheck{baseCheck{exec: 12, num: 6, description: "Redirects"}}, - &providerMetadataCheck{baseCheck{exec: 1, num: 7, description: "provider-metadata.json"}}, - &securityCheck{baseCheck{exec: 2, num: 8, description: "security.txt"}}, - &wellknownMetadataCheck{baseCheck{exec: 3, num: 9, description: "/.well-known/csaf/provider-metadata.json"}}, - &dnsPathCheck{baseCheck{exec: 4, num: 10, description: "DNS path"}}, - &oneFolderPerYearCheck{baseCheck{exec: 6, num: 11, description: "One folder per year"}}, - &indexCheck{baseCheck{exec: 7, num: 12, description: "index.txt"}}, - &changesCheck{baseCheck{exec: 8, num: 13, description: "changes.csv"}}, - &directoryListingsCheck{baseCheck{exec: 9, num: 14, description: "Directory listings"}}, - &integrityCheck{baseCheck{exec: 11, num: 18, description: "Integrity"}}, - &signaturesCheck{baseCheck{exec: 12, num: 19, description: "Signatures"}}, - &publicPGPKeyCheck{baseCheck{exec: 5, num: 20, description: "Public PGP Key"}}, +func buildReporters() []Reporter { + return []Reporter{ + &tlsReport{baseReporter{num: 3, description: "TLS"}}, + &redirectsReport{baseReporter{num: 6, description: "Redirects"}}, + &providerMetadataReport{baseReporter{num: 7, description: "provider-metadata.json"}}, + &securityReport{baseReporter{num: 8, description: "security.txt"}}, + &wellknownMetadataReport{baseReporter{num: 9, description: "/.well-known/csaf/provider-metadata.json"}}, + &dnsPathReport{baseReporter{num: 10, description: "DNS path"}}, + &oneFolderPerYearReport{baseReporter{num: 11, description: "One folder per year"}}, + &indexReport{baseReporter{num: 12, description: "index.txt"}}, + &changesReport{baseReporter{num: 13, description: "changes.csv"}}, + &directoryListingsReport{baseReporter{num: 14, description: "Directory listings"}}, + &integrityReport{baseReporter{num: 18, description: "Integrity"}}, + &signaturesReport{baseReporter{num: 19, description: "Signatures"}}, + &publicPGPKeyReport{baseReporter{num: 20, description: "Public PGP Key"}}, } } @@ -129,7 +129,7 @@ func main() { p := newProcessor(opts) - report, err := p.run(buildChecks(), domains) + report, err := p.run(buildReporters(), domains) errCheck(err) errCheck(writeReport(report, opts)) diff --git a/cmd/csaf_checker/processor.go b/cmd/csaf_checker/processor.go index f34dd4f..80935f6 100644 --- a/cmd/csaf_checker/processor.go +++ b/cmd/csaf_checker/processor.go @@ -21,7 +21,6 @@ import ( "io" "net/http" "net/url" - "sort" "strings" "github.com/PaesslerAG/gval" @@ -39,19 +38,24 @@ type processor struct { pmd256 []byte pmd interface{} keys []*crypto.KeyRing - badHashes []string - badSignatures []string + + badHashes []string + badPGPs []string + badSignatures []string + badProviderMetadatas []string + badSecurity []string + badIntegrity []string builder gval.Language exprs map[string]gval.Evaluable } -type check interface { - executionOrder() int - run(*processor, string) error +type Reporter interface { report(*processor, *Domain) } +var errContinue = errors.New("continue") + func newProcessor(opts *options) *processor { return &processor{ opts: opts, @@ -76,28 +80,29 @@ func (p *processor) clean() { p.pmd256 = nil p.pmd = nil p.keys = nil - p.badSignatures = nil + p.badHashes = nil + p.badPGPs = nil + p.badSignatures = nil + p.badProviderMetadatas = nil + p.badSecurity = nil + p.badIntegrity = nil } -func (p *processor) run(checks []check, domains []string) (*Report, error) { +func (p *processor) run(reporter []Reporter, domains []string) (*Report, error) { var report Report - execs := make([]check, len(checks)) - copy(execs, checks) - sort.SliceStable(execs, func(i, j int) bool { - return execs[i].executionOrder() < execs[j].executionOrder() - }) - +domainsLoop: for _, d := range domains { - for _, ch := range execs { - if err := ch.run(p, d); err != nil { - return nil, err + if err := p.checkDomain(d); err != nil { + if err == errContinue { + continue domainsLoop } + return nil, err } domain := &Domain{Name: d} - for _, ch := range checks { + for _, ch := range reporter { ch.report(p, domain) } report.Domains = append(report.Domains, domain) @@ -107,6 +112,15 @@ func (p *processor) run(checks []check, domains []string) (*Report, error) { return &report, nil } +func (p *processor) checkDomain(domain string) error { + + // TODO: Implement me! + if err := p.checkProviderMetadata(domain); err != nil && err != errContinue { + return err + } + return nil +} + func (p *processor) jsonPath(expr string) (interface{}, error) { if p.pmd == nil { return nil, errors.New("no provider metadata loaded") @@ -171,14 +185,22 @@ func (p *processor) httpClient() *http.Client { return &client } -func (p *processor) addBadHash(format string, args ...interface{}) { +func (p *processor) badHash(format string, args ...interface{}) { p.badHashes = append(p.badHashes, fmt.Sprintf(format, args...)) } -func (p *processor) addBadSignature(format string, args ...interface{}) { +func (p *processor) badSignature(format string, args ...interface{}) { p.badSignatures = append(p.badSignatures, fmt.Sprintf(format, args...)) } +func (p *processor) badProviderMetadata(format string, args ...interface{}) { + p.badProviderMetadatas = append(p.badProviderMetadatas, fmt.Sprintf(format, args...)) +} + +func (p *processor) badPGP(format string, args ...interface{}) { + p.badPGPs = append(p.badPGPs, fmt.Sprintf(format, args...)) +} + func (p *processor) integrity( files []string, base string, @@ -250,11 +272,11 @@ func (p *processor) integrity( hashFile := u + "." + x.ext p.checkTLS(hashFile) if res, err = client.Get(hashFile); err != nil { - p.addBadHash("Fetching %s failed: %v.", hashFile, err) + p.badHash("Fetching %s failed: %v.", hashFile, err) continue } if res.StatusCode != http.StatusOK { - p.addBadHash("Fetching %s failed: Status code %d (%s)", + p.badHash("Fetching %s failed: Status code %d (%s)", hashFile, res.StatusCode, res.Status) continue } @@ -263,15 +285,15 @@ func (p *processor) integrity( return hashFromReader(res.Body) }() if err != nil { - p.addBadHash("Reading %s failed: %v.", hashFile, err) + p.badHash("Reading %s failed: %v.", hashFile, err) continue } if len(h) == 0 { - p.addBadHash("No hash found in %s.", hashFile) + p.badHash("No hash found in %s.", hashFile) continue } if !bytes.Equal(h, x.hash) { - p.addBadHash("%s hash of %s does not match %s.", + p.badHash("%s hash of %s does not match %s.", strings.ToUpper(x.ext), u, hashFile) } } @@ -281,11 +303,11 @@ func (p *processor) integrity( p.checkTLS(sigFile) if res, err = client.Get(sigFile); err != nil { - p.addBadSignature("Fetching %s failed: %v.", sigFile, err) + p.badSignature("Fetching %s failed: %v.", sigFile, err) continue } if res.StatusCode != http.StatusOK { - p.addBadSignature("Fetching %s failed: status code %d (%s)", + p.badSignature("Fetching %s failed: status code %d (%s)", sigFile, res.StatusCode, res.Status) continue } @@ -299,7 +321,7 @@ func (p *processor) integrity( return crypto.NewPGPSignatureFromArmored(string(all)) }() if err != nil { - p.addBadSignature("Loading signature from %s failed: %v.", + p.badSignature("Loading signature from %s failed: %v.", sigFile, err) continue } @@ -315,7 +337,7 @@ func (p *processor) integrity( } } if !verified { - p.addBadSignature("Signature of %s could not be verified.", u) + p.badSignature("Signature of %s could not be verified.", u) } } } @@ -327,13 +349,13 @@ func (p *processor) processFeed(feed string, lg func(string, ...interface{})) er client := p.httpClient() res, err := client.Get(feed) if err != nil { - lg("Cannot fetch feed %s: %v.", feed, err) - return nil + lg("Cannot fetch feed %s: %v", feed, err) + return errContinue } if res.StatusCode != http.StatusOK { lg("Fetching %s failed. Status code %d (%s)", feed, res.StatusCode, res.Status) - return nil + return errContinue } rfeed, err := func() (*csaf.ROLIEFeed, error) { defer res.Body.Close() @@ -341,11 +363,12 @@ func (p *processor) processFeed(feed string, lg func(string, ...interface{})) er }() if err != nil { lg("Loading ROLIE feed failed: %v.", err) - return nil + return errContinue } base, err := basePath(feed) if err != nil { - return err + lg("Bad base path: %v", err) + return errContinue } // Extract the CSAF files from feed. @@ -380,7 +403,7 @@ func (p *processor) processFeeds( } feedURL := base.ResolveReference(up).String() p.checkTLS(feedURL) - if err := p.processFeed(feedURL, lg); err != nil { + if err := p.processFeed(feedURL, lg); err != nil && err != errContinue { return err } } @@ -402,20 +425,25 @@ func (p *processor) checkCSAFs(domain string, lg func(string, ...interface{})) e var feeds [][]csaf.Feed if err := util.ReMarshalJSON(&feeds, rolie); err != nil { lg("ROLIE feeds are not compatible: %v.", err) - return nil + goto noRolie } if err := p.processFeeds(domain, feeds, lg); err != nil { + if err == errContinue { + goto noRolie + } return err } - } else { - // No rolie feeds - // TODO: Implement me! } +noRolie: + + // No rolie feeds + // TODO: Implement me! + return nil } -func (p *processor) checkProviderMetadata(domain string, lg func(string, ...interface{})) error { +func (p *processor) checkProviderMetadata(domain string) error { client := p.httpClient() @@ -423,14 +451,14 @@ func (p *processor) checkProviderMetadata(domain string, lg func(string, ...inte res, err := client.Get(url) if err != nil { - lg("Fetching %s: %v.", url, err) - return err + p.badProviderMetadata("Fetching %s: %v.", url, err) + return errContinue } if res.StatusCode != http.StatusOK { - lg("Fetching %s failed. Status code: %d (%s).", + p.badProviderMetadata("Fetching %s failed. Status code: %d (%s)", url, res.StatusCode, res.Status) - return errors.New("Cannot load provider-metadata.json") + return errContinue } // Calculate checksum for later comparison. @@ -441,8 +469,8 @@ func (p *processor) checkProviderMetadata(domain string, lg func(string, ...inte tee := io.TeeReader(res.Body, hash) return json.NewDecoder(tee).Decode(&p.pmd) }(); err != nil { - lg("Decoding JSON failed: %v.", err) - return err + p.badProviderMetadata("Decoding JSON failed: %v", err) + return errContinue } p.pmd256 = hash.Sum(nil) @@ -452,9 +480,9 @@ func (p *processor) checkProviderMetadata(domain string, lg func(string, ...inte return err } if len(errors) > 0 { - lg("Validating against JSON schema failed:") + p.badProviderMetadata("Validating against JSON schema failed:") for _, msg := range errors { - lg(strings.ReplaceAll(msg, `%`, `%%`)) + p.badProviderMetadata(strings.ReplaceAll(msg, `%`, `%%`)) } } return nil @@ -467,13 +495,14 @@ func (p *processor) checkSecurity(domain string, lg func(string, ...interface{}) path := "https://" + domain + "/.well-known/security.txt" res, err := client.Get(path) if err != nil { - return err + lg("Fetchinig %s failed: %v", err) + return errContinue } if res.StatusCode != http.StatusOK { lg("Fetching %s failed. Status code %d (%s)", path, res.StatusCode, res.Status) - return errors.New("fetching security.txt failed") + return errContinue } u, err := func() (string, error) { @@ -489,42 +518,42 @@ func (p *processor) checkSecurity(domain string, lg func(string, ...interface{}) }() if err != nil { lg("Error while reading security.txt: %v", err) - return err + return errContinue } if u == "" { lg("No CSAF line found in security.txt.") - return errors.New("no CSAF line in security.txt") + return errContinue } // Try to load up, err := url.Parse(u) if err != nil { - lg("CSAF URL '%s' invalid: %v.", u, err.Error()) - return err + lg("CSAF URL '%s' invalid: %v", u, err) + return errContinue } base, err := url.Parse("https://" + domain + "/.well-known/") if err != nil { return err } - ur := base.ResolveReference(up) - u = ur.String() + + u = base.ResolveReference(up).String() p.checkTLS(u) if res, err = client.Get(u); err != nil { lg("Cannot fetch %s from security.txt: %v", u, err) - return nil + return errContinue } if res.StatusCode != http.StatusOK { - lg("Fetching %s failed. Status code %d (%s).", + lg("Fetching %s failed. Status code %d (%s)", u, res.StatusCode, res.Status) - return nil + return errContinue } defer res.Body.Close() // Compare checksums to already read provider-metadata.json. h := sha256.New() if _, err := io.Copy(h, res.Body); err != nil { - lg("Reading %s failed: %v.", u, err) - return nil + lg("Reading %s failed: %v", u, err) + return errContinue } if !bytes.Equal(h.Sum(nil), p.pmd256) { @@ -533,3 +562,84 @@ func (p *processor) checkSecurity(domain string, lg func(string, ...interface{}) return nil } + +func (p *processor) checkPGPKeys(domain string, lg func(string, ...interface{})) error { + + src, err := p.jsonPath("$.pgp_keys") + if err != nil { + lg("No PGP keys found: %v.", err) + return errContinue + } + + var keys []csaf.PGPKey + if err := util.ReMarshalJSON(&keys, src); err != nil { + lg("PGP keys invalid: %v.", err) + return errContinue + } + + if len(keys) == 0 { + lg("No PGP keys found.") + return errContinue + } + + // Try to load + + client := p.httpClient() + + base, err := url.Parse("https://" + domain + "/.well-known/csaf/provider-metadata.json") + if err != nil { + return err + } + + for i := range keys { + key := &keys[i] + if key.URL == nil { + lg("Missing URL for fingerprint %x.", key.Fingerprint) + continue + } + up, err := url.Parse(*key.URL) + if err != nil { + lg("Invalid URL '%s': %v", *key.URL, err) + continue + } + + u := base.ResolveReference(up).String() + p.checkTLS(u) + + res, err := client.Get(u) + if err != nil { + lg("Fetching PGP key %s failed: %v.", u, err) + continue + } + if res.StatusCode != http.StatusOK { + lg("Fetching PGP key %s status code: %d (%s)", u, res.StatusCode, res.Status) + continue + } + + ckey, err := func() (*crypto.Key, error) { + defer res.Body.Close() + return crypto.NewKeyFromArmoredReader(res.Body) + }() + + if err != nil { + lg("Reading PGP key %s failed: %v", u, err) + continue + } + + if ckey.GetFingerprint() != string(key.Fingerprint) { + lg("Fingerprint of PGP key %s do not match remotely loaded.", u) + continue + } + keyring, err := crypto.NewKeyRing(ckey) + if err != nil { + lg("Creating key ring for %s failed: %v.", u, err) + continue + } + p.keys = append(p.keys, keyring) + } + + if len(p.keys) == 0 { + lg("No PGP keys loaded.") + } + return nil +} diff --git a/cmd/csaf_checker/report.go b/cmd/csaf_checker/report.go index b90a4c7..8313f64 100644 --- a/cmd/csaf_checker/report.go +++ b/cmd/csaf_checker/report.go @@ -25,3 +25,7 @@ type Domain struct { type Report struct { Domains []*Domain `json:"domains,omitempty"` } + +func (r *Requirement) message(msg ...string) { + r.Messages = append(r.Messages, msg...) +}