From 37d6692fa80a0065ce3e00b9b916486f8b9902ab Mon Sep 17 00:00:00 2001 From: "Sascha L. Teichmann" Date: Wed, 1 Dec 2021 01:05:24 +0100 Subject: [PATCH 01/12] Create security.txt in .wellknown folder. --- cmd/csaf_provider/files.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/csaf_provider/files.go b/cmd/csaf_provider/files.go index ed96913..aadc7c7 100644 --- a/cmd/csaf_provider/files.go +++ b/cmd/csaf_provider/files.go @@ -27,11 +27,11 @@ func ensureFolders(c *config) error { return err } - return createSecurity(c) + return createSecurity(c, wellknown) } -func createSecurity(c *config) error { - security := filepath.Join(c.Web, "security.txt") +func createSecurity(c *config, wellknown string) error { + security := filepath.Join(wellknown, "security.txt") if _, err := os.Stat(security); err != nil { if os.IsNotExist(err) { f, err := os.Create(security) From 70eb8875a42994b95cdf776c82679c16789335a9 Mon Sep 17 00:00:00 2001 From: "Sascha L. Teichmann" Date: Wed, 1 Dec 2021 19:27:46 +0100 Subject: [PATCH 02/12] Read publisher from config. --- cmd/csaf_provider/config.go | 28 ++++++--- cmd/csaf_provider/create.go | 99 ++++++++++++++++++++++++++++++++ cmd/csaf_provider/files.go | 76 ------------------------ cmd/csaf_provider/transaction.go | 41 +------------ csaf/models.go | 50 +++++++++++++--- 5 files changed, 163 insertions(+), 131 deletions(-) create mode 100644 cmd/csaf_provider/create.go diff --git a/cmd/csaf_provider/config.go b/cmd/csaf_provider/config.go index 4218da0..ac72e86 100644 --- a/cmd/csaf_provider/config.go +++ b/cmd/csaf_provider/config.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/BurntSushi/toml" + "github.com/csaf-poc/csaf_distribution/csaf" ) const ( @@ -17,14 +18,15 @@ const ( ) type config struct { - Key string `toml:"key"` - Folder string `toml:"folder"` - Web string `toml:"web"` - TLPs []tlp `toml:"tlps"` - UploadSignature bool `toml:"upload_signature"` - PGPURL string `toml:"pgp_url"` - Domain string `toml:"domain"` - NoPassphrase bool `toml:"no_passphrase"` + Key string `toml:"key"` + Folder string `toml:"folder"` + Web string `toml:"web"` + TLPs []tlp `toml:"tlps"` + UploadSignature bool `toml:"upload_signature"` + PGPURL string `toml:"pgp_url"` + Domain string `toml:"domain"` + NoPassphrase bool `toml:"no_passphrase"` + Publisher *csaf.Publisher `toml:"publisher"` } type tlp string @@ -58,6 +60,16 @@ func (cfg *config) GetPGPURL(key string) string { return strings.ReplaceAll(cfg.PGPURL, "${KEY}", key) } +func (cfg *config) modelTLPs() []csaf.TLPLabel { + tlps := make([]csaf.TLPLabel, len(cfg.TLPs)) + for _, t := range cfg.TLPs { + if t != tlpCSAF { + tlps = append(tlps, csaf.TLPLabel(t)) + } + } + return tlps +} + func loadConfig() (*config, error) { path := os.Getenv(configEnv) if path == "" { diff --git a/cmd/csaf_provider/create.go b/cmd/csaf_provider/create.go new file mode 100644 index 0000000..3641a47 --- /dev/null +++ b/cmd/csaf_provider/create.go @@ -0,0 +1,99 @@ +package main + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/csaf-poc/csaf_distribution/csaf" +) + +func ensureFolders(c *config) error { + + wellknown := filepath.Join(c.Web, ".well-known") + wellknownCSAF := filepath.Join(wellknown, "csaf") + + if err := createWellknown(wellknownCSAF); err != nil { + return err + } + + if err := createFeedFolders(c, wellknownCSAF); err != nil { + return err + } + + if err := createProviderMetadata(c, wellknownCSAF); err != nil { + return err + } + + return createSecurity(c, wellknown) +} + +func createWellknown(wellknown string) error { + st, err := os.Stat(wellknown) + if err != nil { + if os.IsNotExist(err) { + return os.MkdirAll(wellknown, 0755) + } + return err + } + if !st.IsDir() { + return errors.New(".well-known/csaf is not a directory") + } + return nil +} + +func createFeedFolders(c *config, wellknown string) error { + for _, t := range c.TLPs { + if t == tlpCSAF { + continue + } + tlpLink := filepath.Join(wellknown, string(t)) + if _, err := filepath.EvalSymlinks(tlpLink); err != nil { + if os.IsNotExist(err) { + tlpFolder := filepath.Join(c.Folder, string(t)) + if tlpFolder, err = mkUniqDir(tlpFolder); err != nil { + return err + } + if err = os.Symlink(tlpFolder, tlpLink); err != nil { + return err + } + } else { + return err + } + } + } + return nil +} + +func createSecurity(c *config, wellknown string) error { + security := filepath.Join(wellknown, "security.txt") + if _, err := os.Stat(security); err != nil { + if os.IsNotExist(err) { + f, err := os.Create(security) + if err != nil { + return err + } + fmt.Fprintf( + f, "CSAF: %s/.well-known/csaf/provider-metadata.json\n", + c.Domain) + return f.Close() + } + return err + } + return nil +} + +func createProviderMetadata(c *config, wellknownCSAF string) error { + path := filepath.Join(wellknownCSAF, "provider-metadata.json") + _, err := os.Stat(path) + if err == nil { + return nil + } + if !os.IsNotExist(err) { + return err + } + pm := csaf.NewProviderMetadataDomain(c.Domain, c.modelTLPs()) + pm.Publisher = c.Publisher + return saveToFile(path, pm) +} diff --git a/cmd/csaf_provider/files.go b/cmd/csaf_provider/files.go index aadc7c7..3fb5454 100644 --- a/cmd/csaf_provider/files.go +++ b/cmd/csaf_provider/files.go @@ -4,7 +4,6 @@ import ( "bytes" "crypto/sha256" "crypto/sha512" - "errors" "fmt" "hash" "io" @@ -16,81 +15,6 @@ import ( "time" ) -func ensureFolders(c *config) error { - - wellknown, err := createWellknown(c) - if err != nil { - return err - } - - if err := createFeedFolders(c, wellknown); err != nil { - return err - } - - return createSecurity(c, wellknown) -} - -func createSecurity(c *config, wellknown string) error { - security := filepath.Join(wellknown, "security.txt") - if _, err := os.Stat(security); err != nil { - if os.IsNotExist(err) { - f, err := os.Create(security) - if err != nil { - return err - } - fmt.Fprintf( - f, "CSAF: %s/.well-known/csaf/provider-metadata.json\n", - c.Domain) - return f.Close() - } - return err - } - return nil -} - -func createFeedFolders(c *config, wellknown string) error { - for _, t := range c.TLPs { - if t == tlpCSAF { - continue - } - tlpLink := filepath.Join(wellknown, string(t)) - if _, err := filepath.EvalSymlinks(tlpLink); err != nil { - if os.IsNotExist(err) { - tlpFolder := filepath.Join(c.Folder, string(t)) - if tlpFolder, err = mkUniqDir(tlpFolder); err != nil { - return err - } - if err = os.Symlink(tlpFolder, tlpLink); err != nil { - return err - } - } else { - return err - } - } - } - return nil -} - -func createWellknown(c *config) (string, error) { - wellknown := filepath.Join(c.Web, ".well-known", "csaf") - - st, err := os.Stat(wellknown) - if err != nil { - if os.IsNotExist(err) { - if err := os.MkdirAll(wellknown, 0755); err != nil { - return "", err - } - } else { - return "", err - } - } else { - if !st.IsDir() { - return "", errors.New(".well-known/csaf is not a directory") - } - } - return wellknown, nil -} - func deepCopy(dst, src string) error { stack := []string{dst, src} diff --git a/cmd/csaf_provider/transaction.go b/cmd/csaf_provider/transaction.go index 6663d09..07d601d 100644 --- a/cmd/csaf_provider/transaction.go +++ b/cmd/csaf_provider/transaction.go @@ -3,49 +3,10 @@ package main import ( "os" "path/filepath" - "strings" "github.com/csaf-poc/csaf_distribution/csaf" ) -func newProviderMetadata(cfg *config) *csaf.ProviderMetadata { - - pmd := csaf.NewProviderMetadata( - cfg.Domain + "/.wellknown/csaf/provider-metadata.json") - - // Register feeds. - - var feeds []csaf.Feed - - for _, t := range cfg.TLPs { - if t == tlpCSAF { - continue - } - var ( - ts = string(t) - feedName = "csaf-feed-tlp-" + ts + ".json" - feedURL = csaf.JSONURL( - cfg.Domain + "/.well-known/csaf/" + ts + "/" + feedName) - tlpLabel = csaf.TLPLabel(strings.ToUpper(ts)) - ) - feeds = append(feeds, csaf.Feed{ - Summary: "TLP:" + string(tlpLabel) + " advisories", - TLPLabel: &tlpLabel, - URL: &feedURL, - }) - } - - if len(feeds) > 0 { - pmd.Distributions = []csaf.Distribution{{ - Rolie: []csaf.ROLIE{{ - Feeds: feeds, - }}, - }} - } - - return pmd -} - func doTransaction( cfg *config, t tlp, @@ -60,7 +21,7 @@ func doTransaction( f, err := os.Open(metadata) if err != nil { if os.IsNotExist(err) { - return newProviderMetadata(cfg), nil + return csaf.NewProviderMetadataDomain(cfg.Domain, cfg.modelTLPs()), nil } return nil, err } diff --git a/csaf/models.go b/csaf/models.go index 29a9d1a..69c5b67 100644 --- a/csaf/models.go +++ b/csaf/models.go @@ -102,11 +102,11 @@ var csafCategoryPattern = alternativesUnmarshal( // Publisher is the publisher of the feed. type Publisher struct { - Category *Category `json:"category"` // required - Name *string `json:"name"` // required - Namespace *string `json:"namespace"` // required - ContactDetails string `json:"contact_details,omitempty"` - IssuingAuthority string `json:"issuing_authority,omitempty"` + Category *Category `json:"category" toml:"category"` // required + Name *string `json:"name" toml:"name"` // required + Namespace *string `json:"namespace" toml:"namespace"` // required + ContactDetails string `json:"contact_details,omitempty" toml:"contact_details"` + IssuingAuthority string `json:"issuing_authority,omitempty" toml:"issuing_authority"` } // MetadataVersion is the metadata version of the feed. @@ -148,8 +148,8 @@ type ProviderMetadata struct { MetadataVersion *MetadataVersion `json:"metadata_version"` // required MirrorOnCSAFAggregators *bool `json:"mirror_on_CSAF_aggregators"` // required PGPKeys []PGPKey `json:"pgp_keys,omitempty"` - Publisher *Publisher `json:"publisher"` // required - Role *MetadataRole `json:"role"` // required + Publisher *Publisher `json:"publisher,omitempty"` // required + Role *MetadataRole `json:"role"` // required } func patternUnmarshal(pattern string) func([]byte) (string, error) { @@ -384,6 +384,42 @@ func NewProviderMetadata(canonicalURL string) *ProviderMetadata { return pm } +// NewProviderMetadataDomain creates a new provider with the given URL +// and tlps feeds. +func NewProviderMetadataDomain(domain string, tlps []TLPLabel) *ProviderMetadata { + + pm := NewProviderMetadata( + domain + "/.wellknown/csaf/provider-metadata.json") + + // Register feeds. + + var feeds []Feed + + for _, t := range tlps { + var ( + ts = strings.ToLower(string(t)) + feedName = "csaf-feed-tlp-" + ts + ".json" + feedURL = JSONURL( + domain + "/.well-known/csaf/" + ts + "/" + feedName) + ) + feeds = append(feeds, Feed{ + Summary: "TLP:" + string(t) + " advisories", + TLPLabel: &t, + URL: &feedURL, + }) + } + + if len(feeds) > 0 { + pm.Distributions = []Distribution{{ + Rolie: []ROLIE{{ + Feeds: feeds, + }}, + }} + } + + return pm +} + // Save saves a metadata provider to a writer. func (pmd *ProviderMetadata) Save(w io.Writer) error { enc := json.NewEncoder(w) From f2d8cd1e90ff24a1375e54154062cc38557f8361 Mon Sep 17 00:00:00 2001 From: "Sascha L. Teichmann" Date: Wed, 1 Dec 2021 19:35:19 +0100 Subject: [PATCH 03/12] Work in bernhardreiter's PR#4. --- cmd/csaf_provider/config.go | 12 ++++++------ cmd/csaf_provider/controller.go | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/csaf_provider/config.go b/cmd/csaf_provider/config.go index ac72e86..75b006c 100644 --- a/cmd/csaf_provider/config.go +++ b/cmd/csaf_provider/config.go @@ -14,7 +14,7 @@ const ( defaultConfigPath = "/usr/lib/casf/config.toml" defaultFolder = "/var/www/" defaultWeb = "/var/www/html" - defaultPGPURL = "http://pgp.mit.edu/pks/lookup?search=${KEY}&op=index" + defaultOpenPGPURL = "https://openpgp.circl.lu/pks/lookup?search=${KEY}&op=index" ) type config struct { @@ -23,7 +23,7 @@ type config struct { Web string `toml:"web"` TLPs []tlp `toml:"tlps"` UploadSignature bool `toml:"upload_signature"` - PGPURL string `toml:"pgp_url"` + OpenPGPURL string `toml:"openpgp_url"` Domain string `toml:"domain"` NoPassphrase bool `toml:"no_passphrase"` Publisher *csaf.Publisher `toml:"publisher"` @@ -56,8 +56,8 @@ func (t *tlp) UnmarshalText(text []byte) error { return fmt.Errorf("invalid config TLP value: %v", string(text)) } -func (cfg *config) GetPGPURL(key string) string { - return strings.ReplaceAll(cfg.PGPURL, "${KEY}", key) +func (cfg *config) GetOpenPGPURL(key string) string { + return strings.ReplaceAll(cfg.OpenPGPURL, "${KEY}", key) } func (cfg *config) modelTLPs() []csaf.TLPLabel { @@ -98,8 +98,8 @@ func loadConfig() (*config, error) { cfg.TLPs = []tlp{tlpCSAF, tlpWhite, tlpGreen, tlpAmber, tlpRed} } - if cfg.PGPURL == "" { - cfg.PGPURL = defaultPGPURL + if cfg.OpenPGPURL == "" { + cfg.OpenPGPURL = defaultOpenPGPURL } return &cfg, nil diff --git a/cmd/csaf_provider/controller.go b/cmd/csaf_provider/controller.go index d982334..e693d93 100644 --- a/cmd/csaf_provider/controller.go +++ b/cmd/csaf_provider/controller.go @@ -339,7 +339,7 @@ func (c *controller) upload(rw http.ResponseWriter, r *http.Request) { // TODO: Check for conflicts. pmd.Publisher = ex.publisher - pmd.SetPGP(fingerprint, c.cfg.GetPGPURL(fingerprint)) + pmd.SetPGP(fingerprint, c.cfg.GetOpenPGPURL(fingerprint)) return nil }); err != nil { From 9cf4a7cb5c6bd32538c9b20f3405d2803f476496 Mon Sep 17 00:00:00 2001 From: "Sascha L. Teichmann" Date: Wed, 1 Dec 2021 20:16:09 +0100 Subject: [PATCH 04/12] Add OpenPGP key to provider metadata when generated at setup. --- cmd/csaf_provider/config.go | 10 ++++++++++ cmd/csaf_provider/controller.go | 11 +---------- cmd/csaf_provider/create.go | 9 +++++++++ 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/cmd/csaf_provider/config.go b/cmd/csaf_provider/config.go index 75b006c..dd94a17 100644 --- a/cmd/csaf_provider/config.go +++ b/cmd/csaf_provider/config.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/BurntSushi/toml" + "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/csaf-poc/csaf_distribution/csaf" ) @@ -70,6 +71,15 @@ func (cfg *config) modelTLPs() []csaf.TLPLabel { return tlps } +func (cfg *config) loadCryptoKey() (*crypto.Key, error) { + f, err := os.Open(cfg.Key) + if err != nil { + return nil, err + } + defer f.Close() + return crypto.NewKeyFromArmoredReader(f) +} + func loadConfig() (*config, error) { path := os.Getenv(configEnv) if path == "" { diff --git a/cmd/csaf_provider/controller.go b/cmd/csaf_provider/controller.go index e693d93..520dca9 100644 --- a/cmd/csaf_provider/controller.go +++ b/cmd/csaf_provider/controller.go @@ -112,19 +112,10 @@ func loadCSAF(r *http.Request) (string, []byte, error) { return cleanFileName(handler.Filename), buf.Bytes(), nil } -func (c *controller) loadCryptoKey() (*crypto.Key, error) { - f, err := os.Open(c.cfg.Key) - if err != nil { - return nil, err - } - defer f.Close() - return crypto.NewKeyFromArmoredReader(f) -} - func (c *controller) handleSignature(r *http.Request, data []byte) (string, string, error) { // Either way ... we need the key. - key, err := c.loadCryptoKey() + key, err := c.cfg.loadCryptoKey() if err != nil { return "", "", err } diff --git a/cmd/csaf_provider/create.go b/cmd/csaf_provider/create.go index 3641a47..0bc9127 100644 --- a/cmd/csaf_provider/create.go +++ b/cmd/csaf_provider/create.go @@ -95,5 +95,14 @@ func createProviderMetadata(c *config, wellknownCSAF string) error { } pm := csaf.NewProviderMetadataDomain(c.Domain, c.modelTLPs()) pm.Publisher = c.Publisher + + // Set OpenPGP key. + key, err := c.loadCryptoKey() + if err != nil { + return err + } + fingerprint := key.GetFingerprint() + pm.SetPGP(fingerprint, c.GetOpenPGPURL(fingerprint)) + return saveToFile(path, pm) } From fbe20dbf602994f8abac2bebdfeba6146d91fd18 Mon Sep 17 00:00:00 2001 From: "Sascha L. Teichmann" Date: Wed, 1 Dec 2021 20:51:39 +0100 Subject: [PATCH 05/12] Use Key ID instead of fingerprint in OpenPGP URL interpolation. --- cmd/csaf_provider/controller.go | 32 +++++++++++++++++--------------- cmd/csaf_provider/create.go | 4 ++-- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/cmd/csaf_provider/controller.go b/cmd/csaf_provider/controller.go index 520dca9..c441106 100644 --- a/cmd/csaf_provider/controller.go +++ b/cmd/csaf_provider/controller.go @@ -112,65 +112,66 @@ func loadCSAF(r *http.Request) (string, []byte, error) { return cleanFileName(handler.Filename), buf.Bytes(), nil } -func (c *controller) handleSignature(r *http.Request, data []byte) (string, string, error) { +func (c *controller) handleSignature( + r *http.Request, + data []byte, +) (string, *crypto.Key, error) { // Either way ... we need the key. key, err := c.cfg.loadCryptoKey() if err != nil { - return "", "", err + return "", nil, err } - fingerprint := key.GetFingerprint() - // Was the signature given via request? if c.cfg.UploadSignature { sigText := r.FormValue("signature") if sigText == "" { - return "", "", errors.New("missing signature in request") + return "", nil, errors.New("missing signature in request") } pgpSig, err := crypto.NewPGPSignatureFromArmored(sigText) if err != nil { - return "", "", err + return "", nil, err } // Use as public key signRing, err := crypto.NewKeyRing(key) if err != nil { - return "", "", err + return "", nil, err } if err := signRing.VerifyDetached( crypto.NewPlainMessage(data), pgpSig, crypto.GetUnixTime(), ); err != nil { - return "", "", err + return "", nil, err } - return sigText, fingerprint, nil + return sigText, key, nil } // Sign ourself if passwd := r.FormValue("passphrase"); !c.cfg.NoPassphrase && passwd != "" { if key, err = key.Unlock([]byte(passwd)); err != nil { - return "", "", err + return "", nil, err } } // Use as private key signRing, err := crypto.NewKeyRing(key) if err != nil { - return "", "", err + return "", nil, err } sig, err := signRing.SignDetached(crypto.NewPlainMessage(data)) if err != nil { - return "", "", err + return "", nil, err } armored, err := sig.GetArmored() - return armored, fingerprint, err + return armored, key, err } func (c *controller) upload(rw http.ResponseWriter, r *http.Request) { @@ -208,7 +209,7 @@ func (c *controller) upload(rw http.ResponseWriter, r *http.Request) { } } - armored, fingerprint, err := c.handleSignature(r, data) + armored, key, err := c.handleSignature(r, data) if err != nil { c.failed(rw, "upload.html", err) return @@ -330,7 +331,8 @@ func (c *controller) upload(rw http.ResponseWriter, r *http.Request) { // TODO: Check for conflicts. pmd.Publisher = ex.publisher - pmd.SetPGP(fingerprint, c.cfg.GetOpenPGPURL(fingerprint)) + keyID, fingerprint := key.GetHexKeyID(), key.GetFingerprint() + pmd.SetPGP(fingerprint, c.cfg.GetOpenPGPURL(keyID)) return nil }); err != nil { diff --git a/cmd/csaf_provider/create.go b/cmd/csaf_provider/create.go index 0bc9127..29a361d 100644 --- a/cmd/csaf_provider/create.go +++ b/cmd/csaf_provider/create.go @@ -101,8 +101,8 @@ func createProviderMetadata(c *config, wellknownCSAF string) error { if err != nil { return err } - fingerprint := key.GetFingerprint() - pm.SetPGP(fingerprint, c.GetOpenPGPURL(fingerprint)) + keyID, fingerprint := key.GetHexKeyID(), key.GetFingerprint() + pm.SetPGP(fingerprint, c.GetOpenPGPURL(keyID)) return saveToFile(path, pm) } From 45299f7e44f7fa34773297fada8bc58803925561 Mon Sep 17 00:00:00 2001 From: "Sascha L. Teichmann" Date: Wed, 1 Dec 2021 21:10:02 +0100 Subject: [PATCH 06/12] Only write provider metadata back if it is marked as dynamic in config. --- cmd/csaf_provider/config.go | 19 ++++++++------- cmd/csaf_provider/transaction.go | 42 +++++++++++++++++--------------- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/cmd/csaf_provider/config.go b/cmd/csaf_provider/config.go index dd94a17..12fce85 100644 --- a/cmd/csaf_provider/config.go +++ b/cmd/csaf_provider/config.go @@ -19,15 +19,16 @@ const ( ) type config struct { - Key string `toml:"key"` - Folder string `toml:"folder"` - Web string `toml:"web"` - TLPs []tlp `toml:"tlps"` - UploadSignature bool `toml:"upload_signature"` - OpenPGPURL string `toml:"openpgp_url"` - Domain string `toml:"domain"` - NoPassphrase bool `toml:"no_passphrase"` - Publisher *csaf.Publisher `toml:"publisher"` + Key string `toml:"key"` + Folder string `toml:"folder"` + Web string `toml:"web"` + TLPs []tlp `toml:"tlps"` + UploadSignature bool `toml:"upload_signature"` + OpenPGPURL string `toml:"openpgp_url"` + Domain string `toml:"domain"` + NoPassphrase bool `toml:"no_passphrase"` + DynamicProviderMetaData bool `toml:"dynamic_provider_metadata"` + Publisher *csaf.Publisher `toml:"publisher"` } type tlp string diff --git a/cmd/csaf_provider/transaction.go b/cmd/csaf_provider/transaction.go index 07d601d..2d0c782 100644 --- a/cmd/csaf_provider/transaction.go +++ b/cmd/csaf_provider/transaction.go @@ -59,29 +59,31 @@ func doTransaction( return err } - // Write back provider metadata. - newMetaName, newMetaFile, err := mkUniqFile(metadata) - if err != nil { - os.RemoveAll(newDir) - return err - } + // Write back provider metadata if its dynamic. + if cfg.DynamicProviderMetaData { + newMetaName, newMetaFile, err := mkUniqFile(metadata) + if err != nil { + os.RemoveAll(newDir) + return err + } - if err := pmd.Save(newMetaFile); err != nil { - newMetaFile.Close() - os.Remove(newMetaName) - os.RemoveAll(newDir) - return err - } + if err := pmd.Save(newMetaFile); err != nil { + newMetaFile.Close() + os.Remove(newMetaName) + os.RemoveAll(newDir) + return err + } - if err := newMetaFile.Close(); err != nil { - os.Remove(newMetaName) - os.RemoveAll(newDir) - return err - } + if err := newMetaFile.Close(); err != nil { + os.Remove(newMetaName) + os.RemoveAll(newDir) + return err + } - if err := os.Rename(newMetaName, metadata); err != nil { - os.RemoveAll(newDir) - return err + if err := os.Rename(newMetaName, metadata); err != nil { + os.RemoveAll(newDir) + return err + } } // Switch directories. From 5276cea0a0f7e57bfdc93e325261aadd293ff121 Mon Sep 17 00:00:00 2001 From: "Sascha L. Teichmann" Date: Wed, 1 Dec 2021 23:57:47 +0100 Subject: [PATCH 07/12] Simplified code. --- csaf/models.go | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/csaf/models.go b/csaf/models.go index 69c5b67..b511cf6 100644 --- a/csaf/models.go +++ b/csaf/models.go @@ -391,31 +391,31 @@ func NewProviderMetadataDomain(domain string, tlps []TLPLabel) *ProviderMetadata pm := NewProviderMetadata( domain + "/.wellknown/csaf/provider-metadata.json") + if len(tlps) == 0 { + return pm + } + // Register feeds. - var feeds []Feed + feeds := make([]Feed, len(tlps)) - for _, t := range tlps { - var ( - ts = strings.ToLower(string(t)) - feedName = "csaf-feed-tlp-" + ts + ".json" - feedURL = JSONURL( - domain + "/.well-known/csaf/" + ts + "/" + feedName) - ) - feeds = append(feeds, Feed{ + for i, t := range tlps { + lt := strings.ToLower(string(t)) + feed := "csaf-feed-tlp-" + lt + ".json" + url := JSONURL(domain + "/.well-known/csaf/" + lt + "/" + feed) + + feeds[i] = Feed{ Summary: "TLP:" + string(t) + " advisories", TLPLabel: &t, - URL: &feedURL, - }) + URL: &url, + } } - if len(feeds) > 0 { - pm.Distributions = []Distribution{{ - Rolie: []ROLIE{{ - Feeds: feeds, - }}, - }} - } + pm.Distributions = []Distribution{{ + Rolie: []ROLIE{{ + Feeds: feeds, + }}, + }} return pm } From 22c7da1ed162ee7779deded91de7e5c8ff2c9ba6 Mon Sep 17 00:00:00 2001 From: "Sascha L. Teichmann" Date: Thu, 2 Dec 2021 00:24:27 +0100 Subject: [PATCH 08/12] use io.WriterTo instead of custom save interface ti serialize metadata and rolie. --- cmd/csaf_provider/files.go | 8 ++------ cmd/csaf_provider/transaction.go | 2 +- csaf/models.go | 19 ++++++++++++++++--- csaf/rolie.go | 8 +++++--- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/cmd/csaf_provider/files.go b/cmd/csaf_provider/files.go index 3fb5454..50ba98f 100644 --- a/cmd/csaf_provider/files.go +++ b/cmd/csaf_provider/files.go @@ -138,16 +138,12 @@ func writeHashedFile(fname, name string, data []byte, armored string) error { return nil } -type saver interface { - Save(io.Writer) error -} - -func saveToFile(fname string, s saver) error { +func saveToFile(fname string, wt io.WriterTo) error { f, err1 := os.Create(fname) if err1 != nil { return err1 } - err1 = s.Save(f) + _, err1 = wt.WriteTo(f) err2 := f.Close() if err1 != nil { return err1 diff --git a/cmd/csaf_provider/transaction.go b/cmd/csaf_provider/transaction.go index 2d0c782..04f4d45 100644 --- a/cmd/csaf_provider/transaction.go +++ b/cmd/csaf_provider/transaction.go @@ -67,7 +67,7 @@ func doTransaction( return err } - if err := pmd.Save(newMetaFile); err != nil { + if _, err := pmd.WriteTo(newMetaFile); err != nil { newMetaFile.Close() os.Remove(newMetaName) os.RemoveAll(newDir) diff --git a/csaf/models.go b/csaf/models.go index b511cf6..dcdbcf2 100644 --- a/csaf/models.go +++ b/csaf/models.go @@ -420,11 +420,24 @@ func NewProviderMetadataDomain(domain string, tlps []TLPLabel) *ProviderMetadata return pm } -// Save saves a metadata provider to a writer. -func (pmd *ProviderMetadata) Save(w io.Writer) error { +type nWriter struct { + io.Writer + n int64 +} + +func (nw *nWriter) Write(p []byte) (int, error) { + n, err := nw.Write(p) + nw.n += int64(n) + return n, err +} + +// WriteTo saves a metadata provider to a writer. +func (pmd *ProviderMetadata) WriteTo(w io.Writer) (int64, error) { enc := json.NewEncoder(w) enc.SetIndent("", " ") - return enc.Encode(pmd) + nw := nWriter{w, 0} + err := enc.Encode(&nw) + return nw.n, err } // LoadProviderMetadata loads a metadata provider from a reader. diff --git a/csaf/rolie.go b/csaf/rolie.go index e49641f..78d8c63 100644 --- a/csaf/rolie.go +++ b/csaf/rolie.go @@ -68,11 +68,13 @@ func LoadROLIEFeed(r io.Reader) (*ROLIEFeed, error) { return &rf, nil } -// Save saves a ROLIE feed to a writer. -func (rf *ROLIEFeed) Save(w io.Writer) error { +// WriteTo saves a ROLIE feed to a writer. +func (rf *ROLIEFeed) WriteTo(w io.Writer) (int64, error) { enc := json.NewEncoder(w) enc.SetIndent("", " ") - return enc.Encode(rf) + nw := nWriter{w, 0} + err := enc.Encode(&nw) + return nw.n, err } // EntryByID looks up an entry by its ID. From bd8846baa672b8085c0349f4c5f210cf8f991992 Mon Sep 17 00:00:00 2001 From: "Sascha L. Teichmann" Date: Thu, 2 Dec 2021 00:41:43 +0100 Subject: [PATCH 09/12] Fixed stupid calling mistakes in new WriteTo methods. --- csaf/models.go | 8 ++++---- csaf/rolie.go | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/csaf/models.go b/csaf/models.go index dcdbcf2..4b66811 100644 --- a/csaf/models.go +++ b/csaf/models.go @@ -426,17 +426,17 @@ type nWriter struct { } func (nw *nWriter) Write(p []byte) (int, error) { - n, err := nw.Write(p) + n, err := nw.Writer.Write(p) nw.n += int64(n) return n, err } // WriteTo saves a metadata provider to a writer. func (pmd *ProviderMetadata) WriteTo(w io.Writer) (int64, error) { - enc := json.NewEncoder(w) - enc.SetIndent("", " ") nw := nWriter{w, 0} - err := enc.Encode(&nw) + enc := json.NewEncoder(&nw) + enc.SetIndent("", " ") + err := enc.Encode(pmd) return nw.n, err } diff --git a/csaf/rolie.go b/csaf/rolie.go index 78d8c63..b79c56a 100644 --- a/csaf/rolie.go +++ b/csaf/rolie.go @@ -70,10 +70,10 @@ func LoadROLIEFeed(r io.Reader) (*ROLIEFeed, error) { // WriteTo saves a ROLIE feed to a writer. func (rf *ROLIEFeed) WriteTo(w io.Writer) (int64, error) { - enc := json.NewEncoder(w) - enc.SetIndent("", " ") nw := nWriter{w, 0} - err := enc.Encode(&nw) + enc := json.NewEncoder(&nw) + enc.SetIndent("", " ") + err := enc.Encode(rf) return nw.n, err } From 048c0dce8918367cd89224ad599f98b3da391dce Mon Sep 17 00:00:00 2001 From: "Sascha L. Teichmann" Date: Thu, 2 Dec 2021 00:56:21 +0100 Subject: [PATCH 10/12] Added "0x" before key id in openpgp url to make search on key server happy. --- cmd/csaf_provider/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/csaf_provider/config.go b/cmd/csaf_provider/config.go index 12fce85..6868995 100644 --- a/cmd/csaf_provider/config.go +++ b/cmd/csaf_provider/config.go @@ -59,7 +59,7 @@ func (t *tlp) UnmarshalText(text []byte) error { } func (cfg *config) GetOpenPGPURL(key string) string { - return strings.ReplaceAll(cfg.OpenPGPURL, "${KEY}", key) + return strings.ReplaceAll(cfg.OpenPGPURL, "${KEY}", "0x"+key) } func (cfg *config) modelTLPs() []csaf.TLPLabel { From e5a6a8e2da804cb06700aa0d16039e3f583d9d00 Mon Sep 17 00:00:00 2001 From: "Sascha L. Teichmann" Date: Thu, 2 Dec 2021 01:36:57 +0100 Subject: [PATCH 11/12] Fixed TLP model conversion. Fixed wrong .well-known path --- cmd/csaf_provider/config.go | 4 ++-- csaf/models.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/csaf_provider/config.go b/cmd/csaf_provider/config.go index 6868995..c21f1b2 100644 --- a/cmd/csaf_provider/config.go +++ b/cmd/csaf_provider/config.go @@ -63,10 +63,10 @@ func (cfg *config) GetOpenPGPURL(key string) string { } func (cfg *config) modelTLPs() []csaf.TLPLabel { - tlps := make([]csaf.TLPLabel, len(cfg.TLPs)) + tlps := make([]csaf.TLPLabel, 0, len(cfg.TLPs)) for _, t := range cfg.TLPs { if t != tlpCSAF { - tlps = append(tlps, csaf.TLPLabel(t)) + tlps = append(tlps, csaf.TLPLabel(strings.ToUpper(string(t)))) } } return tlps diff --git a/csaf/models.go b/csaf/models.go index 4b66811..b5ebac4 100644 --- a/csaf/models.go +++ b/csaf/models.go @@ -389,7 +389,7 @@ func NewProviderMetadata(canonicalURL string) *ProviderMetadata { func NewProviderMetadataDomain(domain string, tlps []TLPLabel) *ProviderMetadata { pm := NewProviderMetadata( - domain + "/.wellknown/csaf/provider-metadata.json") + domain + "/.well-known/csaf/provider-metadata.json") if len(tlps) == 0 { return pm From f77bb5f1a8f75eb91dfa07789744b0d72dc2c8cc Mon Sep 17 00:00:00 2001 From: "Sascha L. Teichmann" Date: Thu, 2 Dec 2021 10:51:25 +0100 Subject: [PATCH 12/12] Added default publisher if not configured. Warning if uploads don't have the same publisher as in metadata. --- cmd/csaf_provider/config.go | 8 +++++++ cmd/csaf_provider/controller.go | 19 ++++++++++++--- cmd/csaf_provider/tmpl/upload.html | 10 ++++++++ csaf/models.go | 37 ++++++++++++++++++++++++++---- 4 files changed, 66 insertions(+), 8 deletions(-) diff --git a/cmd/csaf_provider/config.go b/cmd/csaf_provider/config.go index c21f1b2..1221397 100644 --- a/cmd/csaf_provider/config.go +++ b/cmd/csaf_provider/config.go @@ -113,5 +113,13 @@ func loadConfig() (*config, error) { cfg.OpenPGPURL = defaultOpenPGPURL } + if cfg.Publisher == nil { + cfg.Publisher = &csaf.Publisher{ + Category: func(c csaf.Category) *csaf.Category { return &c }(csaf.CSAFCategoryVendor), + Name: func(s string) *string { return &s }("ACME"), + Namespace: func(s string) *string { return &s }("https://example.com"), + } + } + return &cfg, nil } diff --git a/cmd/csaf_provider/controller.go b/cmd/csaf_provider/controller.go index c441106..d155594 100644 --- a/cmd/csaf_provider/controller.go +++ b/cmd/csaf_provider/controller.go @@ -215,6 +215,9 @@ func (c *controller) upload(rw http.ResponseWriter, r *http.Request) { return } + var warnings []string + warn := func(msg string) { warnings = append(warnings, msg) } + if err := doTransaction( c.cfg, t, func(folder string, pmd *csaf.ProviderMetadata) error { @@ -328,14 +331,23 @@ func (c *controller) upload(rw http.ResponseWriter, r *http.Request) { } // Take over publisher - // TODO: Check for conflicts. - pmd.Publisher = ex.publisher + switch { + case pmd.Publisher == nil: + warn("Publisher in provider metadata is not initialized. Forgot to configure?") + if c.cfg.DynamicProviderMetaData { + warn("Taking publisher from CSAF") + pmd.Publisher = ex.publisher + } + case !pmd.Publisher.Equals(ex.publisher): + warn("Publishers in provider metadata and CSAF do not match.") + } keyID, fingerprint := key.GetHexKeyID(), key.GetFingerprint() pmd.SetPGP(fingerprint, c.cfg.GetOpenPGPURL(keyID)) return nil - }); err != nil { + }, + ); err != nil { c.failed(rw, "upload.html", err) return } @@ -343,6 +355,7 @@ func (c *controller) upload(rw http.ResponseWriter, r *http.Request) { result := map[string]interface{}{ "Name": newCSAF, "ReleaseDate": ex.currentReleaseDate.Format(dateFormat), + "Warnings": warnings, } c.render(rw, "upload.html", result) diff --git a/cmd/csaf_provider/tmpl/upload.html b/cmd/csaf_provider/tmpl/upload.html index d2501ff..72ebfed 100644 --- a/cmd/csaf_provider/tmpl/upload.html +++ b/cmd/csaf_provider/tmpl/upload.html @@ -14,6 +14,16 @@ CSAF file:{{ .Name }} Release date:{{ .ReleaseDate }} + {{ if .Warnings }} +

+ Warning(s): +

    + {{ range .Warnings }} +
  • {{ . }}
  • + {{ end }} +
+

+ {{ end }} {{ end }}
Back: diff --git a/csaf/models.go b/csaf/models.go index b5ebac4..b313ab3 100644 --- a/csaf/models.go +++ b/csaf/models.go @@ -283,20 +283,47 @@ func (r *ROLIE) Validate() error { // Validate checks if the publisher is valid. // Returns an error if the validation fails otherwise nil. -func (cp *Publisher) Validate() error { +func (p *Publisher) Validate() error { switch { - case cp == nil: + case p == nil: return errors.New("publisher is mandatory") - case cp.Category == nil: + case p.Category == nil: return errors.New("publisher.category is mandatory") - case cp.Name == nil: + case p.Name == nil: return errors.New("publisher.name is mandatory") - case cp.Namespace == nil: + case p.Namespace == nil: return errors.New("publisher.namespace is mandatory") } return nil } +func strPtrEquals(a, b *string) bool { + switch { + case a == nil: + return b == nil + case b == nil: + return false + default: + return *a == *b + } +} + +// Equals checks if the publisher is equal to other componentwise. +func (p *Publisher) Equals(o *Publisher) bool { + switch { + case p == nil: + return o == nil + case o == nil: + return false + default: + return strPtrEquals((*string)(p.Category), (*string)(o.Category)) && + strPtrEquals(p.Name, o.Name) && + strPtrEquals(p.Namespace, o.Namespace) && + p.ContactDetails == o.ContactDetails && + p.IssuingAuthority == o.IssuingAuthority + } +} + // Validate checks if the PGPKey is valid. // Returns an error if the validation fails otherwise nil. func (pk *PGPKey) Validate() error {