Skip to content

Commit

Permalink
Add support to lookup certificate by s/n and authkey
Browse files Browse the repository at this point in the history
Add support to lookup certificate by s/n and authkey. Fixes #982.
  • Loading branch information
bombsimon authored and cbroglie committed Apr 7, 2019
1 parent ea569c5 commit 6ab4345
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 6 deletions.
31 changes: 30 additions & 1 deletion api/certinfo/certinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,47 @@
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"
)

// 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.
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)
Expand All @@ -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
}
}
Expand Down
24 changes: 24 additions & 0 deletions certinfo/certinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"strings"
"time"

"github.com/cloudflare/cfssl/certdb"
"github.com/cloudflare/cfssl/helpers"
)

Expand Down Expand Up @@ -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))
}
149 changes: 149 additions & 0 deletions certinfo/certinfo_test.go
Original file line number Diff line number Diff line change
@@ -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
}
26 changes: 24 additions & 2 deletions cli/certinfo/certinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions cli/serve/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
},

Expand Down
9 changes: 9 additions & 0 deletions cmd/cfssl-certinfo/cfssl-certinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
`
Expand Down Expand Up @@ -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")
}
8 changes: 5 additions & 3 deletions doc/api/endpoint_certinfo.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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": [],
Expand Down Expand Up @@ -126,4 +128,4 @@ curl: (6) Could not resolve host:
}
},
"success": true
}
}

0 comments on commit 6ab4345

Please sign in to comment.