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

Split code to more files. Mainly move processor code to its own file.

This commit is contained in:
Sascha L. Teichmann 2021-12-15 12:14:31 +01:00
parent b5c5d8db4b
commit f2dd5a89a7
3 changed files with 453 additions and 421 deletions

View file

@ -11,433 +11,20 @@ package main
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"context"
"crypto/sha256" "crypto/sha256"
"crypto/sha512"
"crypto/tls"
"encoding/hex"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"hash"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"regexp"
"sort" "sort"
"strings" "strings"
"github.com/PaesslerAG/gval"
"github.com/PaesslerAG/jsonpath"
"github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/csaf-poc/csaf_distribution/csaf" "github.com/csaf-poc/csaf_distribution/csaf"
"github.com/csaf-poc/csaf_distribution/util" "github.com/csaf-poc/csaf_distribution/util"
) )
type processor struct {
opts *options
redirects map[string]string
noneTLS map[string]struct{}
alreadyChecked map[string]struct{}
pmd256 []byte
pmd interface{}
builder gval.Language
keys []*crypto.KeyRing
badHashes []string
badSignatures []string
}
type check interface {
executionOrder() int
run(*processor, string) error
report(*processor, *Domain)
}
func newProcessor(opts *options) *processor {
return &processor{
opts: opts,
redirects: map[string]string{},
noneTLS: map[string]struct{}{},
alreadyChecked: map[string]struct{}{},
builder: gval.Full(jsonpath.Language()),
}
}
func (p *processor) clean() {
for k := range p.redirects {
delete(p.redirects, k)
}
for k := range p.noneTLS {
delete(p.noneTLS, k)
}
for k := range p.alreadyChecked {
delete(p.alreadyChecked, k)
}
p.pmd256 = nil
p.pmd = nil
p.keys = nil
p.badSignatures = nil
p.badHashes = nil
}
func (p *processor) run(checks []check, 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()
})
for _, d := range domains {
for _, ch := range execs {
if err := ch.run(p, d); err != nil {
return nil, err
}
}
domain := &Domain{Name: d}
for _, ch := range checks {
ch.report(p, domain)
}
report.Domains = append(report.Domains, domain)
p.clean()
}
return &report, nil
}
func (p *processor) jsonPath(expr string) (interface{}, error) {
if p.pmd == nil {
return nil, errors.New("no provider metadata loaded")
}
eval, err := p.builder.NewEvaluable(expr)
if err != nil {
return nil, err
}
return eval(context.Background(), p.pmd)
}
func (p *processor) checkTLS(u string) {
if x, err := url.Parse(u); err == nil && x.Scheme != "https" {
p.noneTLS[u] = struct{}{}
}
}
func (p *processor) markChecked(s string) bool {
if _, ok := p.alreadyChecked[s]; ok {
return true
}
p.alreadyChecked[s] = struct{}{}
return false
}
func (p *processor) checkRedirect(r *http.Request, via []*http.Request) error {
var path strings.Builder
for i, v := range via {
if i > 0 {
path.WriteString(", ")
}
path.WriteString(v.URL.String())
}
url := r.URL.String()
p.checkTLS(url)
p.redirects[url] = path.String()
if len(via) > 10 {
return errors.New("Too many redirections")
}
return nil
}
func (p *processor) httpClient() *http.Client {
client := http.Client{
CheckRedirect: p.checkRedirect,
}
if p.opts.Insecure {
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
}
return &client
}
func (p *processor) addBadHash(format string, args ...interface{}) {
p.badHashes = append(p.badHashes, fmt.Sprintf(format, args...))
}
func (p *processor) addBadSignature(format string, args ...interface{}) {
p.badSignatures = append(p.badSignatures, fmt.Sprintf(format, args...))
}
func (p *processor) integrity(
files []string,
base string,
lg func(string, ...interface{}),
) error {
b, err := url.Parse(base)
if err != nil {
return err
}
client := p.httpClient()
for _, f := range files {
fp, err := url.Parse(f)
if err != nil {
return err
}
u := b.ResolveReference(fp).String()
if p.markChecked(u) {
continue
}
p.checkTLS(u)
res, err := client.Get(u)
if err != nil {
lg("Fetching %s failed: %v.", u, err)
continue
}
if res.StatusCode != http.StatusOK {
lg("Fetching %s failed: Status code %d (%s)",
u, res.StatusCode, res.Status)
continue
}
data, err := func() ([]byte, error) {
defer res.Body.Close()
return io.ReadAll(res.Body)
}()
if err != nil {
lg("Reading %s failed: %v", u, err)
continue
}
var doc interface{}
if err := json.Unmarshal(data, &doc); err != nil {
lg("Failed to unmarshal %s: %v", u, err)
continue
}
errors, err := csaf.ValidateCSAF(doc)
if err != nil {
lg("Failed to validate %s: %v", u, err)
continue
}
if len(errors) > 0 {
lg("CSAF file %s has %d validation errors.", u, len(errors))
}
// Check hashes
for _, x := range []struct {
ext string
hash func() hash.Hash
}{
{"sha256", sha256.New},
{"sha512", sha512.New},
} {
hashFile := u + "." + x.ext
p.checkTLS(hashFile)
if res, err = client.Get(hashFile); err != nil {
p.addBadHash("Fetching %s failed: %v.", hashFile, err)
continue
}
if res.StatusCode != http.StatusOK {
p.addBadHash("Fetching %s failed: Status code %d (%s)",
hashFile, res.StatusCode, res.Status)
continue
}
h, err := func() ([]byte, error) {
defer res.Body.Close()
return hashFromReader(res.Body)
}()
if err != nil {
p.addBadHash("Reading %s failed: %v.", hashFile, err)
continue
}
if len(h) == 0 {
p.addBadHash("No hash found in %s.", hashFile)
continue
}
orig := x.hash()
if _, err := orig.Write(data); err != nil {
p.addBadHash("%s hashing of %s failed: %v.",
strings.ToUpper(x.ext), u, err)
continue
}
if !bytes.Equal(h, orig.Sum(nil)) {
p.addBadHash("%s hash of %s does not match %s.",
strings.ToUpper(x.ext), u, hashFile)
}
}
// Check signature
sigFile := u + ".asc"
p.checkTLS(sigFile)
if res, err = client.Get(sigFile); err != nil {
p.addBadSignature("Fetching %s failed: %v.", sigFile, err)
continue
}
if res.StatusCode != http.StatusOK {
p.addBadSignature("Fetching %s failed: status code %d (%s)",
sigFile, res.StatusCode, res.Status)
continue
}
sig, err := func() (*crypto.PGPSignature, error) {
defer res.Body.Close()
all, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
return crypto.NewPGPSignatureFromArmored(string(all))
}()
if err != nil {
p.addBadSignature("Loading signature from %s failed: %v.",
sigFile, err)
continue
}
if len(p.keys) > 0 {
pm := crypto.NewPlainMessage(data)
t := crypto.GetUnixTime()
var verified bool
for _, key := range p.keys {
if err := key.VerifyDetached(pm, sig, t); err == nil {
verified = true
break
}
}
if !verified {
p.addBadSignature("Signature of %s could not be verified.", u)
}
}
}
return nil
}
var hexRe = regexp.MustCompile(`^([[:xdigit:]]+)`)
func hashFromReader(r io.Reader) ([]byte, error) {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
if m := hexRe.FindStringSubmatch(scanner.Text()); m != nil {
return hex.DecodeString(m[1])
}
}
return nil, scanner.Err()
}
func basePath(p string) (string, error) {
u, err := url.Parse(p)
if err != nil {
return "", err
}
ep := u.EscapedPath()
if idx := strings.LastIndexByte(ep, '/'); idx != -1 {
ep = ep[:idx]
}
user := u.User.String()
if user != "" {
user += "@"
}
return u.Scheme + "://" + user + u.Host + "/" + ep, nil
}
func (p *processor) processFeed(feed string, lg func(string, ...interface{})) error {
client := p.httpClient()
res, err := client.Get(feed)
if err != nil {
lg("Cannot fetch feed %s: %v.", feed, err)
return nil
}
if res.StatusCode != http.StatusOK {
lg("Fetching %s failed. Status code %d (%s)",
feed, res.StatusCode, res.Status)
return nil
}
rfeed, err := func() (*csaf.ROLIEFeed, error) {
defer res.Body.Close()
return csaf.LoadROLIEFeed(res.Body)
}()
if err != nil {
lg("Loading ROLIE feed failed: %v.", err)
return nil
}
base, err := basePath(feed)
if err != nil {
return err
}
// Extract the CSAF files from feed.
var files []string
for _, f := range rfeed.Entry {
for i := range f.Link {
files = append(files, f.Link[i].HRef)
}
}
return p.integrity(files, base, lg)
}
func (p *processor) processFeeds(
domain string,
feeds [][]csaf.Feed,
lg func(string, ...interface{}),
) error {
base, err := url.Parse("https://" + domain + "/.well-known/csaf/")
if err != nil {
return err
}
for i := range feeds {
for j := range feeds[i] {
feed := &feeds[i][j]
if feed.URL == nil {
continue
}
up, err := url.Parse(string(*feed.URL))
if err != nil {
lg("Invalid URL %s in feed: %v.", *feed.URL, err)
continue
}
feedURL := base.ResolveReference(up).String()
p.checkTLS(feedURL)
if err := p.processFeed(feedURL, lg); err != nil {
return err
}
}
}
return nil
}
func (p *processor) checkCSAFs(domain string, lg func(string, ...interface{})) error {
// Check for ROLIE
rolie, err := p.jsonPath("$.distributions[*].rolie.feeds")
if err != nil {
return err
}
fs, hasRolie := rolie.([]interface{})
hasRolie = hasRolie && len(fs) > 0
if hasRolie {
var feeds [][]csaf.Feed
if err := util.ReMarshalJSON(&feeds, rolie); err != nil {
lg("ROLIE feeds are not compatible: %v.", err)
return nil
}
if err := p.processFeeds(domain, feeds, lg); err != nil {
return err
}
} else {
// No rolie feeds
// TODO: Implement me!
}
return nil
}
type baseCheck struct { type baseCheck struct {
exec int exec int
num int num int
@ -732,14 +319,6 @@ func (sc *signaturesCheck) run(p *processor, _ string) error {
return nil return nil
} }
func reserialize(dst, src interface{}) error {
s, err := json.Marshal(src)
if err != nil {
return err
}
return json.Unmarshal(s, dst)
}
func (ppkc *publicPGPKeyCheck) run(p *processor, domain string) error { func (ppkc *publicPGPKeyCheck) run(p *processor, domain string) error {
src, err := p.jsonPath("$.pgp_keys") src, err := p.jsonPath("$.pgp_keys")

View file

@ -0,0 +1,407 @@
// This file is Free Software under the MIT License
// without warranty, see README.md and LICENSES/MIT.txt for details.
//
// SPDX-License-Identifier: MIT
//
// SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
// Software-Engineering: 2021 Intevation GmbH <https://intevation.de>
package main
import (
"bytes"
"context"
"crypto/sha256"
"crypto/sha512"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"hash"
"io"
"net/http"
"net/url"
"sort"
"strings"
"github.com/PaesslerAG/gval"
"github.com/PaesslerAG/jsonpath"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/csaf-poc/csaf_distribution/csaf"
"github.com/csaf-poc/csaf_distribution/util"
)
type processor struct {
opts *options
redirects map[string]string
noneTLS map[string]struct{}
alreadyChecked map[string]struct{}
pmd256 []byte
pmd interface{}
builder gval.Language
keys []*crypto.KeyRing
badHashes []string
badSignatures []string
}
type check interface {
executionOrder() int
run(*processor, string) error
report(*processor, *Domain)
}
func newProcessor(opts *options) *processor {
return &processor{
opts: opts,
redirects: map[string]string{},
noneTLS: map[string]struct{}{},
alreadyChecked: map[string]struct{}{},
builder: gval.Full(jsonpath.Language()),
}
}
func (p *processor) clean() {
for k := range p.redirects {
delete(p.redirects, k)
}
for k := range p.noneTLS {
delete(p.noneTLS, k)
}
for k := range p.alreadyChecked {
delete(p.alreadyChecked, k)
}
p.pmd256 = nil
p.pmd = nil
p.keys = nil
p.badSignatures = nil
p.badHashes = nil
}
func (p *processor) run(checks []check, 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()
})
for _, d := range domains {
for _, ch := range execs {
if err := ch.run(p, d); err != nil {
return nil, err
}
}
domain := &Domain{Name: d}
for _, ch := range checks {
ch.report(p, domain)
}
report.Domains = append(report.Domains, domain)
p.clean()
}
return &report, nil
}
func (p *processor) jsonPath(expr string) (interface{}, error) {
if p.pmd == nil {
return nil, errors.New("no provider metadata loaded")
}
eval, err := p.builder.NewEvaluable(expr)
if err != nil {
return nil, err
}
return eval(context.Background(), p.pmd)
}
func (p *processor) checkTLS(u string) {
if x, err := url.Parse(u); err == nil && x.Scheme != "https" {
p.noneTLS[u] = struct{}{}
}
}
func (p *processor) markChecked(s string) bool {
if _, ok := p.alreadyChecked[s]; ok {
return true
}
p.alreadyChecked[s] = struct{}{}
return false
}
func (p *processor) checkRedirect(r *http.Request, via []*http.Request) error {
var path strings.Builder
for i, v := range via {
if i > 0 {
path.WriteString(", ")
}
path.WriteString(v.URL.String())
}
url := r.URL.String()
p.checkTLS(url)
p.redirects[url] = path.String()
if len(via) > 10 {
return errors.New("Too many redirections")
}
return nil
}
func (p *processor) httpClient() *http.Client {
client := http.Client{
CheckRedirect: p.checkRedirect,
}
if p.opts.Insecure {
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
}
return &client
}
func (p *processor) addBadHash(format string, args ...interface{}) {
p.badHashes = append(p.badHashes, fmt.Sprintf(format, args...))
}
func (p *processor) addBadSignature(format string, args ...interface{}) {
p.badSignatures = append(p.badSignatures, fmt.Sprintf(format, args...))
}
func (p *processor) integrity(
files []string,
base string,
lg func(string, ...interface{}),
) error {
b, err := url.Parse(base)
if err != nil {
return err
}
client := p.httpClient()
for _, f := range files {
fp, err := url.Parse(f)
if err != nil {
return err
}
u := b.ResolveReference(fp).String()
if p.markChecked(u) {
continue
}
p.checkTLS(u)
res, err := client.Get(u)
if err != nil {
lg("Fetching %s failed: %v.", u, err)
continue
}
if res.StatusCode != http.StatusOK {
lg("Fetching %s failed: Status code %d (%s)",
u, res.StatusCode, res.Status)
continue
}
data, err := func() ([]byte, error) {
defer res.Body.Close()
return io.ReadAll(res.Body)
}()
if err != nil {
lg("Reading %s failed: %v", u, err)
continue
}
var doc interface{}
if err := json.Unmarshal(data, &doc); err != nil {
lg("Failed to unmarshal %s: %v", u, err)
continue
}
errors, err := csaf.ValidateCSAF(doc)
if err != nil {
lg("Failed to validate %s: %v", u, err)
continue
}
if len(errors) > 0 {
lg("CSAF file %s has %d validation errors.", u, len(errors))
}
// Check hashes
for _, x := range []struct {
ext string
hash func() hash.Hash
}{
{"sha256", sha256.New},
{"sha512", sha512.New},
} {
hashFile := u + "." + x.ext
p.checkTLS(hashFile)
if res, err = client.Get(hashFile); err != nil {
p.addBadHash("Fetching %s failed: %v.", hashFile, err)
continue
}
if res.StatusCode != http.StatusOK {
p.addBadHash("Fetching %s failed: Status code %d (%s)",
hashFile, res.StatusCode, res.Status)
continue
}
h, err := func() ([]byte, error) {
defer res.Body.Close()
return hashFromReader(res.Body)
}()
if err != nil {
p.addBadHash("Reading %s failed: %v.", hashFile, err)
continue
}
if len(h) == 0 {
p.addBadHash("No hash found in %s.", hashFile)
continue
}
orig := x.hash()
if _, err := orig.Write(data); err != nil {
p.addBadHash("%s hashing of %s failed: %v.",
strings.ToUpper(x.ext), u, err)
continue
}
if !bytes.Equal(h, orig.Sum(nil)) {
p.addBadHash("%s hash of %s does not match %s.",
strings.ToUpper(x.ext), u, hashFile)
}
}
// Check signature
sigFile := u + ".asc"
p.checkTLS(sigFile)
if res, err = client.Get(sigFile); err != nil {
p.addBadSignature("Fetching %s failed: %v.", sigFile, err)
continue
}
if res.StatusCode != http.StatusOK {
p.addBadSignature("Fetching %s failed: status code %d (%s)",
sigFile, res.StatusCode, res.Status)
continue
}
sig, err := func() (*crypto.PGPSignature, error) {
defer res.Body.Close()
all, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
return crypto.NewPGPSignatureFromArmored(string(all))
}()
if err != nil {
p.addBadSignature("Loading signature from %s failed: %v.",
sigFile, err)
continue
}
if len(p.keys) > 0 {
pm := crypto.NewPlainMessage(data)
t := crypto.GetUnixTime()
var verified bool
for _, key := range p.keys {
if err := key.VerifyDetached(pm, sig, t); err == nil {
verified = true
break
}
}
if !verified {
p.addBadSignature("Signature of %s could not be verified.", u)
}
}
}
return nil
}
func (p *processor) processFeed(feed string, lg func(string, ...interface{})) error {
client := p.httpClient()
res, err := client.Get(feed)
if err != nil {
lg("Cannot fetch feed %s: %v.", feed, err)
return nil
}
if res.StatusCode != http.StatusOK {
lg("Fetching %s failed. Status code %d (%s)",
feed, res.StatusCode, res.Status)
return nil
}
rfeed, err := func() (*csaf.ROLIEFeed, error) {
defer res.Body.Close()
return csaf.LoadROLIEFeed(res.Body)
}()
if err != nil {
lg("Loading ROLIE feed failed: %v.", err)
return nil
}
base, err := basePath(feed)
if err != nil {
return err
}
// Extract the CSAF files from feed.
var files []string
for _, f := range rfeed.Entry {
for i := range f.Link {
files = append(files, f.Link[i].HRef)
}
}
return p.integrity(files, base, lg)
}
func (p *processor) processFeeds(
domain string,
feeds [][]csaf.Feed,
lg func(string, ...interface{}),
) error {
base, err := url.Parse("https://" + domain + "/.well-known/csaf/")
if err != nil {
return err
}
for i := range feeds {
for j := range feeds[i] {
feed := &feeds[i][j]
if feed.URL == nil {
continue
}
up, err := url.Parse(string(*feed.URL))
if err != nil {
lg("Invalid URL %s in feed: %v.", *feed.URL, err)
continue
}
feedURL := base.ResolveReference(up).String()
p.checkTLS(feedURL)
if err := p.processFeed(feedURL, lg); err != nil {
return err
}
}
}
return nil
}
func (p *processor) checkCSAFs(domain string, lg func(string, ...interface{})) error {
// Check for ROLIE
rolie, err := p.jsonPath("$.distributions[*].rolie.feeds")
if err != nil {
return err
}
fs, hasRolie := rolie.([]interface{})
hasRolie = hasRolie && len(fs) > 0
if hasRolie {
var feeds [][]csaf.Feed
if err := util.ReMarshalJSON(&feeds, rolie); err != nil {
lg("ROLIE feeds are not compatible: %v.", err)
return nil
}
if err := p.processFeeds(domain, feeds, lg); err != nil {
return err
}
} else {
// No rolie feeds
// TODO: Implement me!
}
return nil
}

46
cmd/csaf_checker/util.go Normal file
View file

@ -0,0 +1,46 @@
// This file is Free Software under the MIT License
// without warranty, see README.md and LICENSES/MIT.txt for details.
//
// SPDX-License-Identifier: MIT
//
// SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
// Software-Engineering: 2021 Intevation GmbH <https://intevation.de>
package main
import (
"bufio"
"encoding/hex"
"io"
"net/url"
"regexp"
"strings"
)
var hexRe = regexp.MustCompile(`^([[:xdigit:]]+)`)
func hashFromReader(r io.Reader) ([]byte, error) {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
if m := hexRe.FindStringSubmatch(scanner.Text()); m != nil {
return hex.DecodeString(m[1])
}
}
return nil, scanner.Err()
}
func basePath(p string) (string, error) {
u, err := url.Parse(p)
if err != nil {
return "", err
}
ep := u.EscapedPath()
if idx := strings.LastIndexByte(ep, '/'); idx != -1 {
ep = ep[:idx]
}
user := u.User.String()
if user != "" {
user += "@"
}
return u.Scheme + "://" + user + u.Host + "/" + ep, nil
}