diff --git a/api/certinfo/certinfo.go b/api/certinfo/certinfo.go index 75bde07ac..eb2eb12c1 100644 --- a/api/certinfo/certinfo.go +++ b/api/certinfo/certinfo.go @@ -2,9 +2,11 @@ package certinfo import ( + "errors" "net/http" "github.com/cloudflare/cfssl/api" + "github.com/cloudflare/cfssl/certdb" "github.com/cloudflare/cfssl/certinfo" "github.com/cloudflare/cfssl/log" ) @@ -12,7 +14,9 @@ import ( // Handler accepts requests for either remote or uploaded // certificates to be bundled, and returns a certificate bundle (or // error). -type Handler struct{} +type Handler struct { + dbAccessor certdb.Accessor +} // NewHandler creates a new bundler that uses the root bundle and // intermediate bundle in the trust chain. @@ -20,12 +24,25 @@ func NewHandler() http.Handler { return api.HTTPHandler{Handler: new(Handler), Methods: []string{"POST"}} } +// NewAccessorHandler creates a new bundler with database access via the +// certdb.Accessor interface. If this handler is constructed it will be possible +// to lookup certificates issued earlier by the CA. +func NewAccessorHandler(dbAccessor certdb.Accessor) http.Handler { + return api.HTTPHandler{ + Handler: &Handler{ + dbAccessor: dbAccessor, + }, + Methods: []string{"POST"}, + } +} + // Handle implements an http.Handler interface for the bundle handler. func (h *Handler) Handle(w http.ResponseWriter, r *http.Request) (err error) { blob, matched, err := api.ProcessRequestFirstMatchOf(r, [][]string{ {"certificate"}, {"domain"}, + {"serial", "authority_key_id"}, }) if err != nil { log.Warningf("invalid request: %v", err) @@ -42,6 +59,18 @@ func (h *Handler) Handle(w http.ResponseWriter, r *http.Request) (err error) { case "certificate": if cert, err = certinfo.ParseCertificatePEM([]byte(blob["certificate"])); err != nil { log.Warningf("bad PEM certifcate: %v", err) + return err + } + case "serial", "authority_key_id": + if h.dbAccessor == nil { + log.Warning("could not find certificates with db access") + + return errors.New("cannot lookup certificate from serial without db access") + } + + if cert, err = certinfo.ParseSerialNumber(blob["serial"], blob["authority_key_id"], h.dbAccessor); err != nil { + log.Warningf("couldn't find certificate: %v", err) + return err } } diff --git a/certinfo/certinfo.go b/certinfo/certinfo.go index 412ea2cd0..6c089a0a3 100644 --- a/certinfo/certinfo.go +++ b/certinfo/certinfo.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "github.com/cloudflare/cfssl/certdb" "github.com/cloudflare/cfssl/helpers" ) @@ -164,3 +165,26 @@ func ParseCertificateDomain(domain string) (cert *Certificate, err error) { cert = ParseCertificate(conn.ConnectionState().PeerCertificates[0]) return } + +// ParseSerialNumber parses the serial number and does a lookup in the data +// storage used for certificates. The authority key is required for the lookup +// to work and must be passed as a hex string. +func ParseSerialNumber(serial, aki string, dbAccessor certdb.Accessor) (*Certificate, error) { + normalizedAKI := strings.ToLower(aki) + normalizedAKI = strings.Replace(normalizedAKI, ":", "", -1) + + certificates, err := dbAccessor.GetCertificate(serial, normalizedAKI) + if err != nil { + return nil, err + } + + if len(certificates) < 1 { + return nil, errors.New("no certificate found") + } + + if len(certificates) > 1 { + return nil, errors.New("more than one certificate found") + } + + return ParseCertificatePEM([]byte(certificates[0].PEM)) +} diff --git a/certinfo/certinfo_test.go b/certinfo/certinfo_test.go new file mode 100644 index 000000000..ada3b83ec --- /dev/null +++ b/certinfo/certinfo_test.go @@ -0,0 +1,149 @@ +package certinfo + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "strings" + "testing" + "time" + + "github.com/cloudflare/cfssl/certdb" + "github.com/cloudflare/cfssl/certdb/sql" + "github.com/cloudflare/cfssl/certdb/testdb" +) + +const ( + sqliteDBFile = "../certdb/testdb/certstore_development.db" + fakeAKI = "fake_aki" + testSerial = 1337 +) + +func TestParseSerialNumber(t *testing.T) { + db := testdb.SQLiteDB(sqliteDBFile) + accessor := sql.NewAccessor(db) + + certificate, err := createCertificate() + if err != nil { + t.Logf("could not create certificate: %s", err.Error()) + t.FailNow() + } + + err = accessor.InsertCertificate( + certdb.CertificateRecord{ + Serial: big.NewInt(testSerial).String(), + AKI: fakeAKI, + PEM: certificate, + }, + ) + + if err != nil { + t.Log(err.Error()) + t.FailNow() + } + + cases := []struct { + description string + serial string + aki string + errorShouldContain string + }{ + { + description: "no certificate found - wrong serial", + serial: "1", + aki: fakeAKI, + errorShouldContain: "no certificate found", + }, + { + description: "no certificate found - wrong AKI", + serial: "123456789", + aki: "1", + errorShouldContain: "no certificate found", + }, + { + description: "certificate found", + serial: big.NewInt(testSerial).String(), + aki: fakeAKI, + }, + } + + for _, tc := range cases { + t.Run(tc.description, func(t *testing.T) { + cert, err := ParseSerialNumber(tc.serial, tc.aki, accessor) + + if tc.errorShouldContain != "" { + if cert != nil { + t.Error("no certificate should be returned if error occurs") + } + + if err == nil { + t.Error("err expected to not be nil") + + return + } + + if !strings.Contains(err.Error(), tc.errorShouldContain) { + t.Errorf("expected error to contain '%s' but was '%s'", tc.errorShouldContain, err.Error()) + } + + return + } + + if err != nil { + t.Errorf("expected error to be nil but got '%s'", err.Error()) + + return + } + + if cert.SerialNumber != tc.serial { + t.Errorf("returned certificate doesn't match the serial queried for") + } + }) + } +} + +func createCertificate() (string, error) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return "", err + } + + cert := &x509.Certificate{ + SerialNumber: big.NewInt(testSerial), + Subject: pkix.Name{ + Country: []string{"SE"}, + Organization: []string{"CFSSL Unit Testing"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(1, 0, 0), + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + + certificate, err := x509.CreateCertificate(rand.Reader, cert, cert, &key.PublicKey, key) + if err != nil { + return "", err + } + + return certificateToPEMBlock(certificate) +} + +func certificateToPEMBlock(cert []byte) (string, error) { + buf := &bytes.Buffer{} + + err := pem.Encode(buf, &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert, + }) + + if err != nil { + return "", err + } + + return buf.String(), nil +} diff --git a/cli/certinfo/certinfo.go b/cli/certinfo/certinfo.go index 127445c23..883fafe58 100644 --- a/cli/certinfo/certinfo.go +++ b/cli/certinfo/certinfo.go @@ -7,8 +7,11 @@ import ( "errors" "fmt" + "github.com/cloudflare/cfssl/certdb/dbconf" + "github.com/cloudflare/cfssl/certdb/sql" "github.com/cloudflare/cfssl/certinfo" "github.com/cloudflare/cfssl/cli" + "github.com/jmoiron/sqlx" ) // Usage text of 'cfssl certinfo' @@ -21,12 +24,14 @@ Usage of certinfo: cfssl certinfo -csr file - Data from certificate from remote server. cfssl certinfo -domain domain_name + - Data from CA storage + cfssl certinfo -sn serial (requires -db-config and -aki) Flags: ` // flags used by 'cfssl certinfo' -var certinfoFlags = []string{"cert", "csr", "domain"} +var certinfoFlags = []string{"aki", "cert", "csr", "db-config", "domain", "serial"} // certinfoMain is the main CLI of certinfo functionality func certinfoMain(args []string, c cli.Config) (err error) { @@ -66,8 +71,25 @@ func certinfoMain(args []string, c cli.Config) (err error) { if cert, err = certinfo.ParseCertificateDomain(c.Domain); err != nil { return } + } else if c.Serial != "" && c.AKI != "" { + if c.DBConfigFile == "" { + return errors.New("need DB config file (provide with -db-config)") + } + + var db *sqlx.DB + + db, err = dbconf.DBFromConfig(c.DBConfigFile) + if err != nil { + return + } + + dbAccessor := sql.NewAccessor(db) + + if cert, err = certinfo.ParseSerialNumber(c.Serial, c.AKI, dbAccessor); err != nil { + return + } } else { - return errors.New("Must specify certinfo target through -cert, -csr, or -domain") + return errors.New("Must specify certinfo target through -cert, -csr, -domain or -serial + -aki") } var b []byte diff --git a/cli/serve/serve.go b/cli/serve/serve.go index 57c7ef590..0cc1ad65b 100644 --- a/cli/serve/serve.go +++ b/cli/serve/serve.go @@ -218,6 +218,10 @@ var endpoints = map[string]func() (http.Handler, error){ }, "certinfo": func() (http.Handler, error) { + if db != nil { + return certinfo.NewAccessorHandler(certsql.NewAccessor(db)), nil + } + return certinfo.NewHandler(), nil }, diff --git a/cmd/cfssl-certinfo/cfssl-certinfo.go b/cmd/cfssl-certinfo/cfssl-certinfo.go index 148adfe83..9c14454a0 100644 --- a/cmd/cfssl-certinfo/cfssl-certinfo.go +++ b/cmd/cfssl-certinfo/cfssl-certinfo.go @@ -8,6 +8,10 @@ import ( "github.com/cloudflare/cfssl/cli" "github.com/cloudflare/cfssl/cli/certinfo" "github.com/cloudflare/cfssl/config" + + _ "github.com/go-sql-driver/mysql" // import to support MySQL + _ "github.com/lib/pq" // import to support Postgres + _ "github.com/mattn/go-sqlite3" // import to support SQLite3 ) // main defines the newkey usage and registers all defined commands and flags. @@ -23,6 +27,8 @@ func main() { certinfo -cert file - Data from certificate from remote server. certinfo -domain domain_name + - Data from CA storage + certinfo -serial serial_number -aki authority_key_id (requires -db-config) Flags: ` @@ -68,4 +74,7 @@ func printDefaultValue(f *flag.Flag) { func registerFlags(c *cli.Config, f *flag.FlagSet) { f.StringVar(&c.CertFile, "cert", "", "Client certificate that contains the public key") f.StringVar(&c.Domain, "domain", "", "remote server domain name") + f.StringVar(&c.Serial, "serial", "", "certificate serial number") + f.StringVar(&c.AKI, "aki", "", "certificate issuer (authority) key identifier") + f.StringVar(&c.DBConfigFile, "db-config", "", "certificate db configuration file") } diff --git a/doc/api/endpoint_certinfo.txt b/doc/api/endpoint_certinfo.txt index c0181e666..339017dc5 100644 --- a/doc/api/endpoint_certinfo.txt +++ b/doc/api/endpoint_certinfo.txt @@ -5,11 +5,13 @@ Method: POST Required parameters: - One of the following two parameters is required. + One of the following parameters is required. * certificate: the PEM-encoded certificate to be parsed. * domain: a domain name indicating a remote host to retrieve a certificate for. + * serial and authority_key_id: a certificate serial number and a + matching authority key to look for in the database Result: @@ -41,7 +43,7 @@ Example: % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 3229 0 3201 100 28 66658 583 --:--:-- --:--:-- --:--:-- 68106 -curl: (6) Could not resolve host: +curl: (6) Could not resolve host: { "errors": [], "messages": [], @@ -126,4 +128,4 @@ curl: (6) Could not resolve host: } }, "success": true -} \ No newline at end of file +}