Skip to content

Commit

Permalink
Cyrill Map Server Fixes and Improvements (#57)
Browse files Browse the repository at this point in the history
* show mapserver public key when starting and add helper functions

* adjust parsing rules to CT log standard (addresses issue #50)

* log updater progress

* fix null pointer exception in empty tree

* fix null pointer exception if initializing tree with policy certificate

* make HTTP API port configurable

* fix issue of responder using wrong SMT root

* sign tree head after update

* allow log fetchers to run multiple times (fix closed results channel bug)

* extend policy attributes with disallowed and excluded domains

* fix closing bad idle connection bug

* remove cooloff checking at PCA (should be done at client)

* add/clean up logging

* add version information

* formatting

* Add warning if fetching speed is too low

* Fix comment.

---------

Co-authored-by: Juan A. Garcia Pardo <[email protected]>
  • Loading branch information
cyrill-k and juagargi authored Mar 15, 2024
1 parent be4d4fe commit 79ba0a1
Show file tree
Hide file tree
Showing 13 changed files with 259 additions and 33 deletions.
31 changes: 28 additions & 3 deletions cmd/mapserver/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"bytes"
"context"
"crypto/rsa"
"flag"
"fmt"
"os"
Expand All @@ -18,6 +19,8 @@ import (
"github.com/netsec-ethz/fpki/pkg/util"
)

const VERSION = "0.1.0"

const waitForExitBeforePanicTime = 10 * time.Second

func main() {
Expand All @@ -36,12 +39,20 @@ func mainFunc() int {
}
flag.CommandLine.Usage = flag.Usage

var showVersion bool
flag.BoolVar(&showVersion, "version", false, "Print map server version")
flag.BoolVar(&showVersion, "v", false, "Print map server version")
updateVar := flag.Bool("updateNow", false, "Immediately trigger an update cycle")
createSampleConfig := flag.Bool("createSampleConfig", false,
"Create configuration file specified by positional argument")
insertPolicyVar := flag.String("policyFile", "", "policy certificate file to be ingested into the mapserver")
flag.Parse()

if showVersion {
fmt.Printf("FP-PKI Map Server %s\n", VERSION)
return 0
}

// We need the configuration file as the first positional argument.
if flag.NArg() != 1 {
flag.Usage()
Expand Down Expand Up @@ -109,7 +120,9 @@ func insertPolicyFromFile(policyFile string) error {
if err != nil {
return err
}
if bytes.Equal(root[:], newRoot[:]) {
if root == nil {
fmt.Printf("MHT root value initially set to %v\n", newRoot)
} else if bytes.Equal(root[:], newRoot[:]) {
fmt.Printf("MHT root value was not updated (%v)\n", newRoot)
} else {
fmt.Printf("MHT root value updated from %v to %v\n", root, newRoot)
Expand All @@ -129,6 +142,7 @@ func writeSampleConfig() error {
CTLogServerURLs: []string{"https://ct.googleapis.com/logs/xenon2023/"},
CertificatePemFile: "tests/testdata/servercert.pem",
PrivateKeyPemFile: "tests/testdata/serverkey.pem",
HttpAPIPort: 8443,

UpdateAt: util.NewTimeOfDay(3, 00, 00, 00),
UpdateTimer: util.DurationWrap{
Expand Down Expand Up @@ -170,7 +184,15 @@ func runWithConfig(
if err != nil {
return err
}
fmt.Printf("Running map server with root: %v\n", root)
base64PublicKey, err := util.RSAPublicToDERBase64(server.Cert.PublicKey.(*rsa.PublicKey))
if err != nil {
return fmt.Errorf("error converting public key to DER base64: %w", err)
}
if root == nil {
fmt.Printf("Running empy map server (%s) with public key: %s\n", VERSION, base64PublicKey)
} else {
fmt.Printf("Running map server (%s) with root: %x and public key: %s\n", VERSION, *root, base64PublicKey)
}

// Should update now?
if updateNow {
Expand All @@ -183,10 +205,13 @@ func runWithConfig(
// Set update cycle timer.
util.RunWhen(ctx, conf.UpdateAt.NextTimeOfDay(), conf.UpdateTimer.Duration,
func(ctx context.Context) {
err := server.PruneAndUpdate(ctx)
updatePossible, err := server.PruneAndUpdateIfPossible(ctx)
if err != nil {
fmt.Printf("ERROR: update returned %s\n", err)
}
if !updatePossible {
fmt.Printf("WARNING: Unable to schedule update due to currently running update (CT log fetching and map server ingestion speed may be too low) at %s\n", time.Now().UTC().Format(time.RFC3339))
}
})

// Listen in responder.
Expand Down
9 changes: 9 additions & 0 deletions pkg/common/crypto/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ func SignBytes(b []byte, key *rsa.PrivateKey) ([]byte, error) {
return signature, nil
}

func VerifySignedBytes(b []byte, signature []byte, key *rsa.PublicKey) error {
hashOutput := sha256.Sum256(b)
err := rsa.VerifyPKCS1v15(key, crypto.SHA256, hashOutput[:], signature)
if err != nil {
return fmt.Errorf("VerifySignature | VerifyPKCS1V15 | %w", err)
}
return nil
}

// SignAsOwner generates a signature using the owner's key, and fills the owner signature in
// the policy certificate signing request.
//
Expand Down
106 changes: 103 additions & 3 deletions pkg/common/policies.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package common

import (
"bytes"
"fmt"
"slices"
"strings"
"time"
)

Expand Down Expand Up @@ -87,8 +90,25 @@ func (o PolicyCertificate) Raw() ([]byte, error) {

// PolicyAttributes is a domain policy that specifies what is or not acceptable for a domain.
type PolicyAttributes struct {
TrustedCA []string `json:",omitempty"`
// List of CA subject names allowed to issue certificates for this domain and subdomains. The
// string representation is according to the golang x509 library
// golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
AllowedCAs []string `json:",omitempty"`

// The following three attributes specify which subdomains are allowed and which are excluded
// (i.e., policies do not apply to these subdomains). Only one of these attributes is allowed to
// be set to the wildcard value "*" and covers all domains that are not covered by other
// attributes. No subdomain name can be covered by more than one attribute (including the
// wildcard value). Only single labels without "." can be specified.

// This attribute lists subdomains that are allowed
AllowedSubdomains []string `json:",omitempty"`

// This attribute lists subdomains that are not allowed
DisallowedSubdomains []string `json:",omitempty"`

// This attribute lists sudomains for which no policy applies
ExcludedSubdomains []string `json:",omitempty"`
}

type PolicyCertificateRevocationFields struct {
Expand All @@ -110,6 +130,84 @@ func (o PolicyCertificateRevocation) Raw() ([]byte, error) {
return rawTemplate(o)
}

type PolicyAttributeDomainValidityResult int

const (
PolicyAttributeDomainAllowed PolicyAttributeDomainValidityResult = iota
PolicyAttributeDomainDisallowed = iota
PolicyAttributeDomainExcluded = iota
PolicyAttributeDomainNotApplicable = iota
)

func (a PolicyAttributes) ValidateAttributes() error {
caMap := map[string]struct{}{}
for _, caName := range a.AllowedCAs {
caMap[caName] = struct{}{}
}
if len(caMap) < len(a.AllowedCAs) {
return fmt.Errorf("Allowed CA attribute contains %d duplicate CAs", len(caMap)-len(a.AllowedCAs))
}

// TODO: could also check CA subject name formatting

labelMap := map[string]struct{}{}
for _, label := range a.AllowedSubdomains {
labelMap[label] = struct{}{}
}
for _, label := range a.DisallowedSubdomains {
labelMap[label] = struct{}{}
}
for _, label := range a.ExcludedSubdomains {
labelMap[label] = struct{}{}
}
nLabels := len(a.AllowedSubdomains) + len(a.DisallowedSubdomains) + len(a.ExcludedSubdomains)
if len(labelMap) < nLabels {
return fmt.Errorf("Subdomain attributes contain %d duplicate labels", nLabels-len(labelMap))
}
return nil
}

func (a PolicyAttributes) CheckDomainValidity(policyAttributeDomain, domain string) PolicyAttributeDomainValidityResult {
// remove trailing dot if present (example.com. -> example.com)
policyAttributeDomain, _ = strings.CutSuffix(policyAttributeDomain, ".")
domain, _ = strings.CutSuffix(domain, ".")

// get relative domain path compared to the policy attribute's domain (www.test.example.com -> www.test
targetSubdomain, ok := strings.CutSuffix(domain, "."+policyAttributeDomain)
if !ok {
return PolicyAttributeDomainNotApplicable
}

// get the relevant subdomain label on which the domain validity is evaluated on (www.test.example.com -> test)
targetSubdomainLabels := strings.Split(targetSubdomain, ".")
targetSubdomainLabel := targetSubdomainLabels[len(targetSubdomainLabels)-1]

// check for exact match
if slices.Contains(a.AllowedSubdomains, targetSubdomainLabel) {
return PolicyAttributeDomainAllowed
}
if slices.Contains(a.DisallowedSubdomains, targetSubdomainLabel) {
return PolicyAttributeDomainDisallowed
}
if slices.Contains(a.ExcludedSubdomains, targetSubdomainLabel) {
return PolicyAttributeDomainExcluded
}

// check for wildcards
if slices.Contains(a.AllowedSubdomains, "*") {
return PolicyAttributeDomainAllowed
}
if slices.Contains(a.DisallowedSubdomains, "*") {
return PolicyAttributeDomainDisallowed
}
if slices.Contains(a.ExcludedSubdomains, "*") {
return PolicyAttributeDomainExcluded
}

// default is to allow any subdomain
return PolicyAttributeDomainAllowed
}

func NewPolicyCertificateFields(
version int,
serialNumber int,
Expand Down Expand Up @@ -237,8 +335,10 @@ func (c PolicyCertificate) Equal(x PolicyCertificate) bool {

func (s PolicyAttributes) Equal(o PolicyAttributes) bool {
return true &&
equalStringSlices(s.TrustedCA, o.TrustedCA) &&
equalStringSlices(s.AllowedSubdomains, o.AllowedSubdomains)
equalStringSlices(s.AllowedCAs, o.AllowedCAs) &&
equalStringSlices(s.AllowedSubdomains, o.AllowedSubdomains) &&
equalStringSlices(s.DisallowedSubdomains, o.DisallowedSubdomains) &&
equalStringSlices(s.ExcludedSubdomains, o.ExcludedSubdomains)
}

func NewPolicyCertificateRevocationFields(
Expand Down
6 changes: 6 additions & 0 deletions pkg/db/mysql/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"database/sql"
"fmt"
"net/url"
"time"

_ "github.com/go-sql-driver/mysql"

Expand Down Expand Up @@ -33,6 +34,11 @@ func Connect(config *db.Configuration) (db.Conn, error) {
maxConnections := 8
db.SetMaxOpenConns(maxConnections)

// Set the maximum idle connection time to a lower value than the mysql wait_timeout (8h) to
// ensure that idle connections that are closed by the mysql DB are not reused
connMaxIdleTime := 1 * time.Hour
db.SetConnMaxIdleTime(connMaxIdleTime)

// check schema
if config.CheckSchema {
if err := checkSchema(db); err != nil {
Expand Down
1 change: 1 addition & 0 deletions pkg/mapserver/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type Config struct {
DBConfig *db.Configuration
CertificatePemFile string // A X509 pem certificate
PrivateKeyPemFile string // A RSA pem key
HttpAPIPort int

UpdateAt util.TimeOfDayWrap
UpdateTimer util.DurationWrap
Expand Down
7 changes: 5 additions & 2 deletions pkg/mapserver/logfetcher/logfetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func NewLogFetcher(url string) (*LogFetcher, error) {
serverBatchSize: defaultServerBatchSize,
processBatchSize: defaultProcessBatchSize,
ctClient: ctClient,
chanResults: make(chan *result, preloadCount),
chanResults: nil,
}, nil
}

Expand All @@ -123,6 +123,7 @@ func (f LogFetcher) GetCurrentState(ctx context.Context) (State, error) {
// StartFetching will start fetching certificates in the background, so that there is
// at most two batches ready to be immediately read by NextBatch.
func (f *LogFetcher) StartFetching(start, end int64) {
f.chanResults = make(chan *result, preloadCount)
f.start = start
f.end = end
go f.fetch()
Expand Down Expand Up @@ -233,7 +234,9 @@ func (f *LogFetcher) fetch() {
}
// Certificate.
cert, err := ctx509.ParseCertificate(raw.Cert.Data)
if err != nil {
// Accept the same certificates as CT logs, i.e., don't be too restrictive in terms of
// which certificates to reject (i.e., allow for non-fatal parsing/validation errors)
if ctx509.IsFatal(err) {
f.chanResults <- &result{
err: err,
}
Expand Down
44 changes: 38 additions & 6 deletions pkg/mapserver/mapserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ import (
"github.com/netsec-ethz/fpki/pkg/util"
)

const APIPort = 8443 // TODO: should be a config parameter

type PayloadReturnType int

const (
Expand All @@ -43,6 +41,7 @@ type MapServer struct {
TLS *tls.Certificate
ReadTimeout time.Duration
WriteTimeout time.Duration
HttpAPIPort int

apiStopServerChan chan struct{}
updateChan chan context.Context
Expand Down Expand Up @@ -112,6 +111,7 @@ func NewMapServer(ctx context.Context, conf *config.Config) (*MapServer, error)
TLS: &tlsCert,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
HttpAPIPort: conf.HttpAPIPort,

apiStopServerChan: make(chan struct{}, 1),
updateChan: make(chan context.Context),
Expand All @@ -124,7 +124,14 @@ func NewMapServer(ctx context.Context, conf *config.Config) (*MapServer, error)
for {
select {
case c := <-s.updateChan:
// TODO: ensure that the Mapserver is never in an inconsistent state. Currently, if
// the new SMT is applied but the responder does not yet use the updated SMT root
// value, queries will fail
s.pruneAndUpdate(c)
err := s.Responder.ReloadRootAndSignTreeHead(c, s.Key)
if err != nil {
s.updateErrChan <- err
}
case <-ctx.Done():
// Requested to exit.
close(s.updateChan)
Expand Down Expand Up @@ -156,7 +163,7 @@ func (s *MapServer) listen(ctx context.Context, useTLS bool) error {
http.HandleFunc("/getpolicypayloads", func(w http.ResponseWriter, r *http.Request) { s.apiGetPayloads(w, r, Policies) })

server := &http.Server{
Addr: fmt.Sprintf(":%d", APIPort),
Addr: fmt.Sprintf(":%d", s.HttpAPIPort),
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{*s.TLS},
},
Expand All @@ -172,7 +179,7 @@ func (s *MapServer) listen(ctx context.Context, useTLS bool) error {
server.Shutdown(ctx)
}()
// This call blocks
fmt.Printf("Listening on %d\n", APIPort)
fmt.Printf("Listening on %d\n", s.HttpAPIPort)
if useTLS {
chanErr <- server.ListenAndServeTLS("", "")
} else {
Expand All @@ -195,6 +202,21 @@ func (s *MapServer) Shutdown(ctx context.Context) {
s.apiStopServerChan <- struct{}{}
}

// PruneAndUpdateIfPossible tries to trigger an update if no update is currently running.
// If an ongoing update is still in process, it returns false. Returns true if a new update was
// triggered.
func (s *MapServer) PruneAndUpdateIfPossible(ctx context.Context) (bool, error) {
select {
// Signal we want an update.
case s.updateChan <- ctx:
// Wait for the answer (in form of an error).
err := <-s.updateErrChan
return true, err
default:
return false, nil
}
}

// PruneAndUpdate triggers an update. If an ongoing update is still in process, it blocks.
func (s *MapServer) PruneAndUpdate(ctx context.Context) error {
// Signal we want an update.
Expand Down Expand Up @@ -340,10 +362,20 @@ func (s *MapServer) updateCerts(ctx context.Context) error {
defer s.Updater.StopFetching()

// Main update loop.
start := time.Now()
for s.Updater.NextBatch(ctx) {
n, err := s.Updater.UpdateNextBatch(ctx)
// print progress information
logUrl, currentIndex, maxIndex, err := s.Updater.GetProgress()
if err != nil {
return fmt.Errorf("retrieve progress: %s", err)
}
fmt.Printf("Running updater for log %s in range (%d, %d)\n", logUrl, currentIndex, maxIndex)

fmt.Printf("updated %5d certs batch at %s\n", n, getTime())
fetchDuration := time.Now().Sub(start)
start = time.Now()

n, err := s.Updater.UpdateNextBatch(ctx)
fmt.Printf("Fetched %d certs in %.2f seconds at %s\n", n, fetchDuration.Seconds(), getTime())
if err != nil {
// We stop the loop here, as probably requires manual inspection of the logs, etc.
return fmt.Errorf("updating next batch of x509 certificates: %w", err)
Expand Down
Loading

0 comments on commit 79ba0a1

Please sign in to comment.