diff --git a/cli/ocspserve/ocspserve.go b/cli/ocspserve/ocspserve.go index 9f8519fc9..31d0300d7 100644 --- a/cli/ocspserve/ocspserve.go +++ b/cli/ocspserve/ocspserve.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/cloudflare/cfssl/cli" + "github.com/cloudflare/cfssl/helpers" "github.com/cloudflare/cfssl/log" "github.com/cloudflare/cfssl/ocsp" ) @@ -26,6 +27,7 @@ var ocspServerFlags = []string{"address", "port", "responses"} // ocspServerMain is the command line entry point to the OCSP responder. // It sets up a new HTTP server that responds to OCSP requests. func ocspServerMain(args []string, c cli.Config) error { + var src ocsp.Source // serve doesn't support arguments. if len(args) > 0 { return errors.New("argument is provided but not defined; please refer to the usage by flag -h") @@ -35,9 +37,33 @@ func ocspServerMain(args []string, c cli.Config) error { return errors.New("no response file provided, please set the -responses flag") } - src, err := ocsp.NewSourceFromFile(c.Responses) + typ, path, err := helpers.ParseConnString(c.Responses) if err != nil { - return errors.New("unable to read response file") + return errors.New("unable to parse responses connection string") + } + switch typ { + case "file": + src, err = ocsp.NewSourceFromFile(path) + if err != nil { + return errors.New("unable to read response file") + } + case "sqlite": + src, err = ocsp.NewSourceFromConnString("sqlite", path) + if err != nil { + return errors.New("unable to read Sqlite connection string") + } + case "mysql": + src, err = ocsp.NewSourceFromConnString("mysql", path) + if err != nil { + return errors.New("unable to read MySQL connection string") + } + case "postgres": + src, err = ocsp.NewSourceFromConnString("postgres", path) + if err != nil { + return errors.New("unable to read PostgreSQL connection string") + } + default: + return errors.New("unrecognized connection string format") } log.Info("Registering OCSP responder handler") diff --git a/helpers/helpers.go b/helpers/helpers.go index 3b3349ac2..17845eccc 100644 --- a/helpers/helpers.go +++ b/helpers/helpers.go @@ -19,11 +19,8 @@ import ( "io" "io/ioutil" "math/big" + "net/url" "os" - - "github.com/google/certificate-transparency/go" - "golang.org/x/crypto/ocsp" - "strings" "time" @@ -31,6 +28,8 @@ import ( cferr "github.com/cloudflare/cfssl/errors" "github.com/cloudflare/cfssl/helpers/derhelpers" "github.com/cloudflare/cfssl/log" + "github.com/google/certificate-transparency/go" + "golang.org/x/crypto/ocsp" "golang.org/x/crypto/pkcs12" ) @@ -646,3 +645,26 @@ func ReadBytes(valFile string) ([]byte, error) { strings.Join(splitVal[:len(splitVal)-1], ", ")) } } + +// ParseConnString parses a string representing the source of OCSP responses +// which can either be a file or the path to a DB (Sqlite, MySQL or PostgreSQL). +// It returns the type of the connection string (e.g. "File" or "MySQL" etc.) as +// well as the path to the source. +func ParseConnString(conn string) (string, string, error) { + u, err := url.Parse(conn) + if err != nil { + return "", "", err + } + switch u.Scheme { + case "", "file": + return "file", u.Path, nil + case "sqlite3": + return "sqlite", u.Path, nil + case "mysql": + return "mysql", conn[8:], nil + case "postgresql": + return "postgres", "dbname=" + u.Path[1:] + " sslmode=disable", nil + default: + return "DB", u.Path, nil + } +} diff --git a/helpers/helpers_test.go b/helpers/helpers_test.go index c851700d9..8d7daac0e 100644 --- a/helpers/helpers_test.go +++ b/helpers/helpers_test.go @@ -625,3 +625,26 @@ func TestSCTListFromOCSPResponse(t *testing.T) { t.Fatal("SCTs don't match") } } + +func TestParseConnString(t *testing.T) { + filePath := "/path/to/file.txt" + typ, path, err := ParseConnString(filePath) + if typ != "file" || path != "/path/to/file.txt" || err != nil { + t.Fatal("Incorrect parsing of file path") + } + sqliteStr := "sqlite3:///path/to/db/file.db" + typ, path, err = ParseConnString(sqliteStr) + if typ != "sqlite" || path != "/path/to/db/file.db" || err != nil { + t.Fatal("Incorrect parsing of sqlite connection string") + } + mysqlStr := "mysql://root@tcp(localhost:3306)/certdb_development?parseTime=true" + typ, path, err = ParseConnString(mysqlStr) + if typ != "mysql" || path != "root@tcp(localhost:3306)/certdb_development?parseTime=true" || err != nil { + t.Fatal("Incorrect parsing of MySQL connection string") + } + postgresStr := "postgresql://root@tcp(localhost:3306)/certdb_development?parseTime=true" + typ, path, err = ParseConnString(postgresStr) + if typ != "postgres" || path != "dbname=certdb_development sslmode=disable" || err != nil { + t.Fatal("Incorrect parsing of PostgreSQL connection string") + } +} diff --git a/ocsp/responder.go b/ocsp/responder.go index 27f8a6f91..8a65986df 100644 --- a/ocsp/responder.go +++ b/ocsp/responder.go @@ -19,8 +19,10 @@ import ( "time" "github.com/cloudflare/cfssl/certdb" + "github.com/cloudflare/cfssl/certdb/sql" "github.com/cloudflare/cfssl/log" "github.com/jmhodges/clock" + "github.com/jmoiron/sqlx" "golang.org/x/crypto/ocsp" ) @@ -63,6 +65,21 @@ func NewDBSource(dbAccessor certdb.Accessor) Source { } } +// NewSourceFromConnString creates a new DBSource object with an associated +// dbAccessor. They type of the DB connection is specificied by the typ +// argument, and this function currently supports Sqlite, MySQL and PostgreSQL. +// The dbpath argument is a connection string aiding in connecting to the +// associated DB type. +func NewSourceFromConnString(typ, dbpath string) (Source, error) { + db, err := sqlx.Open(typ, dbpath) + if err != nil { + return nil, err + } + accessor := sql.NewAccessor(db) + src := NewDBSource(accessor) + return src, nil +} + // Response implements cfssl.ocsp.responder.Source, which returns the // OCSP response in the Database for the given request with the expiration // date furthest in the future. Response also returns a bool that is false diff --git a/ocsp/responder_test.go b/ocsp/responder_test.go index 62e34ec75..f83534e41 100644 --- a/ocsp/responder_test.go +++ b/ocsp/responder_test.go @@ -3,9 +3,11 @@ package ocsp import ( "encoding/hex" "io/ioutil" + "math/big" "net/http" "net/http/httptest" "net/url" + "strconv" "testing" "time" @@ -166,7 +168,7 @@ func TestNewSourceFromFile(t *testing.T) { } } -func TestSqliteTrivial(t *testing.T) { +func TestResponseTrivial(t *testing.T) { // First, read and parse certificate and issuer files needed to make // an OCSP request. certFile := "testdata/sqlite_ca.pem" @@ -197,10 +199,22 @@ func TestSqliteTrivial(t *testing.T) { if err != nil { t.Errorf("Error parsing OCSP request: %s", err) } + // Truncate the Serial Number so it can fit into the DB tables. + truncSN, _ := strconv.Atoi(req.SerialNumber.String()[:20]) + req.SerialNumber = big.NewInt(int64(truncSN)) + // Create SQLite DB and accossiated accessor. sqliteDBfile := "testdata/sqlite_test.db" - db := testdb.SQLiteDB(sqliteDBfile) - accessor := sql.NewAccessor(db) + sqlitedb := testdb.SQLiteDB(sqliteDBfile) + sqliteAccessor := sql.NewAccessor(sqlitedb) + + // Create MySQL DB and accossiated accessor. + mysqldb := testdb.MySQLDB() + mysqlAccessor := sql.NewAccessor(mysqldb) + + // Create PostgreSQL DB and accossiated accessor. + postgresdb := testdb.PostgreSQLDB() + postgresAccessor := sql.NewAccessor(postgresdb) // Populate the DB with the OCSPRecord, and check // that Response() handles the request appropiately. @@ -210,28 +224,84 @@ func TestSqliteTrivial(t *testing.T) { Expiry: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), Serial: req.SerialNumber.String(), } - err = accessor.InsertOCSP(ocsp) + err = sqliteAccessor.InsertOCSP(ocsp) + if err != nil { + t.Errorf("Error inserting OCSP record into SQLite DB: %s", err) + } + + err = mysqlAccessor.InsertOCSP(ocsp) + if err != nil { + t.Errorf("Error inserting OCSP record into MySQL DB: %s", err) + } + + // Need to create and insert Certificate record into PostgreSQL + // before inserting OCSP record due to foreign key constraints + // of the Postgres tables. + certRec := certdb.CertificateRecord{ + Serial: req.SerialNumber.String(), + AKI: hex.EncodeToString(req.IssuerKeyHash), + CALabel: "Example Certificate", + Status: "Good", + Reason: 1, + Expiry: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + RevokedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + PEM: "PEM", + } + + err = postgresAccessor.InsertCertificate(certRec) + if err != nil { + t.Errorf("Error inserting Certificate record into PostgreSQL DB: %s", err) + } + + err = postgresAccessor.InsertOCSP(ocsp) if err != nil { - t.Errorf("Error inserting OCSP record into DB: %s", err) + t.Errorf("Error inserting OCSP record into PostgreSQL DB: %s", err) } // Use the created Accessor to create a new DBSource. - src := NewDBSource(accessor) + sqliteSrc := NewDBSource(sqliteAccessor) + mysqlSrc := NewDBSource(mysqlAccessor) + postgresSrc := NewDBSource(postgresAccessor) // Call Response() method on constructed request and check the output. - response, present := src.Response(req) + response, present := sqliteSrc.Response(req) if !present { - t.Error("No response present for given request") + t.Error("No response present in SQLite DB for given request") } if string(response) != "Test OCSP" { t.Error("Incorrect response received from Sqlite DB") } + + response, present = mysqlSrc.Response(req) + if !present { + t.Error("No response present in MySQL DB for given request") + } + if string(response) != "Test OCSP" { + t.Error("Incorrect response received from MySQL DB") + } + + response, present = postgresSrc.Response(req) + if !present { + t.Error("No response present in PostgreSQL DB for given request") + } + if string(response) != "Test OCSP" { + t.Error("Incorrect response received from PostgreSQL DB") + } } -func TestSqliteRealResponse(t *testing.T) { +func TestRealResponse(t *testing.T) { + // Create SQLite DB and accossiated accessor. sqliteDBfile := "testdata/sqlite_test.db" - db := testdb.SQLiteDB(sqliteDBfile) - accessor := sql.NewAccessor(db) + sqlitedb := testdb.SQLiteDB(sqliteDBfile) + sqliteAccessor := sql.NewAccessor(sqlitedb) + + // Create MySQL DB and accossiated accessor. + mysqldb := testdb.MySQLDB() + mysqlAccessor := sql.NewAccessor(mysqldb) + + // Create PostgreSQL DB and accossiated accessor. + postgresdb := testdb.PostgreSQLDB() + postgresAccessor := sql.NewAccessor(postgresdb) certFile := "testdata/cert.pem" issuerFile := "testdata/ca.pem" @@ -291,22 +361,101 @@ func TestSqliteRealResponse(t *testing.T) { Expiry: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), Serial: req.SerialNumber.String(), } - err = accessor.InsertOCSP(ocsp) + + err = sqliteAccessor.InsertOCSP(ocsp) if err != nil { - t.Errorf("Error inserting OCSP record into DB: %s", err) + t.Errorf("Error inserting OCSP record into SQLite DB: %s", err) + } + + err = mysqlAccessor.InsertOCSP(ocsp) + if err != nil { + t.Errorf("Error inserting OCSP record into MySQL DB: %s", err) + } + + // Need to create and insert Certificate record into PostgreSQL + // before inserting OCSP record due to foreign key constraints + // of the Postgres tables. + certRec := certdb.CertificateRecord{ + Serial: req.SerialNumber.String(), + AKI: hex.EncodeToString(req.IssuerKeyHash), + CALabel: "Example Certificate", + Status: "Good", + Reason: 1, + Expiry: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + RevokedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + PEM: "PEM", + } + + err = postgresAccessor.InsertCertificate(certRec) + if err != nil { + t.Errorf("Error inserting Certificate record into PostgreSQL DB: %s", err) + } + + err = postgresAccessor.InsertOCSP(ocsp) + if err != nil { + t.Errorf("Error inserting OCSP record into PostgreSQL DB: %s", err) } // Use the created Accessor to create new DBSource. - src := NewDBSource(accessor) + sqliteSrc := NewDBSource(sqliteAccessor) + mysqlSrc := NewDBSource(mysqlAccessor) + postgresSrc := NewDBSource(postgresAccessor) // Call Response() method on constructed request and check the output. - response, present := src.Response(req) + // Then, attempt to parse the returned response and make sure it is well formed. + response, present := sqliteSrc.Response(req) + if !present { + t.Error("No response present in SQLite DB for given request") + } + _, err = goocsp.ParseResponse(response, issuer) + if err != nil { + t.Errorf("Error parsing SQLite response: %v", err) + } + + response, present = mysqlSrc.Response(req) + if !present { + t.Error("No response present in MySQL DB for given request") + } + _, err = goocsp.ParseResponse(response, issuer) + if err != nil { + t.Errorf("Error parsing MySQL response: %v", err) + } + + response, present = postgresSrc.Response(req) if !present { - t.Error("No response present for given request") + t.Error("No response present in PostgreSQL for given request") } - // Attempt to parse the returned response and make sure it is well formed. _, err = goocsp.ParseResponse(response, issuer) if err != nil { - t.Errorf("Error parsing response: %v", err) + t.Errorf("Error parsing PostgreSQL response: %v", err) + } +} + +// Manually run the query "SELECT max(version_id) FROM goose_db_version;" +// on testdata/sqlite_test.db after running this test to verify that the +// DB was properly connected to. +func TestNewSqliteSource(t *testing.T) { + dbpath := "testdata/sqlite_test.db" + _, err := NewSourceFromConnString("sqlite3", dbpath) + if err != nil { + t.Errorf("Error connecting to Sqlite DB: %v", err) + } +} + +func TestNewMySQLSource(t *testing.T) { + dbpath := "root@tcp(localhost:3306)/certdb_development?parseTime=true" + // Error should be thrown here if DB cannot be connected to. + _, err := NewSourceFromConnString("mysql", dbpath) + if err != nil { + t.Errorf("Error connecting to MySQL DB: %v", err) + } +} + +func TestNewPostgresSource(t *testing.T) { + dbpath := "dbname=certdb_development sslmode=disable" + // Error should be thrown here if DB cannot be connected to. + _, err := NewSourceFromConnString("postgres", dbpath) + if err != nil { + t.Errorf("Error connecting to PostgreSQL DB: %v", err) } }