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

Merge pull request #5 from csaf-poc/static-metadata

Static metadata
This commit is contained in:
Fadi Abbud 2021-12-02 11:40:01 +01:00 committed by GitHub
commit b21cef4677
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 319 additions and 203 deletions

View file

@ -6,6 +6,8 @@ import (
"strings" "strings"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/csaf-poc/csaf_distribution/csaf"
) )
const ( const (
@ -13,7 +15,7 @@ const (
defaultConfigPath = "/usr/lib/casf/config.toml" defaultConfigPath = "/usr/lib/casf/config.toml"
defaultFolder = "/var/www/" defaultFolder = "/var/www/"
defaultWeb = "/var/www/html" 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 { type config struct {
@ -22,9 +24,11 @@ type config struct {
Web string `toml:"web"` Web string `toml:"web"`
TLPs []tlp `toml:"tlps"` TLPs []tlp `toml:"tlps"`
UploadSignature bool `toml:"upload_signature"` UploadSignature bool `toml:"upload_signature"`
PGPURL string `toml:"pgp_url"` OpenPGPURL string `toml:"openpgp_url"`
Domain string `toml:"domain"` Domain string `toml:"domain"`
NoPassphrase bool `toml:"no_passphrase"` NoPassphrase bool `toml:"no_passphrase"`
DynamicProviderMetaData bool `toml:"dynamic_provider_metadata"`
Publisher *csaf.Publisher `toml:"publisher"`
} }
type tlp string type tlp string
@ -54,8 +58,27 @@ func (t *tlp) UnmarshalText(text []byte) error {
return fmt.Errorf("invalid config TLP value: %v", string(text)) return fmt.Errorf("invalid config TLP value: %v", string(text))
} }
func (cfg *config) GetPGPURL(key string) string { func (cfg *config) GetOpenPGPURL(key string) string {
return strings.ReplaceAll(cfg.PGPURL, "${KEY}", key) return strings.ReplaceAll(cfg.OpenPGPURL, "${KEY}", "0x"+key)
}
func (cfg *config) modelTLPs() []csaf.TLPLabel {
tlps := make([]csaf.TLPLabel, 0, len(cfg.TLPs))
for _, t := range cfg.TLPs {
if t != tlpCSAF {
tlps = append(tlps, csaf.TLPLabel(strings.ToUpper(string(t))))
}
}
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) { func loadConfig() (*config, error) {
@ -86,8 +109,16 @@ func loadConfig() (*config, error) {
cfg.TLPs = []tlp{tlpCSAF, tlpWhite, tlpGreen, tlpAmber, tlpRed} cfg.TLPs = []tlp{tlpCSAF, tlpWhite, tlpGreen, tlpAmber, tlpRed}
} }
if cfg.PGPURL == "" { if cfg.OpenPGPURL == "" {
cfg.PGPURL = defaultPGPURL 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 return &cfg, nil

View file

@ -112,74 +112,66 @@ func loadCSAF(r *http.Request) (string, []byte, error) {
return cleanFileName(handler.Filename), buf.Bytes(), nil return cleanFileName(handler.Filename), buf.Bytes(), nil
} }
func (c *controller) loadCryptoKey() (*crypto.Key, error) { func (c *controller) handleSignature(
f, err := os.Open(c.cfg.Key) r *http.Request,
if err != nil { data []byte,
return nil, err ) (string, *crypto.Key, error) {
}
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. // Either way ... we need the key.
key, err := c.loadCryptoKey() key, err := c.cfg.loadCryptoKey()
if err != nil { if err != nil {
return "", "", err return "", nil, err
} }
fingerprint := key.GetFingerprint()
// Was the signature given via request? // Was the signature given via request?
if c.cfg.UploadSignature { if c.cfg.UploadSignature {
sigText := r.FormValue("signature") sigText := r.FormValue("signature")
if sigText == "" { if sigText == "" {
return "", "", errors.New("missing signature in request") return "", nil, errors.New("missing signature in request")
} }
pgpSig, err := crypto.NewPGPSignatureFromArmored(sigText) pgpSig, err := crypto.NewPGPSignatureFromArmored(sigText)
if err != nil { if err != nil {
return "", "", err return "", nil, err
} }
// Use as public key // Use as public key
signRing, err := crypto.NewKeyRing(key) signRing, err := crypto.NewKeyRing(key)
if err != nil { if err != nil {
return "", "", err return "", nil, err
} }
if err := signRing.VerifyDetached( if err := signRing.VerifyDetached(
crypto.NewPlainMessage(data), crypto.NewPlainMessage(data),
pgpSig, crypto.GetUnixTime(), pgpSig, crypto.GetUnixTime(),
); err != nil { ); err != nil {
return "", "", err return "", nil, err
} }
return sigText, fingerprint, nil return sigText, key, nil
} }
// Sign ourself // Sign ourself
if passwd := r.FormValue("passphrase"); !c.cfg.NoPassphrase && passwd != "" { if passwd := r.FormValue("passphrase"); !c.cfg.NoPassphrase && passwd != "" {
if key, err = key.Unlock([]byte(passwd)); err != nil { if key, err = key.Unlock([]byte(passwd)); err != nil {
return "", "", err return "", nil, err
} }
} }
// Use as private key // Use as private key
signRing, err := crypto.NewKeyRing(key) signRing, err := crypto.NewKeyRing(key)
if err != nil { if err != nil {
return "", "", err return "", nil, err
} }
sig, err := signRing.SignDetached(crypto.NewPlainMessage(data)) sig, err := signRing.SignDetached(crypto.NewPlainMessage(data))
if err != nil { if err != nil {
return "", "", err return "", nil, err
} }
armored, err := sig.GetArmored() armored, err := sig.GetArmored()
return armored, fingerprint, err return armored, key, err
} }
func (c *controller) upload(rw http.ResponseWriter, r *http.Request) { func (c *controller) upload(rw http.ResponseWriter, r *http.Request) {
@ -217,12 +209,15 @@ 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 { if err != nil {
c.failed(rw, "upload.html", err) c.failed(rw, "upload.html", err)
return return
} }
var warnings []string
warn := func(msg string) { warnings = append(warnings, msg) }
if err := doTransaction( if err := doTransaction(
c.cfg, t, c.cfg, t,
func(folder string, pmd *csaf.ProviderMetadata) error { func(folder string, pmd *csaf.ProviderMetadata) error {
@ -336,13 +331,23 @@ func (c *controller) upload(rw http.ResponseWriter, r *http.Request) {
} }
// Take over publisher // Take over publisher
// TODO: Check for conflicts. 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 pmd.Publisher = ex.publisher
}
case !pmd.Publisher.Equals(ex.publisher):
warn("Publishers in provider metadata and CSAF do not match.")
}
pmd.SetPGP(fingerprint, c.cfg.GetPGPURL(fingerprint)) keyID, fingerprint := key.GetHexKeyID(), key.GetFingerprint()
pmd.SetPGP(fingerprint, c.cfg.GetOpenPGPURL(keyID))
return nil return nil
}); err != nil { },
); err != nil {
c.failed(rw, "upload.html", err) c.failed(rw, "upload.html", err)
return return
} }
@ -350,6 +355,7 @@ func (c *controller) upload(rw http.ResponseWriter, r *http.Request) {
result := map[string]interface{}{ result := map[string]interface{}{
"Name": newCSAF, "Name": newCSAF,
"ReleaseDate": ex.currentReleaseDate.Format(dateFormat), "ReleaseDate": ex.currentReleaseDate.Format(dateFormat),
"Warnings": warnings,
} }
c.render(rw, "upload.html", result) c.render(rw, "upload.html", result)

108
cmd/csaf_provider/create.go Normal file
View file

@ -0,0 +1,108 @@
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
// Set OpenPGP key.
key, err := c.loadCryptoKey()
if err != nil {
return err
}
keyID, fingerprint := key.GetHexKeyID(), key.GetFingerprint()
pm.SetPGP(fingerprint, c.GetOpenPGPURL(keyID))
return saveToFile(path, pm)
}

View file

@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"crypto/sha256" "crypto/sha256"
"crypto/sha512" "crypto/sha512"
"errors"
"fmt" "fmt"
"hash" "hash"
"io" "io"
@ -16,81 +15,6 @@ import (
"time" "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)
}
func createSecurity(c *config) error {
security := filepath.Join(c.Web, "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 { func deepCopy(dst, src string) error {
stack := []string{dst, src} stack := []string{dst, src}
@ -214,16 +138,12 @@ func writeHashedFile(fname, name string, data []byte, armored string) error {
return nil return nil
} }
type saver interface { func saveToFile(fname string, wt io.WriterTo) error {
Save(io.Writer) error
}
func saveToFile(fname string, s saver) error {
f, err1 := os.Create(fname) f, err1 := os.Create(fname)
if err1 != nil { if err1 != nil {
return err1 return err1
} }
err1 = s.Save(f) _, err1 = wt.WriteTo(f)
err2 := f.Close() err2 := f.Close()
if err1 != nil { if err1 != nil {
return err1 return err1

View file

@ -14,6 +14,16 @@
<tr><td>CSAF file:</td><td><tt>{{ .Name }}</tt></td></tr> <tr><td>CSAF file:</td><td><tt>{{ .Name }}</tt></td></tr>
<tr><td>Release date:</td><td><tt>{{ .ReleaseDate }}</tt></td></tr> <tr><td>Release date:</td><td><tt>{{ .ReleaseDate }}</tt></td></tr>
</table> </table>
{{ if .Warnings }}
<p>
Warning(s):
<ul>
{{ range .Warnings }}
<li>{{ . }}</li>
{{ end }}
</ul>
</p>
{{ end }}
{{ end }} {{ end }}
<br> <br>
<a href="/cgi-bin/csaf_provider.go/">Back</a>: <a href="/cgi-bin/csaf_provider.go/">Back</a>:

View file

@ -3,49 +3,10 @@ package main
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/csaf-poc/csaf_distribution/csaf" "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( func doTransaction(
cfg *config, cfg *config,
t tlp, t tlp,
@ -60,7 +21,7 @@ func doTransaction(
f, err := os.Open(metadata) f, err := os.Open(metadata)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return newProviderMetadata(cfg), nil return csaf.NewProviderMetadataDomain(cfg.Domain, cfg.modelTLPs()), nil
} }
return nil, err return nil, err
} }
@ -98,14 +59,15 @@ func doTransaction(
return err return err
} }
// Write back provider metadata. // Write back provider metadata if its dynamic.
if cfg.DynamicProviderMetaData {
newMetaName, newMetaFile, err := mkUniqFile(metadata) newMetaName, newMetaFile, err := mkUniqFile(metadata)
if err != nil { if err != nil {
os.RemoveAll(newDir) os.RemoveAll(newDir)
return err return err
} }
if err := pmd.Save(newMetaFile); err != nil { if _, err := pmd.WriteTo(newMetaFile); err != nil {
newMetaFile.Close() newMetaFile.Close()
os.Remove(newMetaName) os.Remove(newMetaName)
os.RemoveAll(newDir) os.RemoveAll(newDir)
@ -122,6 +84,7 @@ func doTransaction(
os.RemoveAll(newDir) os.RemoveAll(newDir)
return err return err
} }
}
// Switch directories. // Switch directories.
symlink := filepath.Join(newDir, string(t)) symlink := filepath.Join(newDir, string(t))

View file

@ -102,11 +102,11 @@ var csafCategoryPattern = alternativesUnmarshal(
// Publisher is the publisher of the feed. // Publisher is the publisher of the feed.
type Publisher struct { type Publisher struct {
Category *Category `json:"category"` // required Category *Category `json:"category" toml:"category"` // required
Name *string `json:"name"` // required Name *string `json:"name" toml:"name"` // required
Namespace *string `json:"namespace"` // required Namespace *string `json:"namespace" toml:"namespace"` // required
ContactDetails string `json:"contact_details,omitempty"` ContactDetails string `json:"contact_details,omitempty" toml:"contact_details"`
IssuingAuthority string `json:"issuing_authority,omitempty"` IssuingAuthority string `json:"issuing_authority,omitempty" toml:"issuing_authority"`
} }
// MetadataVersion is the metadata version of the feed. // MetadataVersion is the metadata version of the feed.
@ -148,7 +148,7 @@ type ProviderMetadata struct {
MetadataVersion *MetadataVersion `json:"metadata_version"` // required MetadataVersion *MetadataVersion `json:"metadata_version"` // required
MirrorOnCSAFAggregators *bool `json:"mirror_on_CSAF_aggregators"` // required MirrorOnCSAFAggregators *bool `json:"mirror_on_CSAF_aggregators"` // required
PGPKeys []PGPKey `json:"pgp_keys,omitempty"` PGPKeys []PGPKey `json:"pgp_keys,omitempty"`
Publisher *Publisher `json:"publisher"` // required Publisher *Publisher `json:"publisher,omitempty"` // required
Role *MetadataRole `json:"role"` // required Role *MetadataRole `json:"role"` // required
} }
@ -283,20 +283,47 @@ func (r *ROLIE) Validate() error {
// Validate checks if the publisher is valid. // Validate checks if the publisher is valid.
// Returns an error if the validation fails otherwise nil. // Returns an error if the validation fails otherwise nil.
func (cp *Publisher) Validate() error { func (p *Publisher) Validate() error {
switch { switch {
case cp == nil: case p == nil:
return errors.New("publisher is mandatory") return errors.New("publisher is mandatory")
case cp.Category == nil: case p.Category == nil:
return errors.New("publisher.category is mandatory") return errors.New("publisher.category is mandatory")
case cp.Name == nil: case p.Name == nil:
return errors.New("publisher.name is mandatory") return errors.New("publisher.name is mandatory")
case cp.Namespace == nil: case p.Namespace == nil:
return errors.New("publisher.namespace is mandatory") return errors.New("publisher.namespace is mandatory")
} }
return nil 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. // Validate checks if the PGPKey is valid.
// Returns an error if the validation fails otherwise nil. // Returns an error if the validation fails otherwise nil.
func (pk *PGPKey) Validate() error { func (pk *PGPKey) Validate() error {
@ -384,11 +411,60 @@ func NewProviderMetadata(canonicalURL string) *ProviderMetadata {
return pm return pm
} }
// Save saves a metadata provider to a writer. // NewProviderMetadataDomain creates a new provider with the given URL
func (pmd *ProviderMetadata) Save(w io.Writer) error { // and tlps feeds.
enc := json.NewEncoder(w) func NewProviderMetadataDomain(domain string, tlps []TLPLabel) *ProviderMetadata {
pm := NewProviderMetadata(
domain + "/.well-known/csaf/provider-metadata.json")
if len(tlps) == 0 {
return pm
}
// Register feeds.
feeds := make([]Feed, len(tlps))
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: &url,
}
}
pm.Distributions = []Distribution{{
Rolie: []ROLIE{{
Feeds: feeds,
}},
}}
return pm
}
type nWriter struct {
io.Writer
n int64
}
func (nw *nWriter) Write(p []byte) (int, error) {
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) {
nw := nWriter{w, 0}
enc := json.NewEncoder(&nw)
enc.SetIndent("", " ") enc.SetIndent("", " ")
return enc.Encode(pmd) err := enc.Encode(pmd)
return nw.n, err
} }
// LoadProviderMetadata loads a metadata provider from a reader. // LoadProviderMetadata loads a metadata provider from a reader.

View file

@ -68,11 +68,13 @@ func LoadROLIEFeed(r io.Reader) (*ROLIEFeed, error) {
return &rf, nil return &rf, nil
} }
// Save saves a ROLIE feed to a writer. // WriteTo saves a ROLIE feed to a writer.
func (rf *ROLIEFeed) Save(w io.Writer) error { func (rf *ROLIEFeed) WriteTo(w io.Writer) (int64, error) {
enc := json.NewEncoder(w) nw := nWriter{w, 0}
enc := json.NewEncoder(&nw)
enc.SetIndent("", " ") enc.SetIndent("", " ")
return enc.Encode(rf) err := enc.Encode(rf)
return nw.n, err
} }
// EntryByID looks up an entry by its ID. // EntryByID looks up an entry by its ID.